Skip to content

Commit

Permalink
Replace user/token with principal/anchor. Fixes #156.
Browse files Browse the repository at this point in the history
The ‘user’ role is now ‘principal’. Having an anchor (previously a
token) no longer grants the ‘principal’ role.
  • Loading branch information
jace committed Feb 5, 2018
1 parent 833a435 commit 54f5e44
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 63 deletions.
66 changes: 33 additions & 33 deletions coaster/sqlalchemy/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
Access is defined as one of 'call' (for methods), 'read' or 'write' (both for attributes).
Roles are freeform string tokens. A model may freely define and grant roles to
users based on internal criteria. The following standard tokens are
users and other principals based on internal criteria. The following standard tokens are
recommended. Required tokens are granted by :class:`RoleMixin` itself.
1. ``all``: Any user, authenticated or anonymous (required)
2. ``anon``: Anonymous user (required)
3. ``user``: Logged in user or user token (required)
1. ``all``: Any principal, authenticated or anonymous (required)
2. ``anon``: Anonymous principal (required)
3. ``principal``: Authenticated principal (required)
4. ``creator``: The creator of an object (may or may not be the current owner)
5. ``owner``: The current owner of an object
6. ``author``: Author of the object's contents
Expand Down Expand Up @@ -95,13 +95,13 @@ def title(self, value):
def hello(self):
return "Hello!"
# Your model is responsible for granting roles given a user or
# user token. The format of tokens is not specified by RoleMixin.
# Your model is responsible for granting roles given a principal or
# an anchor. The format of anchors is not specified by RoleMixin.
def roles_for(self, user=None, token=None):
def roles_for(self, principal=None, anchor=None):
# Calling super give us a result set with the standard roles
result = super(RoleModel, self).roles_for(user, token)
if token == 'owner-secret':
result = super(RoleModel, self).roles_for(principal, anchor)
if anchor == 'owner-secret':
result.add('owner') # Grant owner role
return result
"""
Expand Down Expand Up @@ -308,39 +308,39 @@ class RoleMixin(object):
# This empty dictionary is necessary for the configure step below to work
__roles__ = {}

def roles_for(self, user=None, token=None):
def roles_for(self, principal=None, anchor=None):
"""
Return roles available to the given ``user`` or ``token`` on this
Return roles available to the given ``principal`` or ``anchor`` on this
object. The data type for both parameters are intentionally undefined
here. Subclasses are free to define them in any way appropriate. Users
and tokens are assumed to be valid.
here. Subclasses are free to define them in any way appropriate. Principals
and anchors are assumed to be valid.
The role ``all`` is always granted. If either ``user`` or ``token`` is
specified, the role ``user`` is granted. If neither, ``anon`` is
The role ``all`` is always granted. If ``principal`` is
specified, the role ``principal`` is granted. If not, ``anon`` is
granted.
"""
if user is not None and token is not None:
raise TypeError('Either user or token must be specified, not both')
if principal is not None and anchor is not None:
raise TypeError('Either principal or anchor must be specified, not both')

if user is None and token is None:
if principal is None:
result = {'all', 'anon'}
else:
result = {'all', 'user'}
result = {'all', 'principal'}
return result

def users_with(self, roles):
def principals_with(self, roles):
"""
Return an iterable of all users who have the specified roles on this
Return an iterable of all principals who have the specified roles on this
object. The iterable may be a list, tuple, set or SQLAlchemy query.
Must be implemented by subclasses.
"""
raise NotImplementedError('Subclasses must implement users_with')
raise NotImplementedError('Subclasses must implement principals_with')

def make_token_for(self, user, roles=None, token=None):
def make_token_for(self, principal, roles=None, token=None):
"""
Generate a token for the specified user that grants access to this
object alone, with either all roles available to the user, or just
Generate a token for the specified principal that grants access to this
object alone, with either all roles available to the principal, or just
the specified subset. If an existing token is available, add to it.
This method should return ``None`` if a token cannot be generated.
Expand All @@ -351,22 +351,22 @@ def make_token_for(self, user, roles=None, token=None):
# libmacaroons.
raise NotImplementedError('Subclasses must implement make_token_for')

def access_for(self, roles=None, user=None, token=None):
def access_for(self, roles=None, principal=None, anchor=None):
"""
Return a proxy object that limits read and write access to attributes
based on the user's roles. If the ``roles`` parameter isn't provided,
but a ``user`` or ``token`` is provided instead, :meth:`roles_for` is
based on the principal's roles. If the ``roles`` parameter isn't provided,
but a ``principal`` or ``anchor`` is provided instead, :meth:`roles_for` is
called::
# This typical call:
obj.access_for(user=current_auth.user)
obj.access_for(principal=current_auth.principal)
# Is shorthand for:
obj.access_for(roles=obj.roles_for(user=current_auth.user))
obj.access_for(roles=obj.roles_for(principal=current_auth.principal))
"""
if roles is None:
roles = self.roles_for(user=user, token=token)
elif user is not None or token is not None:
raise TypeError('If roles are specified, user and token must not be specified')
roles = self.roles_for(principal=principal, anchor=anchor)
elif principal is not None or anchor is not None:
raise TypeError('If roles are specified, principal and anchor must not be specified')
return RoleAccessProxy(self, roles=roles)


Expand Down
60 changes: 30 additions & 30 deletions tests/test_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ class RoleModel(DeclaredAttrMixin, RoleMixin, db.Model):
def hello(self):
return "Hello!"

# Your model is responsible for granting roles given a user or
# user token. The format of tokens is not specified by RoleMixin.
# Your model is responsible for granting roles given a principal or
# anchor. The format of anchors is not specified by RoleMixin.

def roles_for(self, user=None, token=None):
def roles_for(self, principal=None, anchor=None):
# Calling super give us a result set with the standard roles
result = super(RoleModel, self).roles_for(user, token)
if token == 'owner-secret':
result = super(RoleModel, self).roles_for(principal, anchor)
if anchor == 'owner-secret':
result.add('owner') # Grant owner role
return result

Expand Down Expand Up @@ -157,43 +157,43 @@ def test_uuidmixin_roles(self):
self.assertLessEqual({'uuid', 'url_id', 'buid', 'suuid'}, UuidModel.__roles__['all']['read'])

def test_roles_for_anon(self):
"""An anonymous user should have 'all' and 'anon' roles"""
"""An anonymous principal should have 'all' and 'anon' roles"""
rm = RoleModel(name=u'test', title=u'Test')
roles = rm.roles_for(user=None)
roles = rm.roles_for(principal=None)
self.assertEqual(roles, {'all', 'anon'})

def test_roles_for_user(self):
"""A user or token must have 'all' and 'user' roles"""
def test_roles_for_principal(self):
"""A principal (but not anchor) must have 'all' and 'principal' roles"""
rm = RoleModel(name=u'test', title=u'Test')
roles = rm.roles_for(user=1)
self.assertEqual(roles, {'all', 'user'})
roles = rm.roles_for(token=1)
self.assertEqual(roles, {'all', 'user'})
roles = rm.roles_for(principal=1)
self.assertEqual(roles, {'all', 'principal'})
roles = rm.roles_for(anchor=1)
self.assertEqual(roles, {'all', 'anon'})

def test_roles_for_owner(self):
"""Presenting the correct owner token grants 'owner' role"""
"""Presenting the correct anchor grants 'owner' role"""
rm = RoleModel(name=u'test', title=u'Test')
roles = rm.roles_for(token='owner-secret')
self.assertEqual(roles, {'all', 'user', 'owner'})
roles = rm.roles_for(anchor='owner-secret')
self.assertEqual(roles, {'all', 'anon', 'owner'})

def test_access_for_syntax(self):
"""access_for can be called with either roles or user for identical outcomes"""
"""access_for can be called with either roles or principal for identical outcomes"""
rm = RoleModel(name=u'test', title=u'Test')
proxy1 = rm.access_for(roles=rm.roles_for(user=None))
proxy2 = rm.access_for(user=None)
proxy1 = rm.access_for(roles=rm.roles_for(principal=None))
proxy2 = rm.access_for(principal=None)
self.assertEqual(proxy1, proxy2)

def test_access_for_all(self):
"""All users should be able to read some fields"""
"""All principals should be able to read some fields"""
arm = AutoRoleModel(name=u'test')
proxy = arm.access_for(user=None)
proxy = arm.access_for(principal=None)
self.assertEqual(len(proxy), 2)
self.assertEqual(set(proxy.keys()), {'id', 'name'})

def test_attr_dict_access(self):
"""Proxies support identical attribute and dictionary access"""
rm = RoleModel(name=u'test', title=u'Test')
proxy = rm.access_for(user=None)
proxy = rm.access_for(principal=None)
self.assertIn('name', proxy)
self.assertEqual(proxy.name, u'test')
self.assertEqual(proxy['name'], u'test')
Expand Down Expand Up @@ -265,18 +265,18 @@ def test_bad_decorator(self):
def foo():
pass

def test_roles_for_user_and_token(self):
"""roles_for accepts user or token, not both"""
def test_roles_for_principal_and_anchor(self):
"""roles_for accepts principal or anchor, not both"""
rm = RoleModel(name=u'test', title=u'Test')
with self.assertRaises(TypeError):
rm.roles_for(user=1, token='owner-secret')
rm.roles_for(principal=1, anchor='owner-secret')

def test_access_for_roles_and_user_or_token(self):
"""access_for accepts roles or user/token, not both/all"""
def test_access_for_roles_and_principal_or_anchor(self):
"""access_for accepts roles or principal/anchor, not both/all"""
rm = RoleModel(name=u'test', title=u'Test')
with self.assertRaises(TypeError):
rm.access_for(roles={'all'}, user=1)
rm.access_for(roles={'all'}, principal=1)
with self.assertRaises(TypeError):
rm.access_for(roles={'all'}, token='owner-secret')
rm.access_for(roles={'all'}, anchor='owner-secret')
with self.assertRaises(TypeError):
rm.access_for(roles={'all'}, user=1, token='owner-secret')
rm.access_for(roles={'all'}, principal=1, anchor='owner-secret')

0 comments on commit 54f5e44

Please sign in to comment.