Skip to content

Commit

Permalink
Merge 8a00746 into f1a0b2e
Browse files Browse the repository at this point in the history
  • Loading branch information
millerdev committed Jul 23, 2015
2 parents f1a0b2e + 8a00746 commit a8e833a
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 54 deletions.
10 changes: 5 additions & 5 deletions django_prbac/decorators.py
Expand Up @@ -4,7 +4,7 @@
# Local Imports
from django.http import Http404
from django_prbac.exceptions import PermissionDenied
from django_prbac.utils import ensure_request_has_privilege
from django_prbac.utils import has_privilege


def requires_privilege(slug, **assignment):
Expand All @@ -17,7 +17,8 @@ def decorate(fn):
(in a parameterized fashion)
"""
def wrapped(request, *args, **kwargs):
ensure_request_has_privilege(request, slug, **assignment)
if not has_privilege(request, slug, **assignment):
raise PermissionDenied()
return fn(request, *args, **kwargs)

return wrapped
Expand All @@ -32,9 +33,8 @@ def requires_privilege_raise404(slug, **assignment):
"""
def decorate(fn):
def wrapped(request, *args, **kwargs):
try:
return requires_privilege(slug, **assignment)(fn)(request, *args, **kwargs)
except PermissionDenied:
if not has_privilege(request, slug, **assignment):
raise Http404()
return decorated(request, *args, **kwargs)
return wrapped
return decorate
152 changes: 127 additions & 25 deletions django_prbac/models.py
Expand Up @@ -2,9 +2,12 @@
from __future__ import unicode_literals, absolute_import, print_function

# Standard Library Imports
import time
import weakref

# Django imports
from django.db import models
from django.conf import settings
from django.contrib.auth.models import User

# External Library imports
Expand All @@ -22,18 +25,24 @@
]

class ValidatingModel(object):

def save(self, force_insert=False, force_update=False, **kwargs):
if not (force_insert or force_update):
self.full_clean() # Will raise ValidationError if needed
super(ValidatingModel, self).save(force_insert, force_update, **kwargs)


class Role(ValidatingModel, models.Model):
"""
A PRBAC role, aka a Role parameterized by a set of named variables. Roles
also model privileges: They differ only in that privileges only refer
to real-world consequences when all parameters are instantiated.
"""

PRIVILEGES_BY_SLUG = "DJANGO_PRBAC_PRIVELEGES"
ROLES_BY_ID = "DJANGO_PRBAC_ROLES"
_default_instance = lambda s:None


# Databaes fields
# ---------------
Expand Down Expand Up @@ -65,28 +74,101 @@ class Role(ValidatingModel, models.Model):
# Methods
# -------

@classmethod
def get_cache(cls):
try:
cache = cls.cache
except AttributeError:
timeout = getattr(settings, 'DJANGO_PRBAC_CACHE_TIMEOUT', 60)
cache = cls.cache = DictCache(timeout)
return cache

@classmethod
def update_cache(cls):
roles = cls.objects.prefetch_related('memberships_granted').all()
roles = {role.id: role for role in roles}
for role in roles.values():
role._granted_privileges = privileges = []
# Prevent extra queries by manually linking grants and roles
# because Django 1.6 isn't smart enough to do this for us
for membership in role.memberships_granted.all():
membership.to_role = roles[membership.to_role_id]
membership.from_role = roles[membership.from_role_id]
privileges.append(membership.instantiated_to_role({}))
cache = cls.get_cache()
cache.set(cls.ROLES_BY_ID, roles)
cache.set(cls.PRIVILEGES_BY_SLUG,
{role.slug: role.instantiate({}) for role in roles.values()})

@classmethod
def get_privilege(cls, slug, assignment=None):
"""
Optimized lookup of privilege by slug
This optimization is specifically geared toward cases where
`assignments` is empty.
"""
cache = cls.get_cache()
privileges = cache.get(cls.PRIVILEGES_BY_SLUG)
if privileges is None:
cls.update_cache()
privileges = cache.get(cls.PRIVILEGES_BY_SLUG)
privilege = privileges.get(slug)
if privilege is None:
return None
if assignment:
return privilege.role.instantiate(assignment)
return privilege

def get_cached_role(self):
"""
Optimized lookup of role by id
"""
cache = self.get_cache()
roles = cache.get(self.ROLES_BY_ID)
if roles is None or self.id not in roles:
self.update_cache()
roles = cache.get(self.ROLES_BY_ID)
return roles[self.id]

def get_privileges(self, assignment):
if not assignment:
try:
return self._granted_privileges
except AttributeError:
pass
return [membership.instantiated_to_role(assignment)
for membership in self.memberships_granted.all()]

def instantiate(self, assignment):
"""
An instantiation of this role with some parameters fixed via the provided assignments.
"""
filtered_assignment = dict([(key, assignment[key]) for key in self.parameters & set(assignment.keys())])
return RoleInstance(self, filtered_assignment)

if assignment:
filtered_assignment = {key: assignment[key]
for key in self.parameters & set(assignment.keys())}
else:
value = self._default_instance()
if value is not None:
return value
filtered_assignment = assignment
value = RoleInstance(self, filtered_assignment)
if not filtered_assignment:
self._default_instance = weakref.ref(value)
return value

def has_privilege(self, privilege):
"""
Shortcut for checking privileges easily for roles with no params (aka probably users)
"""

return self.instantiate({}).has_privilege(privilege)
role = self.get_cached_role()
return role.instantiate({}).has_privilege(privilege)

@property
def assignment(self):
"""
A Role stored in the database always has an empty assignment.
"""

return {}

def __repr__(self):
Expand Down Expand Up @@ -133,10 +215,12 @@ def instantiated_to_role(self, assignment):
Returns the super-role instantiated with the parameters of the membership
composed with the `parameters` passed in.
"""
filtered_assignment = dict([(key, assignment[key]) for key in self.to_role.parameters & set(assignment.keys())])
composed_assignment = {}
composed_assignment.update(filtered_assignment)
composed_assignment.update(self.assignment)
if assignment:
for key in self.to_role.parameters & set(assignment.keys()):
composed_assignment[key] = assignment[key]
if self.assignment:
composed_assignment.update(self.assignment)
return self.to_role.instantiate(composed_assignment)

def __repr__(self):
Expand Down Expand Up @@ -171,28 +255,28 @@ class RoleInstance(object):
not a model but only a transient Python object.
"""


def __init__(self, role, assignment):
self.role = role
self.assignment = assignment
self.slug = self.role.slug
self.name = self.role.name
self.parameters = self.role.parameters - set(self.assignment.keys())

self.slug = role.slug
self.name = role.name
self.parameters = role.parameters - set(assignment.keys())

def instantiate(self, assignment):
"""
This role further instantiated with the additional assignment.
Note that any parameters that are already fixed are not actually
available for being assigned, so will _not_ change.
"""
filtered_assignment = dict([(key, assignment[key]) for key in self.parameters & set(assignment.keys())])
composed_assignment = {}
composed_assignment.update(filtered_assignment)
composed_assignment.update(self.assignment)
if assignment:
for key in self.parameters & set(assignment.keys()):
composed_assignment[key] = assignment[key]
if self.assignment:
composed_assignment.update(self.assignment)
# this seems like a bug (wrong arguments). is this method ever called?
return RoleInstance(composed_assignment)


def has_privilege(self, privilege):
"""
True if this instantiated role is allowed the privilege passed in,
Expand All @@ -202,18 +286,36 @@ def has_privilege(self, privilege):
if self == privilege:
return True

for membership in self.role.memberships_granted.all():
if membership.instantiated_to_role(self.assignment).has_privilege(privilege):
return True

return False

return any(p.has_privilege(privilege)
for p in self.role.get_privileges(self.assignment))

def __eq__(self, other):
return self.slug == other.slug and self.assignment == other.assignment


def __repr__(self):
return 'RoleInstance(%r, parameters=%r, assignment=%r)' % (self.slug, self.parameters, self.assignment)


class DictCache(object):
"""A simple in-memory dict cache
:param timeout: Number of seconds until an item in the cache expires.
"""

def __init__(self, timeout=60):
self.timeout = timeout
self.data = {}

def get(self, key, default=None):
now = time.time()
value, expires = self.data.get(key, (default, now))
if now > expires:
self.data.pop(key)
return default
return value

def set(self, key, value):
self.data[key] = (value, time.time() + self.timeout)

def clear(self):
self.data.clear()
4 changes: 2 additions & 2 deletions django_prbac/tests/test_decorators.py
Expand Up @@ -8,11 +8,13 @@
# Local imports
from django_prbac.decorators import requires_privilege
from django_prbac.exceptions import PermissionDenied
from django_prbac.models import Role
from django_prbac import arbitrary

class TestDecorators(TestCase):

def setUp(self):
Role.get_cache().clear()
self.zazzle_privilege = arbitrary.role(slug=arbitrary.unique_slug('zazzle'), parameters=set(['domain']))

def test_requires_privilege_no_current_role(self):
Expand Down Expand Up @@ -89,5 +91,3 @@ def view(request, *args, **kwargs):
request = HttpRequest()
request.role = requestor_role.instantiate({})
view(request)


8 changes: 7 additions & 1 deletion django_prbac/tests/test_models.py
Expand Up @@ -15,6 +15,9 @@

class TestRole(TestCase):

def setUp(self):
Role.get_cache().clear()

def test_has_permission_immediate_no_params(self):
subrole = arbitrary.role()
superrole1 = arbitrary.role()
Expand Down Expand Up @@ -101,6 +104,9 @@ def test_instantiated_to_role_smoke_test(self):

class TestUserRole(TestCase):

def setUp(self):
Role.get_cache().clear()

def test_user_role_integration(self):
"""
Basic smoke test of integration of PRBAC with django.contrib.auth
Expand All @@ -111,7 +117,7 @@ def test_user_role_integration(self):
arbitrary.grant(from_role=role, to_role=priv)
user_role = arbitrary.user_role(user=user, role=role)

self.assertEquals(user.prbac_role, user_role)
self.assertEqual(user.prbac_role, user_role)
self.assertTrue(user.prbac_role.has_privilege(role))
self.assertTrue(user.prbac_role.has_privilege(priv))

39 changes: 18 additions & 21 deletions django_prbac/utils.py
Expand Up @@ -6,40 +6,37 @@
from django_prbac.models import Role, UserRole


def ensure_request_has_privilege(request, slug, **assignment):
def has_privilege(request, slug, **assignment):
"""
Ensures that an HttpRequest object has the privilege specified by slug.
If it does not, it throws a PermissionDenied Exception.
Returns true if the request has the privilege specified by slug,
otherwise false
"""
if not hasattr(request, 'role'):
raise PermissionDenied()
return False

roles = Role.objects.filter(slug=slug)
if not roles:
raise PermissionDenied()
privilege = Role.get_privilege(slug, assignment)
if privilege is None:
return False

privilege = roles[0].instantiate(assignment)
if request.role.has_privilege(privilege):
return
return True

if not hasattr(request, 'user') or not hasattr(request.user, 'prbac_role'):
raise PermissionDenied()

return False
try:
request.user.prbac_role
except UserRole.DoesNotExist:
raise PermissionDenied()
return False

if not request.user.prbac_role.has_privilege(privilege):
raise PermissionDenied()
return request.user.prbac_role.has_privilege(privilege)


def has_privilege(request, slug, **assignment):
def ensure_request_has_privilege(request, slug, **assignment):
"""
Returns true if the request has hte privilege, otherwise false
DEPRECATED
You most likely want `has_permission` or one of the
`requires_privilege` decorators.
"""
try:
ensure_request_has_privilege(request, slug, **assignment)
return True
except PermissionDenied:
return False
if not has_privilege(request, slug, **assignment):
raise PermissionDenied()

0 comments on commit a8e833a

Please sign in to comment.