-
Notifications
You must be signed in to change notification settings - Fork 419
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #59 from mozilla-services/plug-custom-authz
Plug custom authz
- Loading branch information
Showing
18 changed files
with
606 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
from cliquet.authorization import AuthorizationPolicy as CliquetAuthorization | ||
from pyramid.security import IAuthorizationPolicy, Authenticated | ||
from zope.interface import implementer | ||
|
||
|
||
# Vocab really matters when you deal with permissions. Let's do a quick recap | ||
# of the terms used here: | ||
# | ||
# Object URI: | ||
# An unique identifier for an object. | ||
# for instance, /buckets/blog/collections/articles/records/article1 | ||
# | ||
# Object: | ||
# A common denomination of an object (e.g. "collection" or "record") | ||
# | ||
# Unbound permission: | ||
# A permission not bound to an object (e.g. "create") | ||
# | ||
# Bound permission: | ||
# A permission bound to an object (e.g. "collection:create") | ||
|
||
|
||
# Dictionary which list all permissions a given permission enables. | ||
PERMISSIONS_INHERITANCE_TREE = { | ||
'bucket:write': { | ||
'bucket': ['write'] | ||
}, | ||
'bucket:read': { | ||
'bucket': ['write', 'read'] | ||
}, | ||
'bucket:group:create': { | ||
'bucket': ['write', 'group:create'] | ||
}, | ||
'bucket:collection:create': { | ||
'bucket': ['write', 'collection:create'] | ||
}, | ||
'group:write': { | ||
'bucket': ['write'], | ||
'group': ['write'] | ||
}, | ||
'group:read': { | ||
'bucket': ['write', 'read'], | ||
'group': ['write', 'read'] | ||
}, | ||
'collection:write': { | ||
'bucket': ['write'], | ||
'collection': ['write'], | ||
}, | ||
'collection:read': { | ||
'bucket': ['write', 'read'], | ||
'collection': ['write', 'read'], | ||
}, | ||
'collection:record:create': { | ||
'bucket': ['write'], | ||
'collection': ['write', 'record:create'] | ||
}, | ||
'record:write': { | ||
'bucket': ['write'], | ||
'collection': ['write'], | ||
'record': ['write'] | ||
}, | ||
'record:read': { | ||
'bucket': ['write', 'read'], | ||
'collection': ['write', 'read'], | ||
'record': ['write', 'read'] | ||
} | ||
} | ||
|
||
|
||
def get_object_type(object_uri): | ||
"""Return the type of an object from its id.""" | ||
|
||
obj_parts = object_uri.split('/') | ||
if len(obj_parts) % 2 == 0: | ||
object_uri = '/'.join(obj_parts[:-1]) | ||
|
||
# Order matters here. More precise is tested first. | ||
if 'records' in object_uri: | ||
obj_type = 'record' | ||
elif 'collections' in object_uri: | ||
obj_type = 'collection' | ||
elif 'groups' in object_uri: | ||
obj_type = 'group' | ||
elif 'buckets' in object_uri: | ||
obj_type = 'bucket' | ||
else: | ||
obj_type = None | ||
return obj_type | ||
|
||
|
||
def build_permission_tuple(obj_type, unbound_permission, obj_parts): | ||
"""Returns a tuple of (object_uri, unbound_permission)""" | ||
PARTS_LENGTH = { | ||
'bucket': 3, | ||
'collection': 5, | ||
'group': 5, | ||
'record': 7 | ||
} | ||
if obj_type not in PARTS_LENGTH: | ||
raise ValueError('Invalid object type: %s' % obj_type) | ||
|
||
if PARTS_LENGTH[obj_type] > len(obj_parts): | ||
raise ValueError('You cannot build children keys from its parent key.' | ||
'Trying to build type "%s" from object key "%s".' % ( | ||
obj_type, '/'.join(obj_parts))) | ||
length = PARTS_LENGTH[obj_type] | ||
return ('/'.join(obj_parts[:length]), unbound_permission) | ||
|
||
|
||
def build_permissions_set(object_uri, unbound_permission, | ||
inheritance_tree=None): | ||
"""Build a set of all permissions that can grant access to the given | ||
object URI and unbound permission. | ||
>>> build_required_permissions('/buckets/blog', 'write') | ||
set(('/buckets/blog', 'write')) | ||
""" | ||
|
||
if inheritance_tree is None: | ||
inheritance_tree = PERMISSIONS_INHERITANCE_TREE | ||
|
||
obj_type = get_object_type(object_uri) | ||
|
||
bound_permission = '%s:%s' % (obj_type, unbound_permission) | ||
granters = set() | ||
|
||
obj_parts = object_uri.split('/') | ||
for obj, permission_list in inheritance_tree[bound_permission].items(): | ||
for permission in permission_list: | ||
granters.add(build_permission_tuple(obj, permission, obj_parts)) | ||
|
||
return granters | ||
|
||
|
||
@implementer(IAuthorizationPolicy) | ||
class AuthorizationPolicy(CliquetAuthorization): | ||
def get_bound_permissions(self, *args, **kwargs): | ||
return build_permissions_set(*args, **kwargs) | ||
|
||
def permits(self, context, principals, permission): | ||
is_bucket = (context.resource_name == 'bucket') | ||
if is_bucket and context.required_permission in ('create', 'read'): | ||
# XXX: Read settings. | ||
return Authenticated in principals | ||
|
||
return super(AuthorizationPolicy, self).permits(context, | ||
principals, | ||
permission) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
from kinto.authorization import (get_object_type, build_permission_tuple, | ||
build_permissions_set) | ||
|
||
from .support import unittest | ||
|
||
|
||
class PermissionInheritanceTest(unittest.TestCase): | ||
record_uri = '/buckets/blog/collections/articles/records/article1' | ||
collection_uri = '/buckets/blog/collections/articles' | ||
group_uri = '/buckets/blog/groups/moderators' | ||
bucket_uri = '/buckets/blog' | ||
invalid_uri = 'invalid object id' | ||
|
||
def test_get_object_type_return_right_type_for_key(self): | ||
self.assertEqual(get_object_type(self.record_uri), 'record') | ||
self.assertEqual(get_object_type(self.collection_uri), 'collection') | ||
self.assertEqual(get_object_type(self.bucket_uri), 'bucket') | ||
self.assertEqual(get_object_type(self.group_uri), 'group') | ||
self.assertIsNone(get_object_type(self.invalid_uri)) | ||
|
||
def test_get_object_type_return_right_type_for_children_collection(self): | ||
object_type = get_object_type(self.collection_uri + '/records') | ||
self.assertEqual(object_type, 'collection') | ||
object_type = get_object_type(self.bucket_uri + '/collections') | ||
self.assertEqual(object_type, 'bucket') | ||
object_type = get_object_type(self.bucket_uri + '/groups') | ||
self.assertEqual(object_type, 'bucket') | ||
|
||
def test_build_perm_set_uri_can_construct_parents_set_uris(self): | ||
obj_parts = self.record_uri.split('/') | ||
# Can build record_uri from obj_parts | ||
self.assertEqual( | ||
build_permission_tuple('record', 'write', obj_parts), | ||
(self.record_uri, 'write')) | ||
|
||
# Can build collection_uri from obj_parts | ||
self.assertEqual( | ||
build_permission_tuple('collection', 'records:create', obj_parts), | ||
(self.collection_uri, 'records:create')) | ||
|
||
# Can build bucket_uri from obj_parts | ||
self.assertEqual(build_permission_tuple( | ||
'bucket', 'groups:create', obj_parts), | ||
(self.bucket_uri, 'groups:create')) | ||
|
||
# Can build group_uri from group obj_parts | ||
obj_parts = self.group_uri.split('/') | ||
self.assertEqual(build_permission_tuple( | ||
'group', 'read', obj_parts), | ||
(self.group_uri, 'read')) | ||
|
||
# Can build bucket_uri from group obj_parts | ||
obj_parts = self.group_uri.split('/') | ||
self.assertEqual(build_permission_tuple( | ||
'bucket', 'write', obj_parts), | ||
(self.bucket_uri, 'write')) | ||
|
||
def test_build_permission_tuple_fail_construct_children_set_uris(self): | ||
obj_parts = self.bucket_uri.split('/') | ||
# Cannot build record_uri from bucket obj_parts | ||
self.assertRaises(ValueError, | ||
build_permission_tuple, | ||
'record', 'write', obj_parts) | ||
|
||
# Cannot build collection_uri from obj_parts | ||
self.assertRaises(ValueError, | ||
build_permission_tuple, | ||
'collection', 'write', obj_parts) | ||
|
||
# Cannot build bucket_uri from empty obj_parts | ||
self.assertRaises(ValueError, | ||
build_permission_tuple, | ||
'collection', 'write', []) | ||
|
||
def test_build_permission_tuple_fail_on_wrong_type(self): | ||
obj_parts = self.record_uri.split('/') | ||
self.assertRaises(ValueError, | ||
build_permission_tuple, | ||
'schema', 'write', obj_parts) | ||
|
||
def test_get_perm_keys_for_bucket_permission(self): | ||
# write | ||
self.assertEquals( | ||
build_permissions_set(self.bucket_uri, 'write'), | ||
set([(self.bucket_uri, 'write')])) | ||
# read | ||
self.assertEquals( | ||
build_permissions_set(self.bucket_uri, 'read'), | ||
set([(self.bucket_uri, 'write'), (self.bucket_uri, 'read')])) | ||
|
||
# group:create | ||
groups_uri = self.bucket_uri + '/groups' | ||
self.assertEquals( | ||
build_permissions_set(groups_uri, 'group:create'), | ||
set( | ||
[(self.bucket_uri, 'write'), | ||
(self.bucket_uri, 'group:create')]) | ||
) | ||
|
||
# collection:create | ||
collections_uri = self.bucket_uri + '/collections' | ||
self.assertEquals( | ||
build_permissions_set(collections_uri, 'collection:create'), | ||
set([(self.bucket_uri, 'write'), | ||
(self.bucket_uri, 'collection:create')])) | ||
|
||
def test_build_permissions_set_for_group_permission(self): | ||
# write | ||
self.assertEquals( | ||
build_permissions_set(self.group_uri, 'write'), | ||
set([(self.bucket_uri, 'write'), | ||
(self.group_uri, 'write')])) | ||
# read | ||
self.assertEquals( | ||
build_permissions_set(self.group_uri, 'read'), | ||
set([(self.bucket_uri, 'write'), | ||
(self.bucket_uri, 'read'), | ||
(self.group_uri, 'write'), | ||
(self.group_uri, 'read')])) | ||
|
||
def test_build_permissions_set_for_collection_permission(self): | ||
# write | ||
self.assertEquals( | ||
build_permissions_set(self.collection_uri, 'write'), | ||
set([(self.bucket_uri, 'write'), | ||
(self.collection_uri, 'write')])) | ||
# read | ||
self.assertEquals( | ||
build_permissions_set(self.collection_uri, 'read'), | ||
set([(self.bucket_uri, 'write'), | ||
(self.bucket_uri, 'read'), | ||
(self.collection_uri, 'write'), | ||
(self.collection_uri, 'read')])) | ||
# records:create | ||
records_uri = self.collection_uri + '/records' | ||
self.assertEquals( | ||
build_permissions_set(records_uri, 'record:create'), | ||
set([(self.bucket_uri, 'write'), | ||
(self.collection_uri, 'write'), | ||
(self.collection_uri, 'record:create')])) | ||
|
||
def test_build_permissions_set_for_record_permission(self): | ||
# write | ||
self.assertEquals( | ||
build_permissions_set(self.record_uri, 'write'), | ||
set([(self.bucket_uri, 'write'), | ||
(self.collection_uri, 'write'), | ||
(self.record_uri, 'write')])) | ||
# read | ||
self.assertEquals( | ||
build_permissions_set(self.record_uri, 'read'), | ||
set([(self.bucket_uri, 'write'), | ||
(self.bucket_uri, 'read'), | ||
(self.collection_uri, 'write'), | ||
(self.collection_uri, 'read'), | ||
(self.record_uri, 'write'), | ||
(self.record_uri, 'read')])) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.