Last active
August 29, 2015 14:27
-
-
Save justanr/e5a51aa143dab928b382 to your computer and use it in GitHub Desktop.
Proposal: FlaskBB Permissions
This file contains hidden or 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
import operator | |
from inspect import isclass, isfunction | |
def _is_permission_factory(perm): | |
return isclass(perm) or isfunction(perm) | |
class Permission(object): | |
"Base permission class" | |
def allow(self, user, request, *args, **kwargs): | |
raise NotImplementedError("Must impement Permission.allow") | |
class ConditionalPermission(Permission): | |
# see rest_condition from the django community | |
# for direct inspiration | |
@classmethod | |
def Or(cls, *perms): | |
return cls(op=operator.or_, lazy_until=True, *perms) | |
@classmethod | |
def And(cls, *perms): | |
return cls(op=operator.and_, lazy_until=False, *perms) | |
@classmethod | |
def Not(cls, *perms): | |
return cls(negated=True, *perms) | |
def __init__(self, *perms, **kwargs): | |
self.perms = perms | |
self.op = kwargs.get('op', operator.and_) | |
self.negated = kwargs.get('negated') | |
self.lazy_until = kwargs.get('lazy_until') | |
def allow(self, user, request, *args, **kwargs): | |
reduced_result = None | |
for perm in self.perms: | |
if _is_permission_factory(perm): | |
perm = perm() | |
result = perm.allow(user, request, *args, **kwargs) | |
if reduced_result is None: | |
reduced_result = result | |
else: | |
reduced_result = self.op(reduced_result, result) | |
if self.lazy_until is not None and self.lazy_until == reduced_result: | |
break | |
if reduced_result is not None: | |
return not reduced_result if self.negated else reduced_result | |
return False | |
def __or__(self, perm): | |
return self.Or(self, perm) | |
def __and__(self, perm): | |
return self.And(self, perm) | |
def __invert__(self): | |
return self.Not(self) | |
def __call__(self, *args, **kwargs): | |
return self | |
(C, And, Or, Not) = (ConditionalPermission, ConditionalPermission.And, | |
ConditionalPermission.Or, ConditionalPermission.Not) |
This file contains hidden or 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
from functools import wraps | |
from flaskbb.exceptions import AuthorizationRequired | |
def requires(*perms): | |
# initialize permissions | |
perms = [perm() for perm in perms] | |
def deco(f): | |
@wraps(f) | |
def wrapper(*args, **kwargs): | |
# find a way to dependency inject current_user | |
# and request for testing purposes | |
if all(p.allow(current_user, request, *args, **kwargs) for p in perms): | |
return f(*args, **kwargs) | |
else: | |
raise AuthorizationRequired | |
return wrapper | |
return deco |
This file contains hidden or 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
@fourm.route('/topic/<int:topic_id>/lock', methods=['POST']) | |
@fourm.route('/topic/<int:topic_id>-<slug>/lock', methods=['POST']) | |
@requires(Or(IsModerator, IsAtleastSuperMod)) | |
def lock_topic(topic_id=None, topic_slug=None): | |
topic = Topic.query.get(topic_id) | |
topic.lock = True | |
topic.save() | |
return redirect(topic.url) | |
class LockTopic(PermissionedView): | |
methods = ['POST'] | |
permissions = [Or(IsModerator, IsAtleastSuperMod)] | |
def dispatch_request(self, topic_id=None, topic_slug=None): | |
topic = Topic.query.get(topic_id) | |
topic.lock = True | |
topic.save() | |
return redirect(topic.url) | |
class LockTopicMV(PermissionedMethodView): | |
permissions = [Or(IsModerator, IsAtleastSuperMod)] | |
def get(self, topic_id=None, topic_slug=None): | |
topic = Topic.query.get(topic_id) | |
form = TopicLockForm(obj=topic) | |
# maybe we want to provide a form to provide a reason why a topic was locked | |
return render_template('lock_topic.html', form=form, topic=topic) | |
def post(self, topic_id=None, topic_slug=None): | |
form = TopicLockForm() | |
topic = Topic.query.get(topic_id) | |
if form.validate_on_submit(): | |
topic.lock = True | |
topic.lock_reason = form.reason.data | |
topic.save() | |
return redirect(topic.url) | |
forum.add_url_rule('/topic/<int:topic_id>/lock', endpoint='.lock_topic', view_func=LockTopic.as_view()) | |
forum.add_url_rule('/topic/<int:topic_id>-<topic_slug>/lock', endpoint='.lock_topic', view_func=LockTopic.as_view()) |
This file contains hidden or 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
class IsModerator(Permission): | |
def allow(self, user, request, *args, **kwargs): | |
return user in self.determine_forum(request, *args, **kwargs) | |
def determine_forum(self, request, *args, **kwargs): | |
# no validation on existing forums or topics | |
if 'forum_id' in request.view_args: | |
return Forum.query.get(request.view_args['forum_id']) | |
elif 'topic_id' in request.view_args: | |
return Topic.query.get(request.view_args['topic_id']).forum | |
elif 'post_id' in request.view_args: | |
return Post.query.get(request.view_args['post_id']).topic.forum | |
else: | |
raise Exception("Cannot determine forum") | |
class IsSuperMod(Permission): | |
def allow(self, user, request, *args, **kwargs): | |
return user.permissions.get('supermod', False) | |
class IsAdmin(Permission): | |
def allow(self, user, request, *args, **kwargs): | |
return user.permissions.get('admin', False) | |
class IsSameUser(Permission): | |
def allow(self, user, request, *args, **kwargs): | |
return user is self.determine_user(request, *args, **kwargs) | |
def determine_user(request, *args, **kwargs): | |
if 'post_id' in request.view_args: | |
return Post.query.get(self.request.view_args['post_id']).user | |
if 'username' in request.view_args: | |
return User.query.filter_by(username=self.request.view_args['username']).first() | |
IsAtleastSuperMod = Or(IsSuperMod, IsAdmin) | |
CanEditPost = Or(IsSameUser, IsModerator, IsAtleastSuperMod) |
This file contains hidden or 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
from flask.views import View, MethodView | |
# or where ever we stick it | |
from flaskbb.permissions import requires | |
class PermissionedView(View): | |
permissions = () | |
@classmethod | |
def as_view(cls, name, *cls_args, **cls_kwargs): | |
view = super(PermissionedView, cls).as_view(name, *cls_args, **cls_kwargs) | |
return requires(*cls.permissions)(view) | |
class PermissionedMethodView(PermissionedView, MethodView): | |
pass |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Something to consider, we're hitting the database multiple times in cases like:
SQLAlchemy does use an identity map, so it's smart enough to check its local cache for the primary key first (motivation to switch lookups that use
filter_by(id=name_id)
toget(name_id)
, but in some cases this wouldn't be possible).IsModerator
also presents issues since this basically excludes moderators from the management panel. Maybe splitting this into two permissions is a better idea:IsModerator
which just does something likereturn user.permission.get('mod', False)
CanModerate
which doesIsModerator().allow(user, request) and user in self.determine_forum(request)
Another concept I'd like to introduce is a more dynamic permission system, though I'd save this proposal for 2.0, as it'd be a major change.
This would involve creating two more tables in the database:
Permission
table which stores an id and a permission nameRole_to_Permission
table which is just a many-to-manyThis would allow fine grained permission granting. We could extend
Group
andUser
to inherit from a common entity (Role
seems like an obvious name) and do some magic to merge all the permissions an individual user has with all the permissions granted from the user's groups.