Skip to content

Commit

Permalink
Merge pull request #111 from mg-christian-axelsson/pr-21-rebase
Browse files Browse the repository at this point in the history
Rebase of PR-21 (Match operations with permission objects with set operators)
  • Loading branch information
Abdur-rahmaanJ authored Nov 19, 2024
2 parents 54d3da3 + 0e6ffd5 commit 192ec34
Show file tree
Hide file tree
Showing 3 changed files with 605 additions and 48 deletions.
48 changes: 48 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,54 @@ of the resource, in this case the blog post::

abort(403) # HTTP Forbidden

Combining Permissions
---------------------

While the core constructs provided are sufficient for the common simple use
cases, for more complex cases it is possible to combine existing permissions
through the use of bitwise operators to result in a new permission object that
can do the complex validations. For instance, it is to create a permission
where the identity has the both the `"blog_poster"` or `"blog_reviewer"` role
but not the `"under_probation"` role. Example::

blog_admin = Permission(RoleNeed('blog_admin'))
blog_poster = Permission(RoleNeed('blog_poster'))
blog_reviewer = Permission(RoleNeed('blog_reviewer'))
under_probation = Permission(RoleNeed('under_probation'))

prize_permission = ((blog_poster | blog_reviewer) & ~under_probation)

@app.route('/blog/prizes')
@prize_permission.require()
def prize_redeem():
# find out what prizes are available to blog users that are not
# under probation
return render_template('prize_redeem.html')

Any number of these can be chained, but it is also possible to use the
constructor classes directly to combine these things together::

from flask.ext.principal import AndPermission, OrPermission

allperms = AndPermission(blog_poster, blog_reviewer, blog_admin)
anyperms = OrPermission(blog_poster, blog_reviewer, blog_admin)

Custom Permissions
------------------

Sometimes your permissions may be determined by other circumstances specific to
whatever you need to implement. For that you can create custom classes to
address your needs like so::

from flask.ext.principal import BasePermission

class CustomPermission(BasePermission):
def allows(self, identity):
# Implement other conditions that allow this to pass
return False

These custom permissions can be combined together via the bitwise operators as
explained above.

API
===
Expand Down
182 changes: 144 additions & 38 deletions src/flask_principal.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,10 @@ def __exit__(self, *args):
return False


class Permission(object):
"""Represents needs, any of which must be present to access a resource
:param needs: The needs for this permission
"""
def __init__(self, *needs):
"""A set of needs, any of which must be present in an identity to have
access.
"""
class BasePermission(object):
"""The Base Permission."""

self.perms = {n: True for n in needs}
http_exception = None

def _bool(self):
return bool(self.can())
Expand All @@ -236,33 +229,29 @@ def __bool__(self):
"""
return self._bool()

def __and__(self, other):
"""Does the same thing as ``self.union(other)``
"""
return self.union(other)

def __or__(self, other):
"""Does the same thing as ``self.difference(other)``
"""See ``OrPermission``.
"""
return self.difference(other)
return self.or_(other)

def __contains__(self, other):
"""Does the same thing as ``other.issubset(self)``.
def or_(self, other):
return OrPermission(self, other)

def __and__(self, other):
"""See ``AndPermission``.
"""
return other.issubset(self)
return self.and_(other)

def __repr__(self):
return '<{0} needs={1} excludes={2}>'.format(
self.__class__.__name__, self.needs, self.excludes
)
def and_(self, other):
return AndPermission(self, other)

@property
def needs(self):
return set(n for n in self.perms if self.perms[n])
def __invert__(self):
"""See ``NotPermission``.
"""
return self.invert()

@property
def excludes(self):
return set(n for n in self.perms if not self.perms[n])
def invert(self):
return NotPermission(self)

def require(self, http_exception=None):
"""Create a principal for this permission.
Expand All @@ -276,6 +265,10 @@ def require(self, http_exception=None):
:param http_exception: the HTTP exception code (403, 401 etc)
"""

if http_exception is None:
http_exception = self.http_exception

return IdentityContext(self, http_exception)

def test(self, http_exception=None):
Expand All @@ -293,6 +286,127 @@ def test(self, http_exception=None):
with self.require(http_exception):
pass

def allows(self, identity):
"""Whether the identity can access this permission.
:param identity: The identity
"""

raise NotImplementedError

def can(self):
"""Whether the required context for this permission has access
This creates an identity context and tests whether it can access this
permission
"""
return self.require().can()


class _NaryOperatorPermission(BasePermission):

def __init__(self, *permissions):
self.permissions = set(permissions)


# These classes would be unnecessary if we have predicate calculus
# primatives of some kind.

class OrPermission(_NaryOperatorPermission):
"""Result of bitwise ``or`` of BasePermission"""

def allows(self, identity):
"""
Checks for any of the nested permission instances that allow the
identity and return True, else return False.
:param identity: The identity.
"""

for p in self.permissions:
if p.allows(identity):
return True
return False


class AndPermission(_NaryOperatorPermission):
"""Result of bitwise ``and`` of BasePermission"""

def allows(self, identity):
"""
Checks for any of the nested permission instances that disallow
the identity and return False, else return True.
:param identity: The identity.
"""

for p in self.permissions:
if not p.allows(identity):
return False
return True


class NotPermission(BasePermission):
"""
Result of bitwise ``not`` of BasePermission
Really could be implemented by returning a transformed result of the
source class of itself, but for the sake of clear presentation I am
not doing that.
"""

def __init__(self, permission):
self.permission = permission

def invert(self):
return self.permission

def allows(self, identity):
return not self.permission.allows(identity)


class Permission(BasePermission):
"""Represents needs, any of which must be present to access a resource
:param needs: The needs for this permission
"""
def __init__(self, *needs):
"""A set of needs, any of which must be present in an identity to have
access.
"""

self.perms = {n: True for n in needs}

def __or__(self, other):
"""Does the same thing as ``self.union(other)``
"""
if isinstance(other, Permission):
return self.union(other)
return super(Permission, self).__or__(other)

def __sub__(self, other):
"""Does the same thing as ``self.difference(other)``
"""
return self.difference(other)

def __contains__(self, other):
"""Does the same thing as ``other.issubset(self)``.
"""
return other.issubset(self)

def __repr__(self):
return '<{0} needs={1} excludes={2}>'.format(
self.__class__.__name__, self.needs, self.excludes
)

@property
def needs(self):
return set(n for n in self.perms if self.perms[n])

@property
def excludes(self):
return set(n for n in self.perms if not self.perms[n])

def reverse(self):
"""
Returns reverse of current state (needs->excludes, excludes->needs)
Expand Down Expand Up @@ -354,14 +468,6 @@ def allows(self, identity):

return True

def can(self):
"""Whether the required context for this permission has access
This creates an identity context and tests whether it can access this
permission
"""
return self.require().can()


class Denial(Permission):
"""
Expand Down
Loading

0 comments on commit 192ec34

Please sign in to comment.