diff --git a/lib/remote_storage/riak.rb b/lib/remote_storage/riak.rb index 9b54361..de9014a 100644 --- a/lib/remote_storage/riak.rb +++ b/lib/remote_storage/riak.rb @@ -13,11 +13,16 @@ module RemoteStorage end def authorize_request(user, category, token) - return true if category == "public" && env["REQUEST_METHOD"] == "GET" + request_method = env["REQUEST_METHOD"] + return true if category == "public" && request_method == "GET" - categories = client.bucket("authorizations").get("#{user}:#{token}").data + authorizations = client.bucket("authorizations").get("#{user}:#{token}").data + permission = category_permission(authorizations, category) - halt 403 unless categories.include?(category) + halt 403 unless permission + if ["PUT", "DELETE"].include? request_method + halt 403 unless permission == "rw" + end rescue ::Riak::HTTPFailedRequest halt 403 end @@ -63,5 +68,24 @@ module RemoteStorage ::Riak::Serializers[content_type[/^[^;\s]+/]] end + def category_permission(authorizations, category) + authorizations = authorizations.map do |auth| + auth.index(":") ? auth.split(":") : [auth, "rw"] + end + authorizations = Hash[*authorizations.flatten] + + permission = authorizations[""] + + authorizations.each do |key, value| + if category.match key + if permission.nil? || permission == "r" + permission = value + end + end + end + + permission + end + end end diff --git a/liquor-cabinet.rb b/liquor-cabinet.rb index f448dad..3c836cb 100644 --- a/liquor-cabinet.rb +++ b/liquor-cabinet.rb @@ -29,23 +29,23 @@ class LiquorCabinet < Sinatra::Base disable :logging end - before "/:user/:category/:key" do + before "/:user/*/:key" do headers 'Access-Control-Allow-Origin' => '*', 'Access-Control-Allow-Methods' => 'GET, PUT, DELETE', 'Access-Control-Allow-Headers' => 'Authorization, Content-Type, Origin' headers['Access-Control-Allow-Origin'] = env["HTTP_ORIGIN"] if env["HTTP_ORIGIN"] - @user, @category, @key = params[:user], params[:category], params[:key] + @user, @category, @key = params[:user], params[:splat].first, params[:key] token = env["HTTP_AUTHORIZATION"] ? env["HTTP_AUTHORIZATION"].split(" ")[1] : "" authorize_request(@user, @category, token) unless request.options? end - get "/:user/:category/:key" do + get "/:user/*/:key" do get_data(@user, @category, @key) end - put "/:user/:category/:key" do + put "/:user/*/:key" do data = request.body.read if env['CONTENT_TYPE'] == "application/x-www-form-urlencoded" @@ -57,11 +57,11 @@ class LiquorCabinet < Sinatra::Base put_data(@user, @category, @key, data, content_type) end - delete "/:user/:category/:key" do + delete "/:user/*/:key" do delete_data(@user, @category, @key) end - options "/:user/:category/:key" do + options "/:user/*/:key" do halt 200 end diff --git a/spec/permissions_spec.rb b/spec/permissions_spec.rb new file mode 100644 index 0000000..808902f --- /dev/null +++ b/spec/permissions_spec.rb @@ -0,0 +1,240 @@ +require_relative "spec_helper" + +describe "Permissions" do + include Rack::Test::Methods + include RemoteStorage::Riak + + def app + LiquorCabinet + end + + def storage_client + @storage_client ||= ::Riak::Client.new(settings.riak_config) + end + + def data_bucket + @data_bucket ||= storage_client.bucket("user_data") + end + + def auth_bucket + @auth_bucket ||= storage_client.bucket("authorizations") + end + + describe "public data" do + describe "GET" do + before do + object = data_bucket.new("jimmy:public:foo") + object.content_type = "text/plain" + object.data = "some text data" + object.store + end + + after do + data_bucket.delete("jimmy:public:foo") + end + + it "returns the value on all get requests" do + get "/jimmy/public/foo" + + last_response.status.must_equal 200 + last_response.body.must_equal "some text data" + end + end + end + + describe "private data" do + describe "GET" do + before do + object = data_bucket.new("jimmy:documents:foo") + object.content_type = "text/plain" + object.data = "some private, authorized text data" + object.store + + object = data_bucket.new("jimmy:documents/very/interesting:text") + object.content_type = "text/plain" + object.data = "some very interesting writing" + object.store + + object = data_bucket.new("jimmy:confidential:bar") + object.content_type = "text/plain" + object.data = "some private, non-authorized text data" + object.store + + auth = auth_bucket.new("jimmy:123") + auth.data = ["documents:r", "tasks:rw"] + auth.store + + header "Authorization", "Bearer 123" + end + + after do + data_bucket.delete("jimmy:documents:foo") + data_bucket.delete("jimmy:documents/very/interesting:text") + data_bucket.delete("jimmy:confidential:bar") + auth_bucket.delete("jimmy:123") + end + + describe "when authorized" do + it "returns the value for a key in a top-level directory" do + get "/jimmy/documents/foo" + + last_response.status.must_equal 200 + last_response.body.must_equal "some private, authorized text data" + end + + it "returns the value for a key in a sub-directory" do + get "/jimmy/documents/very/interesting/text" + + last_response.status.must_equal 200 + last_response.body.must_equal "some very interesting writing" + end + end + + describe "when not authorized" do + it "returns a 403 for a key in a top-level directory" do + get "/jimmy/confidential/bar" + + last_response.status.must_equal 403 + end + end + end + + describe "PUT" do + before do + auth = auth_bucket.new("jimmy:123") + auth.data = ["documents:r", "contacts:rw", "tasks:r", "tasks/home:rw"] + auth.store + + header "Authorization", "Bearer 123" + end + + after do + auth_bucket.delete("jimmy:123") + end + + describe "to a top-level directory" do + after do + data_bucket.delete("jimmy:contacts:1") + end + + it "saves the value when there are write permissions" do + put "/jimmy/contacts/1", "John Doe" + + last_response.status.must_equal 200 + data_bucket.get("jimmy:contacts:1").data.must_equal "John Doe" + end + + it "returns a 403 when there are read permissions only" do + put "/jimmy/documents/foo", "some text" + + last_response.status.must_equal 403 + end + end + + describe "to a sub-directory" do + after do + data_bucket.delete("jimmy:tasks/home:1") + data_bucket.delete("jimmy:contacts/family:1") + end + + it "saves the value when there are direct write permissions" do + put "/jimmy/tasks/home/1", "take out the trash" + + last_response.status.must_equal 200 + data_bucket.get("jimmy:tasks/home:1").data.must_equal "take out the trash" + end + + it "saves the value when there are write permissions for a parent directory" do + put "/jimmy/contacts/family/1", "Bobby Brother" + + last_response.status.must_equal 200 + data_bucket.get("jimmy:contacts/family:1").data.must_equal "Bobby Brother" + end + + it "returns a 403 when there are read permissions only" do + put "/jimmy/documents/business/1", "some text" + + last_response.status.must_equal 403 + end + end + end + + describe "DELETE" do + before do + auth = auth_bucket.new("jimmy:123") + auth.data = ["documents:r", "tasks:rw"] + auth.store + + header "Authorization", "Bearer 123" + end + + after do + auth_bucket.delete("jimmy:123") + end + + describe "when authorized" do + before do + object = data_bucket.new("jimmy:tasks:1") + object.content_type = "text/plain" + object.data = "do the laundry" + object.store + + object = data_bucket.new("jimmy:tasks/home:1") + object.content_type = "text/plain" + object.data = "take out the trash" + object.store + end + + it "removes the key from a top-level directory" do + delete "/jimmy/tasks/1" + + last_response.status.must_equal 204 + lambda { + data_bucket.get("jimmy:tasks:1") + }.must_raise Riak::HTTPFailedRequest + end + + it "removes the key from a top-level directory" do + delete "/jimmy/tasks/home/1" + + last_response.status.must_equal 204 + lambda { + data_bucket.get("jimmy:tasks/home:1") + }.must_raise Riak::HTTPFailedRequest + end + end + + describe "when not authorized" do + before do + object = data_bucket.new("jimmy:documents:private") + object.content_type = "text/plain" + object.data = "some private, authorized text data" + object.store + + object = data_bucket.new("jimmy:documents/business:foo") + object.content_type = "text/plain" + object.data = "some private, authorized text data" + object.store + end + + after do + data_bucket.delete("jimmy:documents:private") + data_bucket.delete("jimmy:documents/business:foo") + end + + it "returns a 403 for a key in a top-level directory" do + delete "/jimmy/documents/private" + + last_response.status.must_equal 403 + end + + it "returns a 403 for a key in a sub-directory" do + delete "/jimmy/documents/business/foo" + + last_response.status.must_equal 403 + end + end + end + end + +end