Support for new read/write permission system
This commit is contained in:
parent
01017a9d9c
commit
3d573b51ac
@ -13,11 +13,16 @@ module RemoteStorage
|
|||||||
end
|
end
|
||||||
|
|
||||||
def authorize_request(user, category, token)
|
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
|
rescue ::Riak::HTTPFailedRequest
|
||||||
halt 403
|
halt 403
|
||||||
end
|
end
|
||||||
@ -63,5 +68,24 @@ module RemoteStorage
|
|||||||
::Riak::Serializers[content_type[/^[^;\s]+/]]
|
::Riak::Serializers[content_type[/^[^;\s]+/]]
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
@ -29,23 +29,23 @@ class LiquorCabinet < Sinatra::Base
|
|||||||
disable :logging
|
disable :logging
|
||||||
end
|
end
|
||||||
|
|
||||||
before "/:user/:category/:key" do
|
before "/:user/*/:key" do
|
||||||
headers 'Access-Control-Allow-Origin' => '*',
|
headers 'Access-Control-Allow-Origin' => '*',
|
||||||
'Access-Control-Allow-Methods' => 'GET, PUT, DELETE',
|
'Access-Control-Allow-Methods' => 'GET, PUT, DELETE',
|
||||||
'Access-Control-Allow-Headers' => 'Authorization, Content-Type, Origin'
|
'Access-Control-Allow-Headers' => 'Authorization, Content-Type, Origin'
|
||||||
headers['Access-Control-Allow-Origin'] = env["HTTP_ORIGIN"] if env["HTTP_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] : ""
|
token = env["HTTP_AUTHORIZATION"] ? env["HTTP_AUTHORIZATION"].split(" ")[1] : ""
|
||||||
|
|
||||||
authorize_request(@user, @category, token) unless request.options?
|
authorize_request(@user, @category, token) unless request.options?
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/:user/:category/:key" do
|
get "/:user/*/:key" do
|
||||||
get_data(@user, @category, @key)
|
get_data(@user, @category, @key)
|
||||||
end
|
end
|
||||||
|
|
||||||
put "/:user/:category/:key" do
|
put "/:user/*/:key" do
|
||||||
data = request.body.read
|
data = request.body.read
|
||||||
|
|
||||||
if env['CONTENT_TYPE'] == "application/x-www-form-urlencoded"
|
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)
|
put_data(@user, @category, @key, data, content_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
delete "/:user/:category/:key" do
|
delete "/:user/*/:key" do
|
||||||
delete_data(@user, @category, @key)
|
delete_data(@user, @category, @key)
|
||||||
end
|
end
|
||||||
|
|
||||||
options "/:user/:category/:key" do
|
options "/:user/*/:key" do
|
||||||
halt 200
|
halt 200
|
||||||
end
|
end
|
||||||
|
|
||||||
|
240
spec/permissions_spec.rb
Normal file
240
spec/permissions_spec.rb
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user