Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Match operations with permission objects with set operators #1

Merged
merged 17 commits into from
Feb 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,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
170 changes: 138 additions & 32 deletions flask_principal.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,18 +211,10 @@ def __exit__(self, *args):
return False


class Permission(object):
"""Represents needs, any of which must be present to access a resource
class BasePermission(object):
"""The Base Permission."""

: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.needs = set(needs)
self.excludes = set()
http_exception = None

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

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

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

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

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

def __invert__(self):
"""See ``NotPermission``.
"""
return other.issubset(self)
return self.invert()

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

def require(self, http_exception=None):
"""Create a principal for this permission.
Expand All @@ -269,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 @@ -286,6 +286,120 @@ 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.needs = set(needs)
self.excludes = set()

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
)

def reverse(self):
"""
Returns reverse of current state (needs->excludes, excludes->needs)
Expand Down Expand Up @@ -338,14 +452,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