Browse Source

Merge pull request #143 from justanr/permissions

Reimplement Permissions system
Peter Justin 9 years ago
parent
commit
04ce924e56

+ 42 - 27
flaskbb/app.py

@@ -12,6 +12,7 @@ import os
 import logging
 import datetime
 import time
+from functools import partial
 
 from sqlalchemy import event
 from sqlalchemy.engine import Engine
@@ -25,7 +26,6 @@ from flaskbb.user.views import user
 from flaskbb.user.models import User, Guest
 # Import the (private) message blueprint
 from flaskbb.message.views import message
-from flaskbb.message.models import Conversation
 # Import the auth blueprint
 from flaskbb.auth.views import auth
 # Import the admin blueprint
@@ -35,16 +35,18 @@ from flaskbb.forum.views import forum
 from flaskbb.forum.models import Post, Topic, Category, Forum
 # extensions
 from flaskbb.extensions import db, login_manager, mail, cache, redis_store, \
-    debugtoolbar, migrate, themes, plugin_manager, babel, csrf
+    debugtoolbar, migrate, themes, plugin_manager, babel, csrf, allows
 # various helpers
 from flaskbb.utils.helpers import format_date, time_since, crop_title, \
     is_online, render_markup, mark_online, forum_is_unread, topic_is_unread, \
     render_template
 from flaskbb.utils.translations import FlaskBBDomain
 # permission checks (here they are used for the jinja filters)
-from flaskbb.utils.permissions import can_post_reply, can_post_topic, \
-    can_delete_topic, can_delete_post, can_edit_post, can_edit_user, \
-    can_ban_user, can_moderate, is_admin, is_moderator, is_admin_or_moderator
+from flaskbb.utils.requirements import (
+    IsAdmin, IsAtleastModerator, TplCanModerate,
+    CanBanUser, CanEditUser, TplCanDeletePost, TplCanDeleteTopic,
+    TplCanEditPost, TplCanPostTopic, TplCanPostReply
+)
 # app specific configurations
 from flaskbb.utils.settings import flaskbb_config
 
@@ -151,31 +153,44 @@ def configure_extensions(app):
         # otherwise we will just fallback to the default language
         return flaskbb_config["DEFAULT_LANGUAGE"]
 
+    # Flask-Allows
+    allows.init_app(app)
+    allows.identity_loader(lambda: current_user)
+
 
 def configure_template_filters(app):
     """Configures the template filters."""
-
-    app.jinja_env.filters['markup'] = render_markup
-    app.jinja_env.filters['format_date'] = format_date
-    app.jinja_env.filters['time_since'] = time_since
-    app.jinja_env.filters['is_online'] = is_online
-    app.jinja_env.filters['crop_title'] = crop_title
-    app.jinja_env.filters['forum_is_unread'] = forum_is_unread
-    app.jinja_env.filters['topic_is_unread'] = topic_is_unread
-    # Permission filters
-    app.jinja_env.filters['edit_post'] = can_edit_post
-    app.jinja_env.filters['delete_post'] = can_delete_post
-    app.jinja_env.filters['delete_topic'] = can_delete_topic
-    app.jinja_env.filters['post_reply'] = can_post_reply
-    app.jinja_env.filters['post_topic'] = can_post_topic
-    # Moderator permission filters
-    app.jinja_env.filters['is_admin'] = is_admin
-    app.jinja_env.filters['is_moderator'] = is_moderator
-    app.jinja_env.filters['is_admin_or_moderator'] = is_admin_or_moderator
-    app.jinja_env.filters['can_moderate'] = can_moderate
-
-    app.jinja_env.filters['can_edit_user'] = can_edit_user
-    app.jinja_env.filters['can_ban_user'] = can_ban_user
+    filters = {}
+
+    filters['markup'] = render_markup
+    filters['format_date'] = format_date
+    filters['time_since'] = time_since
+    filters['is_online'] = is_online
+    filters['crop_title'] = crop_title
+    filters['forum_is_unread'] = forum_is_unread
+    filters['topic_is_unread'] = topic_is_unread
+
+    permissions = [
+        ('is_admin', IsAdmin),
+        ('is_moderator', IsAtleastModerator),
+        ('is_admin_or_moderator', IsAtleastModerator),
+        ('can_edit_user', CanEditUser),
+        ('can_ban_user', CanBanUser),
+    ]
+
+    filters.update([
+        (name, partial(perm, request=request)) for name, perm in permissions
+    ])
+
+    # these create closures
+    filters['can_moderate'] = TplCanModerate(request)
+    filters['post_reply'] = TplCanPostReply(request)
+    filters['edit_post'] = TplCanEditPost(request)
+    filters['delete_post'] = TplCanDeletePost(request)
+    filters['post_topic'] = TplCanPostTopic(request)
+    filters['delete_topic'] = TplCanDeleteTopic(request)
+
+    app.jinja_env.filters.update(filters)
 
 
 def configure_context_processors(app):

+ 19 - 0
flaskbb/exceptions.py

@@ -0,0 +1,19 @@
+"""
+    flaskbb.exceptions
+    ~~~~~~~~~~~~~~~~~~
+
+    Exceptions implemented by FlaskBB.
+
+    :copyright: (c) 2015 by the FlaskBBB Team.
+    :license: BSD, see LICENSE for more details
+"""
+from werkzeug.exceptions import HTTPException, Forbidden
+
+
+class FlaskBBError(HTTPException):
+    "Root exception for FlaskBB"
+    description = "An internal error has occured"
+
+
+class AuthorizationRequired(FlaskBBError, Forbidden):
+    description = "Authorization is required to access this area."

+ 6 - 0
flaskbb/extensions.py

@@ -8,6 +8,7 @@
     :copyright: (c) 2014 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
 """
+from flask_allows import Allows
 from flask_sqlalchemy import SQLAlchemy
 from flask_login import LoginManager
 from flask_mail import Mail
@@ -20,6 +21,11 @@ from flask_plugins import PluginManager
 from flask_babelex import Babel
 from flask_wtf.csrf import CsrfProtect
 
+from flaskbb.exceptions import AuthorizationRequired
+
+
+# Permissions Manager
+allows = Allows(throws=AuthorizationRequired)
 
 # Database
 db = SQLAlchemy()

+ 0 - 3
flaskbb/forum/models.py

@@ -14,7 +14,6 @@ from flask import url_for, abort
 from sqlalchemy.orm import aliased
 
 from flaskbb.extensions import db
-from flaskbb.utils.decorators import can_access_forum, can_access_topic
 from flaskbb.utils.helpers import slugify, get_categories_and_forums, \
     get_forums
 from flaskbb.utils.database import CRUDMixin
@@ -324,7 +323,6 @@ class Topic(db.Model, CRUDMixin):
         return "<{} {}>".format(self.__class__.__name__, self.id)
 
     @classmethod
-    @can_access_topic
     def get_topic(cls, topic_id, user):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
         return topic
@@ -805,7 +803,6 @@ class Forum(db.Model, CRUDMixin):
 
     # Classmethods
     @classmethod
-    @can_access_forum
     def get_forum(cls, forum_id, user):
         """Returns the forum and forumsread object as a tuple for the user.
 

+ 54 - 29
flaskbb/forum/views.py

@@ -11,22 +11,39 @@
 """
 import datetime
 
-from flask import (Blueprint, redirect, url_for, current_app,
-                   request, flash, jsonify)
+from flask import Blueprint, redirect, url_for, current_app, request, flash
 from flask_login import login_required, current_user
 from flask_babelex import gettext as _
-
-from flaskbb.extensions import db
+from flask_allows import Permission, And
+from flaskbb.extensions import db, allows
 from flaskbb.utils.settings import flaskbb_config
-from flaskbb.utils.helpers import (get_online_users, time_diff, format_quote,
-                                   render_template, do_topic_action)
-from flaskbb.utils.permissions import (can_post_reply, can_post_topic,
-                                       can_delete_topic, can_delete_post,
-                                       can_edit_post, can_moderate)
-from flaskbb.forum.models import (Category, Forum, Topic, Post, ForumsRead,
-                                  TopicsRead)
-from flaskbb.forum.forms import (QuickreplyForm, ReplyForm, NewTopicForm,
-                                 ReportForm, UserSearchForm, SearchPageForm)
+from flaskbb.utils.helpers import (
+    get_online_users, time_diff, format_quote, render_template, do_topic_action
+)
+
+from flaskbb.utils.requirements import (
+    CanAccessForum,
+    CanAccessTopic,
+    CanDeletePost,
+    CanDeleteTopic,
+    CanEditPost,
+    CanPostReply,
+    CanPostTopic,
+    IsAtleastModeratorInForum,
+)
+
+
+from flaskbb.forum.models import (
+    Category, Forum, Topic, Post, ForumsRead, TopicsRead
+)
+from flaskbb.forum.forms import (
+    NewTopicForm,
+    QuickreplyForm,
+    ReplyForm,
+    ReportForm,
+    SearchPageForm,
+    UserSearchForm,
+)
 from flaskbb.user.models import User
 
 forum = Blueprint("forum", __name__)
@@ -75,11 +92,13 @@ def view_category(category_id, slug=None):
 
 @forum.route("/forum/<int:forum_id>")
 @forum.route("/forum/<int:forum_id>-<slug>")
+@allows.requires(CanAccessForum())
 def view_forum(forum_id, slug=None):
     page = request.args.get('page', 1, type=int)
 
-    forum_instance, forumsread = Forum.get_forum(forum_id=forum_id,
-                                                 user=current_user)
+    forum_instance, forumsread = Forum.get_forum(
+        forum_id=forum_id, user=current_user
+    )
 
     if forum_instance.external:
         return redirect(forum_instance.external)
@@ -97,6 +116,7 @@ def view_forum(forum_id, slug=None):
 
 @forum.route("/topic/<int:topic_id>", methods=["POST", "GET"])
 @forum.route("/topic/<int:topic_id>-<slug>", methods=["POST", "GET"])
+@allows.requires(CanAccessTopic())
 def view_topic(topic_id, slug=None):
     page = request.args.get('page', 1, type=int)
 
@@ -125,7 +145,7 @@ def view_topic(topic_id, slug=None):
     topic.update_read(current_user, topic.forum, forumsread)
 
     form = None
-    if can_post_reply(user=current_user, topic=topic):
+    if Permission(CanPostReply):
         form = QuickreplyForm()
         if form.validate_on_submit():
             post = form.save(current_user, topic)
@@ -155,7 +175,7 @@ def view_post(post_id):
 def new_topic(forum_id, slug=None):
     forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
 
-    if not can_post_topic(user=current_user, forum=forum_instance):
+    if not Permission(CanPostTopic):
         flash(_("You do not have the permissions to create a new topic."),
               "danger")
         return redirect(forum.url)
@@ -183,7 +203,7 @@ def new_topic(forum_id, slug=None):
 def delete_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
-    if not can_delete_topic(user=current_user, topic=topic):
+    if not Permission(CanDeleteTopic):
         flash(_("You do not have the permissions to delete this topic."),
               "danger")
         return redirect(topic.forum.url)
@@ -200,7 +220,7 @@ def delete_topic(topic_id=None, slug=None):
 def lock_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
-    if not can_moderate(user=current_user, forum=topic.forum):
+    if not Permission(IsAtleastModeratorInForum(forum=topic.forum)):
         flash(_("You do not have the permissions to lock this topic."),
               "danger")
         return redirect(topic.url)
@@ -216,7 +236,7 @@ def lock_topic(topic_id=None, slug=None):
 def unlock_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
-    if not can_moderate(user=current_user, forum=topic.forum):
+    if not Permission(IsAtleastModeratorInForum(forum=topic.forum)):
         flash(_("You do not have the permissions to unlock this topic."),
               "danger")
         return redirect(topic.url)
@@ -232,7 +252,7 @@ def unlock_topic(topic_id=None, slug=None):
 def highlight_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
-    if not can_moderate(user=current_user, forum=topic.forum):
+    if not Permission(IsAtleastModeratorInForum(forum=topic.forum)):
         flash(_("You do not have the permissions to highlight this topic."),
               "danger")
         return redirect(topic.url)
@@ -249,7 +269,7 @@ def trivialize_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
     # Unlock is basically the same as lock
-    if not can_moderate(user=current_user, forum=topic.forum):
+    if not Permission(IsAtleastModeratorInForum(forum=topic.forum)):
         flash(_("You do not have the permissions to trivialize this topic."),
               "danger")
         return redirect(topic.url)
@@ -272,7 +292,7 @@ def manage_forum(forum_id, slug=None):
     available_forums = Forum.query.order_by(Forum.position).all()
     available_forums.remove(forum_instance)
 
-    if not can_moderate(current_user, forum=forum_instance):
+    if not Permission(IsAtleastModeratorInForum(forum=forum_instance)):
         flash(_("You do not have the permissions to moderate this forum."),
               "danger")
         return redirect(forum_instance.url)
@@ -338,8 +358,13 @@ def manage_forum(forum_id, slug=None):
 
             new_forum = Forum.query.filter_by(id=new_forum_id).first_or_404()
             # check the permission in the current forum and in the new forum
-            if not can_moderate(current_user, forum_instance) or \
-                    not can_moderate(current_user, new_forum):
+
+            if not Permission(
+                And(
+                    IsAtleastModeratorInForum(forum_id=new_forum_id),
+                    IsAtleastModeratorInForum(forum=forum_instance)
+                )
+            ):
                 flash(_("You do not have the permissions to move this topic."),
                       "danger")
                 return redirect(mod_forum_url)
@@ -359,7 +384,7 @@ def manage_forum(forum_id, slug=None):
 def new_post(topic_id, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
-    if not can_post_reply(user=current_user, topic=topic):
+    if not Permission(CanPostReply):
         flash(_("You do not have the permissions to post in this topic."),
               "danger")
         return redirect(topic.forum.url)
@@ -386,7 +411,7 @@ def reply_post(topic_id, post_id):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     post = Post.query.filter_by(id=post_id).first_or_404()
 
-    if not can_post_reply(user=current_user, topic=topic):
+    if not Permission(CanPostReply):
         flash(_("You do not have the permissions to post in this topic."),
               "danger")
         return redirect(topic.forum.url)
@@ -412,7 +437,7 @@ def reply_post(topic_id, post_id):
 def edit_post(post_id):
     post = Post.query.filter_by(id=post_id).first_or_404()
 
-    if not can_edit_post(user=current_user, post=post):
+    if not Permission(CanEditPost):
         flash(_("You do not have the permissions to edit this post."),
               "danger")
         return redirect(post.topic.url)
@@ -443,7 +468,7 @@ def delete_post(post_id):
 
     # TODO: Bulk delete
 
-    if not can_delete_post(user=current_user, post=post):
+    if not Permission(CanDeletePost):
         flash(_("You do not have the permissions to delete this post."),
               "danger")
         return redirect(post.topic.url)

+ 25 - 34
flaskbb/management/forms.py

@@ -9,21 +9,27 @@
     :license: BSD, see LICENSE for more details.
 """
 from flask_wtf import Form
-from wtforms import (StringField, TextAreaField, PasswordField, IntegerField,
-                     BooleanField, SelectField, SubmitField,
-		     HiddenField)
-from wtforms.validators import (DataRequired, Optional, Email, regexp, Length,
-                                URL, ValidationError)
-from wtforms.ext.sqlalchemy.fields import (QuerySelectField,
-                                           QuerySelectMultipleField)
+from wtforms import (
+    BooleanField, HiddenField, IntegerField, PasswordField,
+    SelectField, StringField, SubmitField, TextAreaField,
+)
+from wtforms.validators import (
+    DataRequired, Optional, Email, regexp, Length, URL, ValidationError
+)
+from wtforms.ext.sqlalchemy.fields import (
+    QuerySelectField, QuerySelectMultipleField
+)
 from sqlalchemy.orm.session import make_transient, make_transient_to_detached
 from flask_babelex import lazy_gettext as _
 
 from flaskbb.utils.fields import BirthdayField
-from flaskbb.utils.widgets import SelectBirthdayWidget, MultiSelect
+from flaskbb.utils.widgets import SelectBirthdayWidget
 from flaskbb.extensions import db
 from flaskbb.forum.models import Forum, Category
 from flaskbb.user.models import User, Group
+from flaskbb.utils.requirements import IsAtleastModerator
+from flask_allows import Permission
+
 
 USERNAME_RE = r'^[\w.+-]+$'
 is_username = regexp(USERNAME_RE,
@@ -337,35 +343,20 @@ class ForumForm(Form):
                                     "moderators."))
 
     def validate_moderators(self, field):
-        approved_moderators = list()
+        approved_moderators = []
 
         if field.data:
-            # convert the CSV string in a list
-            moderators = field.data.split(",")
-            # remove leading and ending spaces
-            moderators = [mod.strip() for mod in moderators]
-            for moderator in moderators:
-                # Check if the usernames exist
-                user = User.query.filter_by(username=moderator).first()
-
-                # Check if the user has the permissions to moderate a forum
-                if user:
-                    if not (user.get_permissions()["mod"] or
-                            user.get_permissions()["admin"] or
-                            user.get_permissions()["super_mod"]):
-                        raise ValidationError(
-                            _("%(user)s is not in a moderators group.",
-                              user=user.username)
-                        )
-                    else:
-                        approved_moderators.append(user)
+            moderators = [mod.strip() for mod in field.data.split(',')]
+            users = User.query.filter(User.username.in_(moderators))
+            for user in users:
+                if not Permission(IsAtleastModerator, identity=user):
+                    raise ValidationError(
+                        _("%(user)s is not in a moderators group.",
+                            user=user.username)
+                    )
                 else:
-                    raise ValidationError(_("User %(moderator)s not found.",
-                                            moderator=moderator))
-            field.data = approved_moderators
-
-        else:
-            field.data = approved_moderators
+                    approved_moderators.append(user)
+        field.data = approved_moderators
 
     def save(self):
         data = self.data

+ 51 - 51
flaskbb/management/views.py

@@ -17,15 +17,17 @@ from flask import (Blueprint, current_app, request, redirect, url_for, flash,
 from flask_login import current_user
 from flask_plugins import get_all_plugins, get_plugin, get_plugin_from_all
 from flask_babelex import gettext as _
+from flask_allows import Permission, Not
 
 from flaskbb import __version__ as flaskbb_version
 from flaskbb._compat import iteritems
 from flaskbb.forum.forms import UserSearchForm
 from flaskbb.utils.settings import flaskbb_config
+from flaskbb.utils.requirements import (
+    IsAtleastModerator, IsAdmin, CanBanUser, CanEditUser, IsAtleastSuperModerator
+)
+from flaskbb.extensions import db, allows
 from flaskbb.utils.helpers import render_template, time_diff, get_online_users
-from flaskbb.utils.decorators import admin_required, moderator_required
-from flaskbb.utils.permissions import can_ban_user, can_edit_user
-from flaskbb.extensions import db
 from flaskbb.user.models import Guest, User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
 from flaskbb.management.models import Setting, SettingsGroup
@@ -38,7 +40,7 @@ management = Blueprint("management", __name__)
 
 
 @management.route("/")
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def overview():
     # user and group stats
     banned_users = User.query.filter(
@@ -72,7 +74,7 @@ def overview():
 
 @management.route("/settings", methods=["GET", "POST"])
 @management.route("/settings/<path:slug>", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def settings(slug=None):
     slug = slug if slug else "general"
 
@@ -113,7 +115,7 @@ def settings(slug=None):
 
 # Users
 @management.route("/users", methods=['GET', 'POST'])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def users():
     page = request.args.get("page", 1, type=int)
     search_form = UserSearchForm()
@@ -133,30 +135,28 @@ def users():
 
 
 @management.route("/users/<int:user_id>/edit", methods=["GET", "POST"])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def edit_user(user_id):
     user = User.query.filter_by(id=user_id).first_or_404()
 
-    if not can_edit_user(current_user):
+    if not Permission(CanEditUser, identity=current_user):
         flash(_("You are not allowed to edit this user."), "danger")
         return redirect(url_for("management.users"))
 
-    member_group = db.and_(*[db.not_(getattr(Group, p)) for p in ['admin',
-                                              'mod',
-                                              'super_mod',
-                                              'banned',
-                                              'guest'
-                                              ]])
+    member_group = db.and_(*[db.not_(getattr(Group, p)) for p in
+                             ['admin', 'mod', 'super_mod', 'banned', 'guest']])
 
-    filt = db.or_(Group.id.in_(g.id for g in user.groups),
-                   member_group)
+    filt = db.or_(Group.id.in_(g.id for g in current_user.groups), member_group)
 
-    if any(user.permissions[p] for p in ['super_mod', 'admin']):
+    if Permission(IsAtleastSuperModerator, identity=current_user):
         filt = db.or_(filt, Group.mod)
 
-    if user.permissions['admin']:
+    if Permission(IsAdmin, identity=current_user):
         filt = db.or_(filt, Group.admin, Group.super_mod)
 
+    if Permission(CanBanUser, identity=current_user):
+        filt = db.or_(filt, Group.banned)
+
     group_query = Group.query.filter(filt)
 
     form = EditUserForm(user)
@@ -181,7 +181,7 @@ def edit_user(user_id):
 
 @management.route("/users/delete", methods=["POST"])
 @management.route("/users/<int:user_id>/delete", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def delete_user(user_id=None):
     # ajax request
     if request.is_xhr:
@@ -220,7 +220,7 @@ def delete_user(user_id=None):
 
 
 @management.route("/users/add", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def add_user():
     form = AddUserForm()
     if form.validate_on_submit():
@@ -233,7 +233,7 @@ def add_user():
 
 
 @management.route("/users/banned", methods=["GET", "POST"])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def banned_users():
     page = request.args.get("page", 1, type=int)
     search_form = UserSearchForm()
@@ -256,9 +256,9 @@ def banned_users():
 
 @management.route("/users/ban", methods=["POST"])
 @management.route("/users/<int:user_id>/ban", methods=["POST"])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def ban_user(user_id=None):
-    if not can_ban_user(current_user):
+    if not Permission(CanBanUser, identity=current_user):
         flash(_("You do not have the permissions to ban this user."), "danger")
         return redirect(url_for("management.overview"))
 
@@ -271,10 +271,11 @@ def ban_user(user_id=None):
         for user in users:
             # don't let a user ban himself and do not allow a moderator to ban
             # a admin user
-            if current_user.id == user.id or \
-                    user.get_permissions()['admin'] and \
-                    (current_user.permissions['mod'] or
-                     current_user.permissions['super_mod']):
+            if (
+                current_user.id == user.id or
+                Permission(IsAdmin, identity=user)
+                and Permission(Not(IsAdmin), current_user)
+            ):
                 continue
 
             elif user.ban():
@@ -297,9 +298,8 @@ def ban_user(user_id=None):
     user = User.query.filter_by(id=user_id).first_or_404()
 
     # Do not allow moderators to ban admins
-    if user.get_permissions()['admin'] and \
-            (current_user.permissions['mod'] or
-             current_user.permissions['super_mod']):
+    if Permission(IsAdmin, identity=user) and \
+       Permission(Not(IsAdmin), identity=current_user):
 
         flash(_("A moderator cannot ban an admin user."), "danger")
         return redirect(url_for("management.overview"))
@@ -314,9 +314,9 @@ def ban_user(user_id=None):
 
 @management.route("/users/unban", methods=["POST"])
 @management.route("/users/<int:user_id>/unban", methods=["POST"])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def unban_user(user_id=None):
-    if not can_ban_user(current_user):
+    if not Permission(CanBanUser, identity=current_user):
         flash(_("You do not have the permissions to unban this user."),
               "danger")
         return redirect(url_for("management.overview"))
@@ -356,7 +356,7 @@ def unban_user(user_id=None):
 
 # Reports
 @management.route("/reports")
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def reports():
     page = request.args.get("page", 1, type=int)
     reports = Report.query.\
@@ -367,7 +367,7 @@ def reports():
 
 
 @management.route("/reports/unread")
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def unread_reports():
     page = request.args.get("page", 1, type=int)
     reports = Report.query.\
@@ -380,7 +380,7 @@ def unread_reports():
 
 @management.route("/reports/<int:report_id>/markread", methods=["POST"])
 @management.route("/reports/markread", methods=["POST"])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def report_markread(report_id=None):
     # AJAX request
     if request.is_xhr:
@@ -437,7 +437,7 @@ def report_markread(report_id=None):
 
 # Groups
 @management.route("/groups")
-@admin_required
+@allows.requires(IsAdmin)
 def groups():
     page = request.args.get("page", 1, type=int)
 
@@ -449,7 +449,7 @@ def groups():
 
 
 @management.route("/groups/<int:group_id>/edit", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def edit_group(group_id):
     group = Group.query.filter_by(id=group_id).first_or_404()
 
@@ -471,7 +471,7 @@ def edit_group(group_id):
 
 @management.route("/groups/<int:group_id>/delete", methods=["POST"])
 @management.route("/groups/delete", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def delete_group(group_id=None):
     if request.is_xhr:
         ids = request.get_json()["ids"]
@@ -516,7 +516,7 @@ def delete_group(group_id=None):
 
 
 @management.route("/groups/add", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def add_group():
     form = AddGroupForm()
     if form.validate_on_submit():
@@ -530,14 +530,14 @@ def add_group():
 
 # Forums and Categories
 @management.route("/forums")
-@admin_required
+@allows.requires(IsAdmin)
 def forums():
     categories = Category.query.order_by(Category.position.asc()).all()
     return render_template("management/forums.html", categories=categories)
 
 
 @management.route("/forums/<int:forum_id>/edit", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def edit_forum(forum_id):
     forum = Forum.query.filter_by(id=forum_id).first_or_404()
 
@@ -559,7 +559,7 @@ def edit_forum(forum_id):
 
 
 @management.route("/forums/<int:forum_id>/delete", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def delete_forum(forum_id):
     forum = Forum.query.filter_by(id=forum_id).first_or_404()
 
@@ -574,7 +574,7 @@ def delete_forum(forum_id):
 
 @management.route("/forums/add", methods=["GET", "POST"])
 @management.route("/forums/<int:category_id>/add", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def add_forum(category_id=None):
     form = AddForumForm()
 
@@ -593,7 +593,7 @@ def add_forum(category_id=None):
 
 
 @management.route("/category/add", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def add_category():
     form = CategoryForm()
 
@@ -607,7 +607,7 @@ def add_category():
 
 
 @management.route("/category/<int:category_id>/edit", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def edit_category(category_id):
     category = Category.query.filter_by(id=category_id).first_or_404()
 
@@ -623,7 +623,7 @@ def edit_category(category_id):
 
 
 @management.route("/category/<int:category_id>/delete", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def delete_category(category_id):
     category = Category.query.filter_by(id=category_id).first_or_404()
 
@@ -638,14 +638,14 @@ def delete_category(category_id):
 
 # Plugins
 @management.route("/plugins")
-@admin_required
+@allows.requires(IsAdmin)
 def plugins():
     plugins = get_all_plugins()
     return render_template("management/plugins.html", plugins=plugins)
 
 
 @management.route("/plugins/<path:plugin>/enable", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def enable_plugin(plugin):
     plugin = get_plugin_from_all(plugin)
     if not plugin.enabled:
@@ -677,7 +677,7 @@ def enable_plugin(plugin):
 
 
 @management.route("/plugins/<path:plugin>/disable", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def disable_plugin(plugin):
     try:
         plugin = get_plugin(plugin)
@@ -705,7 +705,7 @@ def disable_plugin(plugin):
 
 
 @management.route("/plugins/<path:plugin>/uninstall", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def uninstall_plugin(plugin):
     plugin = get_plugin_from_all(plugin)
     if plugin.uninstallable:
@@ -720,7 +720,7 @@ def uninstall_plugin(plugin):
 
 
 @management.route("/plugins/<path:plugin>/install", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def install_plugin(plugin):
     plugin = get_plugin_from_all(plugin)
     if plugin.installable and not plugin.uninstallable:

+ 2 - 0
flaskbb/templates/forum/forum.html

@@ -124,10 +124,12 @@
         </div>
     </div>
 
+    {% if current_user|can_moderate(forum) %}
     <div class="row controls-row">
         <a class="btn btn-default" href="{{ url_for('forum.manage_forum', forum_id=forum.id, slug=forum.slug) }}">
             <span class="fa fa-tasks"></span> {% trans %}Moderation Mode{% endtrans %}
         </a>
     </div>
+    {% endif %}
 </div>
 {% endblock %}

+ 33 - 35
flaskbb/user/models.py

@@ -61,7 +61,7 @@ class Group(db.Model, CRUDMixin):
         """Set to a unique key specific to the object in the database.
         Required for cache.memoize() to work across requests.
         """
-        return "<{} {}>".format(self.__class__.__name__, self.id)
+        return "<{} {} {}>".format(self.__class__.__name__, self.id, self.name)
 
     @classmethod
     def selectable_groups_choices(cls):
@@ -71,7 +71,8 @@ class Group(db.Model, CRUDMixin):
 
     @classmethod
     def get_guest_group(cls):
-        return Group.query.filter(cls.guest == True).first()
+        return cls.query.filter(cls.guest==True).first()
+
 
 
 class User(db.Model, UserMixin, CRUDMixin):
@@ -349,33 +350,19 @@ class User(db.Model, UserMixin, CRUDMixin):
 
     @cache.memoize(timeout=max_integer)
     def get_permissions(self, exclude=None):
-        """Returns a dictionary with all the permissions the user has.
-
-        :param exclude: a list with excluded permissions. default is None.
-        """
-
-        exclude = exclude or []
-        exclude.extend(['id', 'name', 'description'])
+        """Returns a dictionary with all permissions the user has"""
+        if exclude:
+            exclude = set(exclude)
+        else:
+            exclude = set()
+        exclude.update(['id', 'name', 'description'])
 
         perms = {}
-        groups = self.secondary_groups.all()
-        groups.append(self.primary_group)
-        for group in groups:
-            for c in group.__table__.columns:
-                # try if the permission already exists in the dictionary
-                # and if the permission is true, set it to True
-                try:
-                    if not perms[c.name] and getattr(group, c.name):
-                        perms[c.name] = True
-
-                # if the permission doesn't exist in the dictionary
-                # add it to the dictionary
-                except KeyError:
-                    # if the permission is in the exclude list,
-                    # skip to the next permission
-                    if c.name in exclude:
-                        continue
-                    perms[c.name] = getattr(group, c.name)
+        # Get the Guest group
+        for group in self.groups:
+            columns = set(group.__table__.columns.keys()) - set(exclude)
+            for c in columns:
+                perms[c] = getattr(group, c) or perms.get(c, False)
         return perms
 
     @cache.memoize(timeout=max_integer)
@@ -489,19 +476,30 @@ class Guest(AnonymousUserMixin):
     def permissions(self):
         return self.get_permissions()
 
+    @property
+    def groups(self):
+        return self.get_groups()
+
+    @cache.memoize(timeout=max_integer)
+    def get_groups(self):
+        return Group.query.filter(Group.guest == True).all()
+
     @cache.memoize(timeout=max_integer)
     def get_permissions(self, exclude=None):
-        """Returns a dictionary with all permissions the user has."""
-        exclude = exclude or []
-        exclude.extend(['id', 'name', 'description'])
+
+        """Returns a dictionary with all permissions the user has"""
+        if exclude:
+            exclude = set(exclude)
+        else:
+            exclude = set()
+        exclude.update(['id', 'name', 'description'])
 
         perms = {}
         # Get the Guest group
-        group = Group.query.filter_by(guest=True).first()
-        for c in group.__table__.columns:
-            if c.name in exclude:
-                continue
-            perms[c.name] = getattr(group, c.name)
+        for group in self.groups:
+            columns = set(group.__table__.columns.keys()) - set(exclude)
+            for c in columns:
+                perms[c] = getattr(group, c) or perms.get(c, False)
         return perms
 
     @classmethod

+ 16 - 0
flaskbb/utils/datastructures.py

@@ -0,0 +1,16 @@
+try:
+    from types import SimpleNamespace
+
+except ImportError:
+
+    class SimpleNamespace(dict):
+
+        def __getattr__(self, name):
+            try:
+                return super(SimpleNamespace, self).__getitem__(name)
+            except KeyError:
+                raise AttributeError('{0} has no attribute {1}'
+                                     .format(self.__class__.__name__, name))
+
+        def __setattr__(self, name, value):
+            super(SimpleNamespace, self).__setitem__(name, value)

+ 0 - 88
flaskbb/utils/decorators.py

@@ -1,88 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    flaskbb.utils.decorators
-    ~~~~~~~~~~~~~~~~~~~~~~~~
-
-    A place for our decorators.
-
-    :copyright: (c) 2014 by the FlaskBB Team.
-    :license: BSD, see LICENSE for more details.
-"""
-from functools import wraps
-
-from flask import abort
-from flask_login import current_user
-
-
-def admin_required(f):
-    @wraps(f)
-    def decorated(*args, **kwargs):
-        if current_user.is_anonymous():
-            abort(403)
-        if not current_user.permissions['admin']:
-            abort(403)
-        return f(*args, **kwargs)
-    return decorated
-
-
-def moderator_required(f):
-    @wraps(f)
-    def decorated(*args, **kwargs):
-        if current_user.is_anonymous():
-            abort(403)
-
-        if not any([current_user.permissions['admin'],
-                    current_user.permissions['super_mod'],
-                    current_user.permissions['mod']]):
-            abort(403)
-
-        return f(*args, **kwargs)
-    return decorated
-
-
-def can_access_forum(func):
-    def decorated(*args, **kwargs):
-        forum_id = kwargs['forum_id'] if 'forum_id' in kwargs else args[1]
-        from flaskbb.forum.models import Forum
-        from flaskbb.user.models import Group
-
-        # get list of user group ids
-        if current_user.is_authenticated():
-            user_groups = [gr.id for gr in current_user.groups]
-        else:
-            user_groups = [Group.get_guest_group().id]
-
-        user_forums = Forum.query.filter(
-            Forum.id == forum_id, Forum.groups.any(Group.id.in_(user_groups))
-        ).all()
-
-        if len(user_forums) < 1:
-            abort(403)
-
-        return func(*args, **kwargs)
-    return decorated
-
-
-def can_access_topic(func):
-    def decorated(*args, **kwargs):
-        topic_id = kwargs['topic_id'] if 'topic_id' in kwargs else args[1]
-        from flaskbb.forum.models import Forum, Topic
-        from flaskbb.user.models import Group
-
-        topic = Topic.query.filter_by(id=topic_id).first()
-        # get list of user group ids
-        if current_user.is_authenticated():
-            user_groups = [gr.id for gr in current_user.groups]
-        else:
-            user_groups = [Group.get_guest_group().id]
-
-        user_forums = Forum.query.filter(
-            Forum.id == topic.forum.id,
-            Forum.groups.any(Group.id.in_(user_groups))
-        ).all()
-
-        if len(user_forums) < 1:
-            abort(403)
-
-        return func(*args, **kwargs)
-    return decorated

+ 10 - 8
flaskbb/utils/helpers.py

@@ -28,7 +28,7 @@ from flaskbb._compat import range_method, text_type
 from flaskbb.extensions import redis_store
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.markup import markdown
-
+from flask_allows import Permission
 
 _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
 
@@ -70,18 +70,20 @@ def do_topic_action(topics, user, action, reverse):
                     For example, to unlock a topic, ``reverse`` should be
                     set to ``True``.
     """
-    from flaskbb.utils.permissions import can_moderate, can_delete_topic
+    from flaskbb.utils.requirements import IsAtleastModeratorInForum, CanDeleteTopic
+
     from flaskbb.user.models import User
     from flaskbb.forum.models import Post
 
+    if not Permission(IsAtleastModeratorInForum(forum=topics[0].forum)):
+        flash(_("You do not have the permissions to execute this "
+                "action."), "danger")
+        return False
+
     modified_topics = 0
     if action != "delete":
-        for topic in topics:
-            if not can_moderate(user, topic.forum):
-                flash(_("You do not have the permissions to execute this "
-                        "action."), "danger")
-                return False
 
+        for topic in topics:
             if getattr(topic, action) and not reverse:
                 continue
 
@@ -90,7 +92,7 @@ def do_topic_action(topics, user, action, reverse):
             topic.save()
     elif action == "delete":
         for topic in topics:
-            if not can_delete_topic(user, topic):
+            if not Permission(CanDeleteTopic):
                 flash(_("You do not have the permissions to delete this "
                         "topic."), "danger")
                 return False

+ 0 - 151
flaskbb/utils/permissions.py

@@ -1,151 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    flaskbb.utils.permissions
-    ~~~~~~~~~~~~~~~~~~~~~~~~~
-
-    A place for all permission checks
-
-    :copyright: (c) 2014 by the FlaskBB Team.
-    :license: BSD, see LICENSE for more details.
-"""
-
-
-def check_perm(user, perm, forum, post_user_id=None):
-    """Checks if the `user` has a specified `perm` in the `forum`
-    If post_user_id is provided, it will also check if the user
-    has created the post
-
-    :param user: The user for whom we should check the permission
-
-    :param perm: The permission. You can find a full list of available
-                 permissions here: <INSERT LINK TO DOCS>
-
-    :param forum: The forum where we should check the permission against
-
-    :param post_user_id: If post_user_id is given, it will also perform an
-                         check if the user is the owner of this topic or post.
-    """
-    if can_moderate(user=user, forum=forum):
-        return True
-
-    if post_user_id and user.is_authenticated():
-        return user.permissions[perm] and user.id == post_user_id
-
-    return not user.permissions['banned'] and user.permissions[perm]
-
-
-def is_moderator(user):
-    """Returns ``True`` if the user is in a moderator or super moderator group.
-
-    :param user: The user who should be checked.
-    """
-    return user.permissions['mod'] or user.permissions['super_mod']
-
-
-def is_admin(user):
-    """Returns ``True`` if the user is a administrator.
-
-    :param user:  The user who should be checked.
-    """
-    return user.permissions['admin']
-
-
-def is_admin_or_moderator(user):
-    """Returns ``True`` if the user is either a admin or in a moderator group
-
-    :param user: The user who should be checked.
-    """
-    return is_admin(user) or is_moderator(user)
-
-
-def can_moderate(user, forum=None, perm=None):
-    """Checks if a user can moderate a forum or a user.
-    He needs to be super moderator or a moderator of the
-    specified forum.
-
-    :param user: The user for whom we should check the permission.
-
-    :param forum: The forum that should be checked. If no forum is specified
-                  it will check if the user has at least moderator permissions
-                  and then it will perform another permission check for ``mod``
-                  permissions (they start with ``mod_``).
-
-    :param perm: Optional - Check if the user also has the permission to do
-                 certain things in the forum. There are a few permissions
-                 where you need to be at least a moderator (or anything higher)
-                 in the forum and therefore you can pass a permission and
-                 it will check if the user has it. Those special permissions
-                 are documented here: <INSERT LINK TO DOCS>
-    """
-    # Check if the user has moderator specific permissions (mod_ prefix)
-    if is_admin_or_moderator(user) and forum is None:
-
-        if perm is not None and perm.startswith("mod_"):
-            return user.permissions[perm]
-
-        # If no permission is definied, return False
-        return False
-
-    # check if the user is a moderation and is moderating the forum
-    if user.permissions['mod'] and user in forum.moderators:
-        return True
-
-    # if the user is a super_mod or admin, he can moderate all forums
-    return user.permissions['super_mod'] or user.permissions['admin']
-
-
-def can_edit_post(user, post):
-    """Check if the post can be edited by the user."""
-    topic = post.topic
-
-    if can_moderate(user, topic.forum):
-        return True
-
-    if topic.locked or topic.forum.locked:
-        return False
-
-    return check_perm(user=user, perm='editpost', forum=post.topic.forum,
-                      post_user_id=post.user_id)
-
-
-def can_delete_post(user, post):
-    """Check if the post can be deleted by the user."""
-    return check_perm(user=user, perm='deletepost', forum=post.topic.forum,
-                      post_user_id=post.user_id)
-
-
-def can_delete_topic(user, topic):
-    """Check if the topic can be deleted by the user."""
-    return check_perm(user=user, perm='deletetopic', forum=topic.forum,
-                      post_user_id=topic.user_id)
-
-
-def can_post_reply(user, topic):
-    """Check if the user is allowed to post in the forum."""
-    if can_moderate(user, topic.forum):
-        return True
-
-    if topic.locked or topic.forum.locked:
-        return False
-
-    return check_perm(user=user, perm='postreply', forum=topic.forum)
-
-
-def can_post_topic(user, forum):
-    """Checks if the user is allowed to create a new topic in the forum."""
-    return check_perm(user=user, perm='posttopic', forum=forum)
-
-
-# Moderator permission checks
-def can_edit_user(user):
-    """Check if the user is allowed to edit another users profile.
-    Requires at least ``mod`` permissions.
-    """
-    return can_moderate(user=user, perm="mod_edituser")
-
-
-def can_ban_user(user):
-    """Check if the user is allowed to ban another user.
-    Requires at least ``mod`` permissions.
-    """
-    return can_moderate(user=user, perm="mod_banuser")

+ 341 - 0
flaskbb/utils/requirements.py

@@ -0,0 +1,341 @@
+"""
+    flaskbb.utils.requirements
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Authorization requirements for FlaskBB.
+
+    :copyright: (c) 2015 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details
+"""
+
+from flask_allows import Requirement, Or, And
+from flaskbb.exceptions import FlaskBBError
+from flaskbb.forum.models import Post, Topic, Forum
+from flaskbb.user.models import Group
+
+
+class Has(Requirement):
+    def __init__(self, permission):
+        self.permission = permission
+
+    def __repr__(self):
+        return "<Has({!s})>".format(self.permission)
+
+    def fulfill(self, user, request):
+        return user.permissions.get(self.permission, False)
+
+
+class IsAuthed(Requirement):
+    def fulfill(self, user, request):
+        return user.is_authenticated()
+
+
+class IsModeratorInForum(IsAuthed):
+    def __init__(self, forum=None, forum_id=None):
+        self.forum_id = forum_id
+        self.forum = forum
+
+    def fulfill(self, user, request):
+        moderators = self._get_forum_moderators(request)
+        return (super(IsModeratorInForum, self).fulfill(user, request) and
+                self._user_is_forum_moderator(user, moderators))
+
+    def _user_is_forum_moderator(self, user, moderators):
+        return user in moderators
+
+    def _get_forum_moderators(self, request):
+        return self._get_forum(request).moderators
+
+    def _get_forum(self, request):
+        if self.forum is not None:
+            return self.forum
+        elif self.forum_id is not None:
+            return self._get_forum_from_id()
+        return self._get_forum_from_request(request)
+
+    def _get_forum_from_id(self):
+        return Forum.query.get(self.forum_id)
+
+    def _get_forum_from_request(self, request):
+        view_args = request.view_args
+        if 'post_id' in view_args:
+            return Post.query.get(view_args['post_id']).topic.forum
+        elif 'topic_id' in view_args:
+            return Topic.query.get(view_args['topic_id']).forum
+        elif 'forum_id' in view_args:
+            return Forum.query.get(view_args['forum_id'])
+        else:
+            raise FlaskBBError
+
+
+class IsSameUser(IsAuthed):
+    def __init__(self, topic_or_post=None):
+        self._topic_or_post = topic_or_post
+
+    def fulfill(self, user, request):
+        return (super(IsSameUser, self).fulfill(user, request) and
+                user.id == self._determine_user(request))
+
+    def _determine_user(self, request):
+        if self._topic_or_post is not None:
+            return self._topic_or_post.user_id
+        return self._get_user_id_from_post(request)
+
+    def _get_user_id_from_post(self, request):
+        view_args = request.view_args
+        if 'post_id' in view_args:
+            return Post.query.get(view_args['post_id']).user_id
+        elif 'topic_id' in view_args:
+            return Topic.query.get(view_args['topic_id']).user_id
+        else:
+            raise FlaskBBError
+
+
+class TopicNotLocked(Requirement):
+    def __init__(self, topic=None, topic_id=None, post_id=None, post=None):
+        self._topic = topic
+        self._topic_id = topic_id
+        self._post = post
+        self._post_id = post_id
+
+    def fulfill(self, user, request):
+        return not any(self._determine_locked(request))
+
+    def _determine_locked(self, request):
+        """
+        Returns a pair of booleans:
+            * Is the topic locked?
+            * Is the forum the topic belongs to locked?
+
+        Except in the case of a topic instance being provided to the constructor,
+        all of these tuples are SQLA KeyedTuples
+        """
+        if self._topic is not None:
+            return self._topic.locked, self._topic.forum.locked
+        elif self._post is not None:
+            return self._post.topic.locked, self._post.topic.forum.locked
+        elif self._topic_id is not None:
+            return (
+                Topic.query.join(Forum, Forum.id == Topic.forum_id)
+                .filter(Topic.id == self._topic_id)
+                .with_entities(Topic.locked, Forum.locked)
+                .first()
+            )
+        else:
+            return self._get_topic_from_request(request)
+
+    def _get_topic_from_request(self, request):
+        view_args = request.view_args
+        if 'post_id' in view_args:
+            return (
+                Topic.query.join(Post, Post.topic_id == Topic.id)
+                .join(Forum, Forum.id == Topic.forum_id)
+                .filter(Post.id == view_args['post_id'])
+                .with_entities(Topic.locked, Forum.locked)
+                .first()
+            )
+        elif 'topic_id' in view_args:
+            return (
+                Topic.query.join(Forum, Forum.id == Topic.forum_id)
+                .filter(Topic.id == view_args['topic_id'])
+                .with_entities(Topic.locked, Forum.locked)
+                .first()
+            )
+        else:
+            raise FlaskBBError("How did you get this to happen?")
+
+
+class ForumNotLocked(Requirement):
+    def __init__(self, forum=None, forum_id=None):
+        self._forum = forum
+        self._forum_id = forum_id
+
+    def fulfill(self, user, request):
+        return not self._is_forum_locked(request)
+
+    def _is_forum_locked(self, request):
+        forum = self._determine_forum(request)
+        return not forum.locked
+
+    def _determine_forum(self, request):
+        if self._forum is not None:
+            return self._forum
+        elif self._forum_id is not None:
+            return Forum.query.get(self._forum_id)
+        else:
+            return self._get_forum_from_request(request)
+
+    def _get_forum_from_request(self, request):
+        view_args = request.view_args
+
+        # These queries look big and nasty, but they really aren't that bad
+        # Basically, find the forum this post or topic belongs to
+        # with_entities returns a KeyedTuple with only the locked status
+
+        if 'post_id' in view_args:
+            return (
+                Forum.query.join(Topic, Topic.forum_id == Forum.id)
+                .join(Post, Post.topic_id == Topic.id)
+                .filter(Post.id == view_args['post_id'])
+                .with_entities(Forum.locked)
+                .first()
+            )
+
+        elif 'topic_id' in view_args:
+            return (
+                Forum.query.join(Topic, Topic.forum_id == Forum.id)
+                .filter(Topic.id == view_args['topic_id'])
+                .with_entities(Forum.locked)
+                .first()
+            )
+
+        elif 'forum_id' in view_args:
+            return Forum.query.get(view_args['forum_id'])
+
+
+class CanAccessForum(Requirement):
+    def fulfill(self, user, request):
+        forum_id = request.view_args['forum_id']
+        group_ids = [g.id for g in user.groups]
+
+        return Forum.query.filter(
+            Forum.id == forum_id,
+            Forum.groups.any(Group.id.in_(group_ids))
+        ).count()
+
+
+class CanAccessTopic(Requirement):
+    def fulfill(self, user, request):
+        topic_id = request.view_args['topic_id']
+        group_ids = [g.id for g in user.groups]
+
+        return Forum.query.join(Topic, Topic.forum_id == Forum.id).filter(
+            Topic.id == topic_id,
+            Forum.groups.any(Group.id.in_(group_ids))
+        ).count()
+
+
+def IsAtleastModeratorInForum(forum_id=None, forum=None):
+    return Or(IsAtleastSuperModerator, IsModeratorInForum(forum_id=forum_id,
+                                                          forum=forum))
+
+IsMod = And(IsAuthed(), Has('mod'))
+IsSuperMod = And(IsAuthed(), Has('super_mod'))
+IsAdmin = And(IsAuthed(), Has('admin'))
+
+IsAtleastModerator = Or(IsAdmin, IsSuperMod, IsMod)
+
+IsAtleastSuperModerator = Or(IsAdmin, IsSuperMod)
+
+CanBanUser = Or(IsAtleastSuperModerator, Has('mod_banuser'))
+
+CanEditUser = Or(IsAtleastSuperModerator, Has('mod_edituser'))
+
+CanEditPost = Or(IsAtleastSuperModerator,
+                 And(IsModeratorInForum(), Has('editpost')),
+                 And(IsSameUser(), Has('editpost'), TopicNotLocked()))
+
+CanDeletePost = CanEditPost
+
+CanPostReply = Or(And(Has('postreply'), TopicNotLocked()),
+                  IsModeratorInForum(),
+                  IsAtleastSuperModerator)
+
+CanPostTopic = Or(And(Has('posttopic'), ForumNotLocked()),
+                  IsAtleastSuperModerator,
+                  IsModeratorInForum())
+
+CanDeleteTopic = Or(IsAtleastSuperModerator,
+                    And(IsModeratorInForum(), Has('deletetopic')),
+                    And(IsSameUser(), Has('deletetopic'), TopicNotLocked()))
+
+
+# Template Allowances -- gross, I know
+
+def TplCanModerate(request):
+    def _(user, forum):
+        kwargs = {}
+
+        if isinstance(forum, int):
+            kwargs['forum_id'] = forum
+        elif isinstance(forum, Forum):
+            kwargs['forum'] = forum
+
+        return IsAtleastModeratorInForum(**kwargs)(user, request)
+    return _
+
+
+def TplCanPostReply(request):
+    def _(user, topic=None):
+        kwargs = {}
+
+        if isinstance(topic, int):
+            kwargs['topic_id'] = topic
+        elif isinstance(topic, Topic):
+            kwargs['topic'] = topic
+
+        return Or(
+            IsAtleastSuperModerator,
+            IsModeratorInForum(),
+            And(Has('postreply'), TopicNotLocked(**kwargs))
+        )(user, request)
+    return _
+
+
+def TplCanEditPost(request):
+    def _(user, topic_or_post=None):
+        kwargs = {}
+
+        if isinstance(topic_or_post, int):
+            kwargs['topic_id'] = topic_or_post
+        elif isinstance(topic_or_post, Topic):
+            kwargs['topic'] = topic_or_post
+        elif isinstance(topic_or_post, Post):
+            kwargs['post'] = topic_or_post
+
+        return Or(
+            IsAtleastSuperModerator,
+            And(IsModeratorInForum(), Has('editpost')),
+            And(
+                IsSameUser(topic_or_post),
+                Has('editpost'),
+                TopicNotLocked(**kwargs)
+            ),
+        )(user, request)
+    return _
+
+TplCanDeletePost = TplCanEditPost
+
+
+def TplCanPostTopic(request):
+    def _(user, forum):
+        kwargs = {}
+
+        if isinstance(forum, int):
+            kwargs['forum_id'] = forum
+        elif isinstance(forum, Forum):
+            kwargs['forum'] = forum
+
+        return Or(
+            IsAtleastSuperModerator,
+            IsModeratorInForum(**kwargs),
+            And(Has('posttopic'), ForumNotLocked(**kwargs))
+        )(user, request)
+    return _
+
+
+def TplCanDeleteTopic(request):
+    def _(user, topic=None):
+        kwargs = {}
+
+        if isinstance(topic, int):
+            kwargs['topic_id'] = topic
+        elif isinstance(topic, Topic):
+            kwargs['topic'] = topic
+
+        return Or(
+            IsAtleastSuperModerator,
+            And(IsModeratorInForum(), Has('deletetopic')),
+            And(IsSameUser(), Has('deletetopic'), TopicNotLocked(**kwargs))
+        )(user, request)
+    return _

+ 12 - 0
flaskbb/utils/views.py

@@ -0,0 +1,12 @@
+from flaskbb.utils.helpers import render_template
+from flask.views import View
+
+
+class RenderableView(View):
+    def __init__(self, template, view):
+        self.template = template
+        self.view = view
+
+    def dispatch_request(self, *args, **kwargs):
+        view_model = self.view(*args, **kwargs)
+        return render_template(self.template, **view_model)

+ 1 - 0
requirements.txt

@@ -4,6 +4,7 @@ blinker==1.3
 cov-core==1.15.0
 coverage==3.7.1
 Flask==0.10.1
+flask-allows==0.1.0
 Flask-Cache==0.13.1
 Flask-DebugToolbar==0.10.0
 Flask-Login==0.2.11

BIN
tests/endtoend/.test_auth_views.py.swp


+ 32 - 0
tests/endtoend/test_auth_views.py

@@ -0,0 +1,32 @@
+from flask_login import login_user
+from flaskbb.management import views
+from flaskbb.exceptions import AuthorizationRequired
+import pytest
+
+
+def test_overview_not_authorized(application, default_settings):
+    with application.test_request_context(), pytest.raises(AuthorizationRequired) as excinfo:
+        views.overview()
+
+    assert "Authorization is required to access this area." == excinfo.value.description
+
+
+def test_overview_with_authorized(admin_user, application, default_settings):
+    with application.test_request_context():
+        login_user(admin_user)
+        resp = views.overview()
+        assert 'Overview' in resp
+
+
+def test_overview_with_supermod(super_moderator_user, application, default_settings):
+    with application.test_request_context():
+        login_user(super_moderator_user)
+        resp = views.overview()
+        assert 'Overview' in resp
+
+
+def test_overview_with_mod(moderator_user, application, default_settings):
+    with application.test_request_context():
+        login_user(moderator_user)
+        resp = views.overview()
+        assert 'Overview' in resp

+ 1 - 1
tests/fixtures/forum.py

@@ -18,7 +18,7 @@ def category(database):
 def forum(category, default_settings, default_groups):
     """A single forum in a category."""
     forum = Forum(title="Test Forum", category_id=category.id)
-    forum.groups  = default_groups
+    forum.groups = default_groups
     forum.save()
     return forum
 

+ 19 - 5
tests/fixtures/user.py

@@ -1,7 +1,7 @@
 import pytest
-
 from flaskbb.user.models import User, Guest
 
+
 @pytest.fixture
 def guest():
     """Return a guest (not logged in) user."""
@@ -12,7 +12,7 @@ def guest():
 def user(default_groups):
     """Creates a user with normal permissions."""
     user = User(username="test_normal", email="test_normal@example.org",
-                password="test", primary_group_id=default_groups[3].id)
+                password="test", primary_group=default_groups[3])
     user.save()
     return user
 
@@ -22,7 +22,7 @@ def moderator_user(user, forum, default_groups):
     """Creates a test user with moderator permissions."""
 
     user = User(username="test_mod", email="test_mod@example.org",
-                password="test", primary_group_id=default_groups[2].id)
+                password="test", primary_group=default_groups[2])
     user.save()
 
     forum.moderators.append(user)
@@ -34,7 +34,7 @@ def moderator_user(user, forum, default_groups):
 def admin_user(default_groups):
     """Creates a admin user."""
     user = User(username="test_admin", email="test_admin@example.org",
-                password="test", primary_group_id=default_groups[0].id)
+                password="test", primary_group=default_groups[0])
     user.save()
     return user
 
@@ -43,6 +43,20 @@ def admin_user(default_groups):
 def super_moderator_user(default_groups):
     """Creates a super moderator user."""
     user = User(username="test_super_mod", email="test_super@example.org",
-                password="test", primary_group_id=default_groups[1].id)
+                password="test", primary_group=default_groups[1])
     user.save()
     return user
+
+
+@pytest.fixture
+def Fred(default_groups):
+    """Fred is an interloper and bad intentioned user, he attempts to
+    access areas he shouldn't, he posts trollish and spammy content,
+    he does everything he can to destroy the board.
+
+    Our job is stop Fred.
+    """
+    fred = User(username='Fred', email='fred@fred.fred',
+                password='fred', primary_group=default_groups[3])
+    fred.save()
+    return fred

+ 83 - 0
tests/unit/test_requirements.py

@@ -0,0 +1,83 @@
+from flaskbb.utils import requirements as r
+from flaskbb.utils.datastructures import SimpleNamespace
+
+
+def test_Fred_IsNotAdmin(Fred):
+    assert not r.IsAdmin(Fred, None)
+
+
+def test_IsAdmin_with_admin(admin_user):
+    assert r.IsAdmin(admin_user, None)
+
+
+def test_IsAtleastModerator_with_mod(moderator_user):
+    assert r.IsAtleastModerator(moderator_user, None)
+
+
+def test_IsAtleastModerator_with_supermod(super_moderator_user):
+    assert r.IsAtleastModerator(super_moderator_user, None)
+
+
+def test_IsAtleastModerator_with_admin(admin_user):
+    assert r.IsAtleastModerator(admin_user, None)
+
+
+def test_IsAtleastSuperModerator_with_not_smod(moderator_user):
+    assert not r.IsAtleastSuperModerator(moderator_user, None)
+
+
+def test_CanBanUser_with_admin(admin_user):
+    assert r.CanBanUser(admin_user, None)
+
+
+def test_CanBanUser_with_smod(super_moderator_user):
+    assert r.CanBanUser(super_moderator_user, None)
+
+
+def test_CanBanUser_with_mod(moderator_user):
+    assert r.CanBanUser(moderator_user, None)
+
+
+def test_Fred_CannotBanUser(Fred):
+    assert not r.CanBanUser(Fred, None)
+
+
+def test_CanEditTopic_with_member(user, topic):
+    request = SimpleNamespace(view_args={'topic_id': topic.id})
+    assert r.CanEditPost(user, request)
+
+
+def test_Fred_cannot_edit_other_members_post(user, Fred, topic):
+    request = SimpleNamespace(view_args={'topic_id': topic.id})
+    assert not r.CanEditPost(Fred, request)
+
+
+def test_Fred_CannotEditLockedTopic(Fred, topic_locked):
+    request = SimpleNamespace(view_args={'topic_id': topic_locked.id})
+    assert not r.CanEditPost(Fred, request)
+
+
+def test_Moderator_in_Forum_CanEditLockedTopic(moderator_user, topic_locked):
+    request = SimpleNamespace(view_args={'topic_id': topic_locked.id})
+    assert r.CanEditPost(moderator_user, request)
+
+
+def test_FredIsAMod_but_still_cant_edit_topic_in_locked_forum(Fred, topic_locked, default_groups):
+    request = SimpleNamespace(view_args={'topic_id': topic_locked.id})
+    Fred.primary_group = default_groups[2]
+    assert not r.CanEditPost(Fred, request)
+
+
+def test_Fred_cannot_reply_to_locked_topic(Fred, topic_locked):
+    request = SimpleNamespace(view_args={'topic_id': topic_locked.id})
+    assert not r.CanPostReply(Fred, request)
+
+
+def test_Fred_cannot_delete_others_post(Fred, topic):
+    request = SimpleNamespace(view_args={'post_id': topic.first_post.id})
+    assert not r.CanDeletePost(Fred, request)
+
+
+def test_Mod_can_delete_others_post(moderator_user, topic):
+    request = SimpleNamespace(view_args={'post_id': topic.first_post.id})
+    assert r.CanDeletePost(moderator_user, request)

+ 0 - 120
tests/unit/utils/test_permissions.py

@@ -1,120 +0,0 @@
-"""
-    This test will use the default permissions found in
-    flaskbb.utils.populate
-"""
-from flaskbb.utils.permissions import *
-
-
-def test_moderator_permissions_in_forum(
-        forum, moderator_user, topic, topic_moderator):
-    """Test the moderator permissions in a forum where the user is a
-    moderator.
-    """
-
-    assert moderator_user in forum.moderators
-
-    assert can_post_reply(moderator_user, topic)
-    assert can_post_topic(moderator_user, forum)
-    assert can_edit_post(moderator_user, topic.first_post)
-
-    assert can_moderate(moderator_user, forum)
-    assert can_delete_post(moderator_user, topic.first_post)
-    assert can_delete_topic(moderator_user, topic)
-
-
-def test_moderator_permissions_without_forum(
-        forum, moderator_user, topic, topic_moderator):
-    """Test the moderator permissions in a forum where the user is not a
-    moderator.
-    """
-    forum.moderators.remove(moderator_user)
-
-    assert not moderator_user in forum.moderators
-    assert not can_moderate(moderator_user, forum)
-
-    assert can_post_reply(moderator_user, topic)
-    assert can_post_topic(moderator_user, forum)
-
-    assert not can_edit_post(moderator_user, topic.first_post)
-    assert not can_delete_post(moderator_user, topic.first_post)
-    assert not can_delete_topic(moderator_user, topic)
-
-    # Test with own topic
-    assert can_delete_post(moderator_user, topic_moderator.first_post)
-    assert can_delete_topic(moderator_user, topic_moderator)
-    assert can_edit_post(moderator_user, topic_moderator.first_post)
-
-    # Test moderator permissions
-    assert can_edit_user(moderator_user)
-    assert can_ban_user(moderator_user)
-
-
-def test_normal_permissions(forum, user, topic):
-    """Test the permissions for a normal user."""
-    assert not can_moderate(user, forum)
-
-    assert can_post_reply(user, topic)
-    assert can_post_topic(user, forum)
-
-    assert can_edit_post(user, topic.first_post)
-    assert not can_delete_post(user, topic.first_post)
-    assert not can_delete_topic(user, topic)
-
-    assert not can_edit_user(user)
-    assert not can_ban_user(user)
-
-
-def test_admin_permissions(forum, admin_user, topic):
-    """Test the permissions for a admin user."""
-    assert can_moderate(admin_user, forum)
-
-    assert can_post_reply(admin_user, topic)
-    assert can_post_topic(admin_user, forum)
-
-    assert can_edit_post(admin_user, topic.first_post)
-    assert can_delete_post(admin_user, topic.first_post)
-    assert can_delete_topic(admin_user, topic)
-
-    assert can_edit_user(admin_user)
-    assert can_ban_user(admin_user)
-
-
-def test_super_moderator_permissions(forum, super_moderator_user, topic):
-    """Test the permissions for a super moderator user."""
-    assert can_moderate(super_moderator_user, forum)
-
-    assert can_post_reply(super_moderator_user, topic)
-    assert can_post_topic(super_moderator_user, forum)
-
-    assert can_edit_post(super_moderator_user, topic.first_post)
-    assert can_delete_post(super_moderator_user, topic.first_post)
-    assert can_delete_topic(super_moderator_user, topic)
-
-    assert can_edit_user(super_moderator_user)
-    assert can_ban_user(super_moderator_user)
-
-
-def test_can_moderate_without_permission(moderator_user):
-    """Test can moderate for a moderator_user without a permission."""
-    assert can_moderate(moderator_user) is False
-
-
-def test_permissions_locked_topic(topic_locked, user):
-    """Test user permission if a topic is locked."""
-    assert topic_locked.locked
-
-    post = topic_locked.first_post
-    assert not can_edit_post(user, post)
-    assert not can_post_reply(user, topic_locked)
-
-
-def test_permissions_locked_forum(topic_in_locked_forum, user):
-    """Test user permission if forum is locked."""
-    topic = topic_in_locked_forum
-    post = topic.first_post
-
-    assert not topic.locked
-    assert topic.forum.locked
-
-    assert not can_edit_post(user, post)
-    assert not can_post_reply(user, topic)