Created
August 30, 2012 23:06
-
-
Save ThiefMaster/3544241 to your computer and use it in GitHub Desktop.
Privilege system with nested privilege groups and inheritance
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
# vim: fileencoding=utf8 | |
"""Privilege system. | |
Stores boolean privileges which can be put in nested groups. | |
By default all privileges "bubble", i.e. they enable all (bubbling) parent | |
groups. When a group is set in a privilege set all children are set to the | |
same state. Basically a group is considered active when it has at least one | |
bubbling privilege/group that is active. | |
>>> privs = Privileges() | |
>>> privs | |
<Privileges([])> | |
>>> privs.add_priv('team', 'Team member') | |
>>> manage_users = privs.add_priv_group('manage_users', 'Access User Management') | |
>>> manage_users | |
<PrivilegeGroup(manage_users, [])> | |
>>> manage_users.add_priv('edit', 'Edit users') | |
>>> manage_users.add_priv('delete', 'Delete users') | |
>>> manage_users | |
<PrivilegeGroup(manage_users, [edit, delete])> | |
>>> manage_files = privs.add_priv_group('manage_files', 'Access File Management') | |
>>> manage_files.add_priv('add', 'Upload files') | |
>>> manage_files.add_priv('delete', 'Delete files') | |
>>> manage_files.add_priv('foreign', 'Permissions apply to foreign files', bubble=False) | |
>>> manage_content = privs.add_priv_group('manage_content', 'Content Management') | |
>>> manage_content.add_priv('list', 'See content list') | |
>>> manage_content.add_priv('edit', 'Post/edit content', imply='list') | |
>>> manage_content.add_priv('delete', 'Delete content', imply='list') | |
>>> manage_content.add_priv('proofread', 'Proofread content', imply=('list', 'edit')) | |
>>> manage_content.add_priv('x', 'Multi-level imply', imply='proofread') | |
>>> manage_content.add_priv('preview', 'Access unpublished content', bubble=False) | |
>>> mc_types = manage_content.add_priv_group('types', 'Accessible content types', bubble=False, | |
... is_priv=False) | |
>>> mc_types.add_priv('news', 'News') | |
>>> mc_types.add_priv('reviews', 'Reviews') | |
>>> privs | |
<Privileges([team, manage_users, manage_files, manage_content])> | |
>>> 'team' in privs | |
True | |
>>> 'manage_content' in privs | |
True | |
>>> 'manage_content.edit' in privs | |
True | |
>>> 'manage_content.edit.foo' in privs | |
False | |
>>> 'foo' in privs | |
False | |
>>> '' in privs | |
False | |
>>> len(list(privs._iter_deep())) | |
14 | |
>>> len(list(manage_content._iter_deep())) | |
8 | |
>>> len(list(manage_content._iter_deep(True))) | |
9 | |
>>> ps = privs.make_privset() | |
>>> ps | |
<PrivilegeSet([])> | |
>>> ps.privs | |
[] | |
>>> ps.set('foo', True) | |
Traceback (most recent call last): | |
... | |
ValueError: Privilege not found: foo | |
>>> ps.check('foo.bar.baz') | |
Traceback (most recent call last): | |
... | |
ValueError: Privilege not found: foo.bar.baz | |
>>> ps.check('manage_content.types') | |
Traceback (most recent call last): | |
... | |
ValueError: Not a privilege: manage_content.types | |
>>> ps.set('team', True) | |
>>> ps.set('manage_content.types.news', True) | |
>>> ps.privs | |
['manage_content.types.news', 'team'] | |
>>> ps.set('manage_content.types', True) | |
>>> ps.privs | |
['manage_content.types.news', 'manage_content.types.reviews', 'team'] | |
>>> ps.set('manage_content.edit', True) | |
>>> ps.privs | |
... # doctest: +NORMALIZE_WHITESPACE | |
['manage_content', 'manage_content.edit', 'manage_content.list', | |
'manage_content.types.news', 'manage_content.types.reviews', 'team'] | |
>>> ps.set('manage_content.types', False) | |
>>> ps.privs | |
['manage_content', 'manage_content.edit', 'manage_content.list', 'team'] | |
>>> ps2 = privs.make_privset(ps.privs) | |
>>> ps3 = privs.make_privset(str(ps)) | |
>>> ps.clear() | |
>>> ps.privs | |
[] | |
>>> ps2.privs | |
['manage_content', 'manage_content.edit', 'manage_content.list', 'team'] | |
>>> ps2 | |
<PrivilegeSet([manage_content.edit, manage_content.list, team])> | |
>>> set(ps2.privs) == set(ps3.privs) | |
True | |
>>> ps.set('manage_content.x', True) | |
>>> ps.privs | |
... # doctest: +NORMALIZE_WHITESPACE | |
['manage_content', 'manage_content.edit', 'manage_content.list', 'manage_content.proofread', | |
'manage_content.x'] | |
>>> ps.set('manage_content.edit', False) | |
>>> ps.privs | |
... # doctest: +NORMALIZE_WHITESPACE | |
['manage_content', 'manage_content.list'] | |
""" | |
from collections import OrderedDict | |
import itertools | |
class _PrivilegeContainer(object): | |
def __init__(self): | |
self._privs = OrderedDict() | |
self._groups = OrderedDict() | |
def add_priv(self, name, title=None, **kwargs): | |
"""Adds a new privilege to the container""" | |
if name in self._privs or name in self._groups: | |
raise ValueError('Privilege name is not unique: %s' % name) | |
self._privs[name] = _Privilege(self, name, title or name, **kwargs) | |
def add_priv_group(self, name, title=None, **kwargs): | |
"""Adds a new privilege group to the container""" | |
if name in self._groups or name in self._privs: | |
raise ValueError('Group name is not unique: %s' % name) | |
group = _PrivilegeGroup(self, name, title or name, **kwargs) | |
self._groups[name] = group | |
return group | |
def all(self): | |
"""Yields all contained privs and groups.""" | |
for item in self._iter_deep(): | |
yield item | |
def _lookup(self, path): | |
if not path: | |
return self | |
if path[0] in self._groups: | |
return self._groups[path[0]]._lookup(path[1:]) | |
if path[0] in self._privs: | |
return self._privs[path[0]]._lookup(path[1:]) | |
abs_path = '.'.join(itertools.chain(self._get_path(), path)) | |
raise ValueError('Privilege not found: %s' % abs_path) | |
def _iter_deep(self, groups=False, only_bubbling=False): | |
for priv in self._privs.itervalues(): | |
if not only_bubbling or priv.bubble: | |
yield priv | |
for group in self._groups.itervalues(): | |
if only_bubbling and not priv.bubble: | |
continue | |
if groups: | |
yield group | |
for child in group._iter_deep(groups, only_bubbling): | |
yield child | |
def __iter__(self): | |
for priv in self._privs.itervalues(): | |
yield priv | |
for group in self._groups.itervalues(): | |
yield group | |
def __contains__(self, item): | |
try: | |
self._lookup(item.split('.')) | |
except ValueError: | |
return False | |
return True | |
class _PrivilegeBase(object): | |
is_group = None | |
is_priv = True | |
def __init__(self, parent, name, title, bubble=True, imply=()): | |
super(_PrivilegeBase, self).__init__() | |
self.parent = parent | |
self.name = name | |
self.title = title | |
self.bubble = bubble | |
if isinstance(imply, basestring): | |
self.imply = set(imply.split(',')) | |
else: | |
self.imply = set(imply) | |
def _get_path(self): | |
return (x.name for x in self._get_chain()) | |
def _get_chain(self): | |
chain = [] | |
elem = self | |
while elem.parent: | |
chain.append(elem) | |
elem = elem.parent | |
return reversed(chain) | |
@property | |
def full_title(self): | |
"""Contains the title prepended by the parents' titles""" | |
titles = (x.title for x in self._get_chain()) | |
return ': '.join(titles) | |
@property | |
def path(self): | |
"""Contains the full name/path of the privilege/group""" | |
return '.'.join(self._get_path()) | |
@property | |
def siblings(self): | |
"""Contains all siblings of the privilege/group""" | |
siblings = [] | |
for group in self.parent._groups.itervalues(): | |
siblings.append(group) | |
for priv in self.parent._privs.itervalues(): | |
siblings.append(priv) | |
return siblings | |
def __repr__(self): | |
type_name = type(self).__name__[1:] | |
if self.is_group: | |
children = ', '.join(itertools.chain(self._privs, self._groups)) | |
return '<%s(%s, [%s])>' % (type_name, self.path, children) | |
else: | |
return '<%s(%s)>' % (type_name, self.path) | |
class _Privilege(_PrivilegeBase): | |
is_group = False | |
def _lookup(self, path): | |
if path: | |
raise ValueError('%r is not a group' % self) | |
return self | |
class _PrivilegeGroup(_PrivilegeBase, _PrivilegeContainer): | |
is_group = True | |
def __init__(self, parent, name, title, bubble=True, is_priv=True, imply=()): | |
self.is_priv = is_priv | |
super(_PrivilegeGroup, self).__init__(parent, name, title, | |
bubble, imply) | |
class Privileges(_PrivilegeContainer): | |
parent = None | |
def is_priv(self, name, allow_group=True): | |
try: | |
priv = self._lookup(name.split('.')) | |
except ValueError: | |
return False | |
return priv.is_priv and (allow_group or not priv.is_group) | |
def make_privset(self, active=None): | |
"""Creates a privilege set for the current privileges""" | |
return _PrivilegeSet(self, active or []) | |
def _get_path(self): | |
return [] | |
def __repr__(self): | |
children = ', '.join(itertools.chain(self._privs, self._groups)) | |
return '<Privileges([%s])>' % children | |
class _PrivilegeSet(object): | |
def __init__(self, template, active): | |
if isinstance(active, basestring): | |
active = active.split(',') | |
self.template = template | |
self.active = set() | |
for name in active: | |
try: | |
priv = template._lookup(name.split('.')) | |
except KeyError: | |
# Ignore invalid, possible deleted, privs | |
continue | |
# Only add real privs, no groups | |
if not priv.is_group: | |
self.active.add(name) | |
def clear(self): | |
"""Removes all privileges""" | |
self.active.clear() | |
def set(self, name, value): | |
"""Sets/removes a privilege | |
When specifying a group all children of that group are modified.""" | |
priv = self.template._lookup(name.split('.')) | |
return self._set(priv, value) | |
def _set(self, priv, value): | |
privs = set() | |
if priv.is_group: | |
# Set all privs within from this group | |
privs = list(priv._iter_deep()) | |
else: | |
privs = [priv] | |
names = set(priv.path for priv in privs) | |
if value: | |
self.active |= names | |
else: | |
self.active -= names | |
self._update_implications(privs, value) | |
def _update_implications(self, privs, value): | |
if value: | |
# Update all implied privs | |
for priv in privs: | |
for name in priv.imply: | |
self._set(priv.parent._lookup([name]), True) | |
else: | |
# Check for privs implying on the removed ones | |
for priv in privs: | |
for sibling in priv.siblings: | |
if priv.name in sibling.imply: | |
self._set(sibling, False) | |
def check(self, name): | |
"""Checks if a privilege is active""" | |
priv = self.template._lookup(name.split('.')) | |
if not priv.is_priv: | |
raise ValueError('Not a privilege: %s' % name) | |
return self._check(priv) | |
def _check(self, priv): | |
if not priv.is_group: # not a group => simple check | |
return priv.path in self.active | |
elif not priv.is_priv: # group that is not priv-like: never active | |
return False | |
# For groups we need to check if any non-bubble subpriv is active | |
for subpriv in priv._iter_deep(only_bubbling=True): | |
if subpriv.path in self.active: | |
return True | |
return False | |
@property | |
def privs(self): | |
"""Contains all active privileges, including groups""" | |
active = [] | |
for priv in self.template._iter_deep(True): | |
if self._check(priv): | |
active.append(priv.path) | |
return sorted(active) | |
def __iter__(self): | |
active = [] | |
for priv in self.template._iter_deep(True): | |
if self._check(priv): | |
yield priv | |
def __nonzero__(self): | |
return bool(self.active) | |
def __str__(self): | |
return ','.join(sorted(self.active)) | |
def __repr__(self): | |
active = ', '.join(sorted(self.active)) | |
return '<PrivilegeSet([%s])>' % active |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment