Skip to content

Instantly share code, notes, and snippets.

@ThiefMaster
Created August 30, 2012 23:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ThiefMaster/3544241 to your computer and use it in GitHub Desktop.
Save ThiefMaster/3544241 to your computer and use it in GitHub Desktop.
Privilege system with nested privilege groups and inheritance
# 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