Skip to content

Commit

Permalink
Merge pull request #59 from mozilla-services/plug-custom-authz
Browse files Browse the repository at this point in the history
Plug custom authz
  • Loading branch information
Natim committed Jun 16, 2015
2 parents 4ffc9d7 + e131085 commit 0a31a1a
Show file tree
Hide file tree
Showing 18 changed files with 606 additions and 59 deletions.
1 change: 1 addition & 0 deletions config/kinto.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ cliquet.cache_url = postgres://postgres:postgres@localhost/postgres
cliquet.storage_backend = cliquet.storage.postgresql
cliquet.storage_url = postgres://postgres:postgres@localhost/postgres
cliquet.http_scheme = http
cliquet.http_host = localhost:8888
cliquet.retry_after_seconds = 30
cliquet.eos =

Expand Down
11 changes: 10 additions & 1 deletion kinto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@
logger = logging.getLogger(__name__)


DEFAULT_SETTINGS = {
'kinto.buckets_creation_allowed_principals': 'system.Authenticated',
'multiauth.authorization_policy': (
'kinto.authorization.AuthorizationPolicy')
}


def main(global_config, **settings):
config = Configurator(settings=settings, root_factory=RouteFactory)
initialize_cliquet(config, version=__version__)
initialize_cliquet(config,
version=__version__,
default_settings=DEFAULT_SETTINGS)
config.scan("kinto.views")
return config.make_wsgi_app()
149 changes: 149 additions & 0 deletions kinto/authorization.py
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)
3 changes: 3 additions & 0 deletions kinto/tests/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ def _get_test_app(self, settings=None):

def get_app_settings(self, additional_settings=None):
settings = cliquet_support.DEFAULT_SETTINGS.copy()
settings['cliquet.cache_backend'] = 'cliquet.cache.memory'
settings['cliquet.storage_backend'] = 'cliquet.storage.memory'
settings['cliquet.permission_backend'] = 'cliquet.permission.memory'
settings['cliquet.project_name'] = 'cloud storage'
settings['cliquet.project_docs'] = 'https://kinto.rtfd.org/'
settings['multiauth.authorization_policy'] = (
Expand Down
157 changes: 157 additions & 0 deletions kinto/tests/test_authorization.py
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')]))
10 changes: 3 additions & 7 deletions kinto/tests/test_views_buckets.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ class BucketViewTest(BaseWebTest, unittest.TestCase):

def setUp(self):
super(BucketViewTest, self).setUp()
bucket = MINIMALIST_BUCKET.copy()
bucket['permissions'] = {'read': ['system.Authenticated']}
resp = self.app.put_json(self.record_url,
MINIMALIST_BUCKET,
bucket,
headers=self.headers)
self.record = resp.json['data']

Expand All @@ -39,9 +41,6 @@ def test_buckets_name_should_be_simple(self):
headers=self.headers,
status=400)

def test_current_user_receives_write_permission_on_creation(self):
pass


class BucketDeletionTest(BaseWebTest, unittest.TestCase):

Expand Down Expand Up @@ -85,6 +84,3 @@ def test_every_records_are_deleted_too(self):
self.app.put_json(self.collection_url, MINIMALIST_COLLECTION,
headers=self.headers)
self.app.get(self.record_url, headers=self.headers, status=404)

def test_permissions_associated_are_deleted_too(self):
pass
12 changes: 6 additions & 6 deletions kinto/tests/test_views_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@ def test_collections_name_should_be_simple(self):
headers=self.headers,
status=400)

def test_unknown_bucket_raises_404(self):
def test_unknown_bucket_raises_403(self):
other_bucket = self.collections_url.replace('beers', 'sodas')
self.app.get(other_bucket, headers=self.headers, status=404)
self.app.get(other_bucket, headers=self.headers, status=403)

def test_collections_are_isolated_by_bucket(self):
other_bucket = self.collection_url.replace('beers', 'water')
other_bucket = self.collection_url.replace('beers', 'sodas')
self.app.put_json('/buckets/sodas',
MINIMALIST_BUCKET,
headers=self.headers)
self.app.get(other_bucket, headers=self.headers, status=404)


Expand Down Expand Up @@ -69,6 +72,3 @@ def test_records_of_collection_are_deleted_too(self):
self.app.put_json(self.collection_url, MINIMALIST_COLLECTION,
headers=self.headers)
self.app.get(self.record_url, headers=self.headers, status=404)

def test_permissions_associated_are_deleted_too(self):
pass

0 comments on commit 0a31a1a

Please sign in to comment.