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 logging
 import datetime
 import datetime
 import time
 import time
+from functools import partial
 
 
 from sqlalchemy import event
 from sqlalchemy import event
 from sqlalchemy.engine import Engine
 from sqlalchemy.engine import Engine
@@ -25,7 +26,6 @@ from flaskbb.user.views import user
 from flaskbb.user.models import User, Guest
 from flaskbb.user.models import User, Guest
 # Import the (private) message blueprint
 # Import the (private) message blueprint
 from flaskbb.message.views import message
 from flaskbb.message.views import message
-from flaskbb.message.models import Conversation
 # Import the auth blueprint
 # Import the auth blueprint
 from flaskbb.auth.views import auth
 from flaskbb.auth.views import auth
 # Import the admin blueprint
 # Import the admin blueprint
@@ -35,16 +35,18 @@ from flaskbb.forum.views import forum
 from flaskbb.forum.models import Post, Topic, Category, Forum
 from flaskbb.forum.models import Post, Topic, Category, Forum
 # extensions
 # extensions
 from flaskbb.extensions import db, login_manager, mail, cache, redis_store, \
 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
 # various helpers
 from flaskbb.utils.helpers import format_date, time_since, crop_title, \
 from flaskbb.utils.helpers import format_date, time_since, crop_title, \
     is_online, render_markup, mark_online, forum_is_unread, topic_is_unread, \
     is_online, render_markup, mark_online, forum_is_unread, topic_is_unread, \
     render_template
     render_template
 from flaskbb.utils.translations import FlaskBBDomain
 from flaskbb.utils.translations import FlaskBBDomain
 # permission checks (here they are used for the jinja filters)
 # 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
 # app specific configurations
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 
 
@@ -151,31 +153,44 @@ def configure_extensions(app):
         # otherwise we will just fallback to the default language
         # otherwise we will just fallback to the default language
         return flaskbb_config["DEFAULT_LANGUAGE"]
         return flaskbb_config["DEFAULT_LANGUAGE"]
 
 
+    # Flask-Allows
+    allows.init_app(app)
+    allows.identity_loader(lambda: current_user)
+
 
 
 def configure_template_filters(app):
 def configure_template_filters(app):
     """Configures the template filters."""
     """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):
 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.
     :copyright: (c) 2014 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
+from flask_allows import Allows
 from flask_sqlalchemy import SQLAlchemy
 from flask_sqlalchemy import SQLAlchemy
 from flask_login import LoginManager
 from flask_login import LoginManager
 from flask_mail import Mail
 from flask_mail import Mail
@@ -20,6 +21,11 @@ from flask_plugins import PluginManager
 from flask_babelex import Babel
 from flask_babelex import Babel
 from flask_wtf.csrf import CsrfProtect
 from flask_wtf.csrf import CsrfProtect
 
 
+from flaskbb.exceptions import AuthorizationRequired
+
+
+# Permissions Manager
+allows = Allows(throws=AuthorizationRequired)
 
 
 # Database
 # Database
 db = SQLAlchemy()
 db = SQLAlchemy()

+ 0 - 3
flaskbb/forum/models.py

@@ -14,7 +14,6 @@ from flask import url_for, abort
 from sqlalchemy.orm import aliased
 from sqlalchemy.orm import aliased
 
 
 from flaskbb.extensions import db
 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, \
 from flaskbb.utils.helpers import slugify, get_categories_and_forums, \
     get_forums
     get_forums
 from flaskbb.utils.database import CRUDMixin
 from flaskbb.utils.database import CRUDMixin
@@ -324,7 +323,6 @@ class Topic(db.Model, CRUDMixin):
         return "<{} {}>".format(self.__class__.__name__, self.id)
         return "<{} {}>".format(self.__class__.__name__, self.id)
 
 
     @classmethod
     @classmethod
-    @can_access_topic
     def get_topic(cls, topic_id, user):
     def get_topic(cls, topic_id, user):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
         return topic
         return topic
@@ -805,7 +803,6 @@ class Forum(db.Model, CRUDMixin):
 
 
     # Classmethods
     # Classmethods
     @classmethod
     @classmethod
-    @can_access_forum
     def get_forum(cls, forum_id, user):
     def get_forum(cls, forum_id, user):
         """Returns the forum and forumsread object as a tuple for the 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
 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_login import login_required, current_user
 from flask_babelex import gettext as _
 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.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
 from flaskbb.user.models import User
 
 
 forum = Blueprint("forum", __name__)
 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>")
 @forum.route("/forum/<int:forum_id>-<slug>")
 @forum.route("/forum/<int:forum_id>-<slug>")
+@allows.requires(CanAccessForum())
 def view_forum(forum_id, slug=None):
 def view_forum(forum_id, slug=None):
     page = request.args.get('page', 1, type=int)
     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:
     if forum_instance.external:
         return redirect(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>", methods=["POST", "GET"])
 @forum.route("/topic/<int:topic_id>-<slug>", methods=["POST", "GET"])
 @forum.route("/topic/<int:topic_id>-<slug>", methods=["POST", "GET"])
+@allows.requires(CanAccessTopic())
 def view_topic(topic_id, slug=None):
 def view_topic(topic_id, slug=None):
     page = request.args.get('page', 1, type=int)
     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)
     topic.update_read(current_user, topic.forum, forumsread)
 
 
     form = None
     form = None
-    if can_post_reply(user=current_user, topic=topic):
+    if Permission(CanPostReply):
         form = QuickreplyForm()
         form = QuickreplyForm()
         if form.validate_on_submit():
         if form.validate_on_submit():
             post = form.save(current_user, topic)
             post = form.save(current_user, topic)
@@ -155,7 +175,7 @@ def view_post(post_id):
 def new_topic(forum_id, slug=None):
 def new_topic(forum_id, slug=None):
     forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
     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."),
         flash(_("You do not have the permissions to create a new topic."),
               "danger")
               "danger")
         return redirect(forum.url)
         return redirect(forum.url)
@@ -183,7 +203,7 @@ def new_topic(forum_id, slug=None):
 def delete_topic(topic_id=None, slug=None):
 def delete_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     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."),
         flash(_("You do not have the permissions to delete this topic."),
               "danger")
               "danger")
         return redirect(topic.forum.url)
         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):
 def lock_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     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."),
         flash(_("You do not have the permissions to lock this topic."),
               "danger")
               "danger")
         return redirect(topic.url)
         return redirect(topic.url)
@@ -216,7 +236,7 @@ def lock_topic(topic_id=None, slug=None):
 def unlock_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()
     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."),
         flash(_("You do not have the permissions to unlock this topic."),
               "danger")
               "danger")
         return redirect(topic.url)
         return redirect(topic.url)
@@ -232,7 +252,7 @@ def unlock_topic(topic_id=None, slug=None):
 def highlight_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()
     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."),
         flash(_("You do not have the permissions to highlight this topic."),
               "danger")
               "danger")
         return redirect(topic.url)
         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()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
 
     # Unlock is basically the same as lock
     # 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."),
         flash(_("You do not have the permissions to trivialize this topic."),
               "danger")
               "danger")
         return redirect(topic.url)
         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 = Forum.query.order_by(Forum.position).all()
     available_forums.remove(forum_instance)
     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."),
         flash(_("You do not have the permissions to moderate this forum."),
               "danger")
               "danger")
         return redirect(forum_instance.url)
         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()
             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
             # 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."),
                 flash(_("You do not have the permissions to move this topic."),
                       "danger")
                       "danger")
                 return redirect(mod_forum_url)
                 return redirect(mod_forum_url)
@@ -359,7 +384,7 @@ def manage_forum(forum_id, slug=None):
 def new_post(topic_id, slug=None):
 def new_post(topic_id, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     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."),
         flash(_("You do not have the permissions to post in this topic."),
               "danger")
               "danger")
         return redirect(topic.forum.url)
         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()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     post = Post.query.filter_by(id=post_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."),
         flash(_("You do not have the permissions to post in this topic."),
               "danger")
               "danger")
         return redirect(topic.forum.url)
         return redirect(topic.forum.url)
@@ -412,7 +437,7 @@ def reply_post(topic_id, post_id):
 def edit_post(post_id):
 def edit_post(post_id):
     post = Post.query.filter_by(id=post_id).first_or_404()
     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."),
         flash(_("You do not have the permissions to edit this post."),
               "danger")
               "danger")
         return redirect(post.topic.url)
         return redirect(post.topic.url)
@@ -443,7 +468,7 @@ def delete_post(post_id):
 
 
     # TODO: Bulk delete
     # 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."),
         flash(_("You do not have the permissions to delete this post."),
               "danger")
               "danger")
         return redirect(post.topic.url)
         return redirect(post.topic.url)

+ 25 - 34
flaskbb/management/forms.py

@@ -9,21 +9,27 @@
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
 from flask_wtf import Form
 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 sqlalchemy.orm.session import make_transient, make_transient_to_detached
 from flask_babelex import lazy_gettext as _
 from flask_babelex import lazy_gettext as _
 
 
 from flaskbb.utils.fields import BirthdayField
 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.extensions import db
 from flaskbb.forum.models import Forum, Category
 from flaskbb.forum.models import Forum, Category
 from flaskbb.user.models import User, Group
 from flaskbb.user.models import User, Group
+from flaskbb.utils.requirements import IsAtleastModerator
+from flask_allows import Permission
+
 
 
 USERNAME_RE = r'^[\w.+-]+$'
 USERNAME_RE = r'^[\w.+-]+$'
 is_username = regexp(USERNAME_RE,
 is_username = regexp(USERNAME_RE,
@@ -337,35 +343,20 @@ class ForumForm(Form):
                                     "moderators."))
                                     "moderators."))
 
 
     def validate_moderators(self, field):
     def validate_moderators(self, field):
-        approved_moderators = list()
+        approved_moderators = []
 
 
         if field.data:
         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:
                 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):
     def save(self):
         data = self.data
         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_login import current_user
 from flask_plugins import get_all_plugins, get_plugin, get_plugin_from_all
 from flask_plugins import get_all_plugins, get_plugin, get_plugin_from_all
 from flask_babelex import gettext as _
 from flask_babelex import gettext as _
+from flask_allows import Permission, Not
 
 
 from flaskbb import __version__ as flaskbb_version
 from flaskbb import __version__ as flaskbb_version
 from flaskbb._compat import iteritems
 from flaskbb._compat import iteritems
 from flaskbb.forum.forms import UserSearchForm
 from flaskbb.forum.forms import UserSearchForm
 from flaskbb.utils.settings import flaskbb_config
 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.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.user.models import Guest, User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
 from flaskbb.management.models import Setting, SettingsGroup
 from flaskbb.management.models import Setting, SettingsGroup
@@ -38,7 +40,7 @@ management = Blueprint("management", __name__)
 
 
 
 
 @management.route("/")
 @management.route("/")
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def overview():
 def overview():
     # user and group stats
     # user and group stats
     banned_users = User.query.filter(
     banned_users = User.query.filter(
@@ -72,7 +74,7 @@ def overview():
 
 
 @management.route("/settings", methods=["GET", "POST"])
 @management.route("/settings", methods=["GET", "POST"])
 @management.route("/settings/<path:slug>", methods=["GET", "POST"])
 @management.route("/settings/<path:slug>", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def settings(slug=None):
 def settings(slug=None):
     slug = slug if slug else "general"
     slug = slug if slug else "general"
 
 
@@ -113,7 +115,7 @@ def settings(slug=None):
 
 
 # Users
 # Users
 @management.route("/users", methods=['GET', 'POST'])
 @management.route("/users", methods=['GET', 'POST'])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def users():
 def users():
     page = request.args.get("page", 1, type=int)
     page = request.args.get("page", 1, type=int)
     search_form = UserSearchForm()
     search_form = UserSearchForm()
@@ -133,30 +135,28 @@ def users():
 
 
 
 
 @management.route("/users/<int:user_id>/edit", methods=["GET", "POST"])
 @management.route("/users/<int:user_id>/edit", methods=["GET", "POST"])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def edit_user(user_id):
 def edit_user(user_id):
     user = User.query.filter_by(id=user_id).first_or_404()
     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")
         flash(_("You are not allowed to edit this user."), "danger")
         return redirect(url_for("management.users"))
         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)
         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)
         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)
     group_query = Group.query.filter(filt)
 
 
     form = EditUserForm(user)
     form = EditUserForm(user)
@@ -181,7 +181,7 @@ def edit_user(user_id):
 
 
 @management.route("/users/delete", methods=["POST"])
 @management.route("/users/delete", methods=["POST"])
 @management.route("/users/<int:user_id>/delete", methods=["POST"])
 @management.route("/users/<int:user_id>/delete", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def delete_user(user_id=None):
 def delete_user(user_id=None):
     # ajax request
     # ajax request
     if request.is_xhr:
     if request.is_xhr:
@@ -220,7 +220,7 @@ def delete_user(user_id=None):
 
 
 
 
 @management.route("/users/add", methods=["GET", "POST"])
 @management.route("/users/add", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def add_user():
 def add_user():
     form = AddUserForm()
     form = AddUserForm()
     if form.validate_on_submit():
     if form.validate_on_submit():
@@ -233,7 +233,7 @@ def add_user():
 
 
 
 
 @management.route("/users/banned", methods=["GET", "POST"])
 @management.route("/users/banned", methods=["GET", "POST"])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def banned_users():
 def banned_users():
     page = request.args.get("page", 1, type=int)
     page = request.args.get("page", 1, type=int)
     search_form = UserSearchForm()
     search_form = UserSearchForm()
@@ -256,9 +256,9 @@ def banned_users():
 
 
 @management.route("/users/ban", methods=["POST"])
 @management.route("/users/ban", methods=["POST"])
 @management.route("/users/<int:user_id>/ban", methods=["POST"])
 @management.route("/users/<int:user_id>/ban", methods=["POST"])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def ban_user(user_id=None):
 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")
         flash(_("You do not have the permissions to ban this user."), "danger")
         return redirect(url_for("management.overview"))
         return redirect(url_for("management.overview"))
 
 
@@ -271,10 +271,11 @@ def ban_user(user_id=None):
         for user in users:
         for user in users:
             # don't let a user ban himself and do not allow a moderator to ban
             # don't let a user ban himself and do not allow a moderator to ban
             # a admin user
             # 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
                 continue
 
 
             elif user.ban():
             elif user.ban():
@@ -297,9 +298,8 @@ def ban_user(user_id=None):
     user = User.query.filter_by(id=user_id).first_or_404()
     user = User.query.filter_by(id=user_id).first_or_404()
 
 
     # Do not allow moderators to ban admins
     # 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")
         flash(_("A moderator cannot ban an admin user."), "danger")
         return redirect(url_for("management.overview"))
         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/unban", methods=["POST"])
 @management.route("/users/<int:user_id>/unban", methods=["POST"])
 @management.route("/users/<int:user_id>/unban", methods=["POST"])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def unban_user(user_id=None):
 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."),
         flash(_("You do not have the permissions to unban this user."),
               "danger")
               "danger")
         return redirect(url_for("management.overview"))
         return redirect(url_for("management.overview"))
@@ -356,7 +356,7 @@ def unban_user(user_id=None):
 
 
 # Reports
 # Reports
 @management.route("/reports")
 @management.route("/reports")
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def reports():
 def reports():
     page = request.args.get("page", 1, type=int)
     page = request.args.get("page", 1, type=int)
     reports = Report.query.\
     reports = Report.query.\
@@ -367,7 +367,7 @@ def reports():
 
 
 
 
 @management.route("/reports/unread")
 @management.route("/reports/unread")
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def unread_reports():
 def unread_reports():
     page = request.args.get("page", 1, type=int)
     page = request.args.get("page", 1, type=int)
     reports = Report.query.\
     reports = Report.query.\
@@ -380,7 +380,7 @@ def unread_reports():
 
 
 @management.route("/reports/<int:report_id>/markread", methods=["POST"])
 @management.route("/reports/<int:report_id>/markread", methods=["POST"])
 @management.route("/reports/markread", methods=["POST"])
 @management.route("/reports/markread", methods=["POST"])
-@moderator_required
+@allows.requires(IsAtleastModerator)
 def report_markread(report_id=None):
 def report_markread(report_id=None):
     # AJAX request
     # AJAX request
     if request.is_xhr:
     if request.is_xhr:
@@ -437,7 +437,7 @@ def report_markread(report_id=None):
 
 
 # Groups
 # Groups
 @management.route("/groups")
 @management.route("/groups")
-@admin_required
+@allows.requires(IsAdmin)
 def groups():
 def groups():
     page = request.args.get("page", 1, type=int)
     page = request.args.get("page", 1, type=int)
 
 
@@ -449,7 +449,7 @@ def groups():
 
 
 
 
 @management.route("/groups/<int:group_id>/edit", methods=["GET", "POST"])
 @management.route("/groups/<int:group_id>/edit", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def edit_group(group_id):
 def edit_group(group_id):
     group = Group.query.filter_by(id=group_id).first_or_404()
     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/<int:group_id>/delete", methods=["POST"])
 @management.route("/groups/delete", methods=["POST"])
 @management.route("/groups/delete", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def delete_group(group_id=None):
 def delete_group(group_id=None):
     if request.is_xhr:
     if request.is_xhr:
         ids = request.get_json()["ids"]
         ids = request.get_json()["ids"]
@@ -516,7 +516,7 @@ def delete_group(group_id=None):
 
 
 
 
 @management.route("/groups/add", methods=["GET", "POST"])
 @management.route("/groups/add", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def add_group():
 def add_group():
     form = AddGroupForm()
     form = AddGroupForm()
     if form.validate_on_submit():
     if form.validate_on_submit():
@@ -530,14 +530,14 @@ def add_group():
 
 
 # Forums and Categories
 # Forums and Categories
 @management.route("/forums")
 @management.route("/forums")
-@admin_required
+@allows.requires(IsAdmin)
 def forums():
 def forums():
     categories = Category.query.order_by(Category.position.asc()).all()
     categories = Category.query.order_by(Category.position.asc()).all()
     return render_template("management/forums.html", categories=categories)
     return render_template("management/forums.html", categories=categories)
 
 
 
 
 @management.route("/forums/<int:forum_id>/edit", methods=["GET", "POST"])
 @management.route("/forums/<int:forum_id>/edit", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def edit_forum(forum_id):
 def edit_forum(forum_id):
     forum = Forum.query.filter_by(id=forum_id).first_or_404()
     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"])
 @management.route("/forums/<int:forum_id>/delete", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def delete_forum(forum_id):
 def delete_forum(forum_id):
     forum = Forum.query.filter_by(id=forum_id).first_or_404()
     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/add", methods=["GET", "POST"])
 @management.route("/forums/<int:category_id>/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):
 def add_forum(category_id=None):
     form = AddForumForm()
     form = AddForumForm()
 
 
@@ -593,7 +593,7 @@ def add_forum(category_id=None):
 
 
 
 
 @management.route("/category/add", methods=["GET", "POST"])
 @management.route("/category/add", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def add_category():
 def add_category():
     form = CategoryForm()
     form = CategoryForm()
 
 
@@ -607,7 +607,7 @@ def add_category():
 
 
 
 
 @management.route("/category/<int:category_id>/edit", methods=["GET", "POST"])
 @management.route("/category/<int:category_id>/edit", methods=["GET", "POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def edit_category(category_id):
 def edit_category(category_id):
     category = Category.query.filter_by(id=category_id).first_or_404()
     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"])
 @management.route("/category/<int:category_id>/delete", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def delete_category(category_id):
 def delete_category(category_id):
     category = Category.query.filter_by(id=category_id).first_or_404()
     category = Category.query.filter_by(id=category_id).first_or_404()
 
 
@@ -638,14 +638,14 @@ def delete_category(category_id):
 
 
 # Plugins
 # Plugins
 @management.route("/plugins")
 @management.route("/plugins")
-@admin_required
+@allows.requires(IsAdmin)
 def plugins():
 def plugins():
     plugins = get_all_plugins()
     plugins = get_all_plugins()
     return render_template("management/plugins.html", plugins=plugins)
     return render_template("management/plugins.html", plugins=plugins)
 
 
 
 
 @management.route("/plugins/<path:plugin>/enable", methods=["POST"])
 @management.route("/plugins/<path:plugin>/enable", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def enable_plugin(plugin):
 def enable_plugin(plugin):
     plugin = get_plugin_from_all(plugin)
     plugin = get_plugin_from_all(plugin)
     if not plugin.enabled:
     if not plugin.enabled:
@@ -677,7 +677,7 @@ def enable_plugin(plugin):
 
 
 
 
 @management.route("/plugins/<path:plugin>/disable", methods=["POST"])
 @management.route("/plugins/<path:plugin>/disable", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def disable_plugin(plugin):
 def disable_plugin(plugin):
     try:
     try:
         plugin = get_plugin(plugin)
         plugin = get_plugin(plugin)
@@ -705,7 +705,7 @@ def disable_plugin(plugin):
 
 
 
 
 @management.route("/plugins/<path:plugin>/uninstall", methods=["POST"])
 @management.route("/plugins/<path:plugin>/uninstall", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def uninstall_plugin(plugin):
 def uninstall_plugin(plugin):
     plugin = get_plugin_from_all(plugin)
     plugin = get_plugin_from_all(plugin)
     if plugin.uninstallable:
     if plugin.uninstallable:
@@ -720,7 +720,7 @@ def uninstall_plugin(plugin):
 
 
 
 
 @management.route("/plugins/<path:plugin>/install", methods=["POST"])
 @management.route("/plugins/<path:plugin>/install", methods=["POST"])
-@admin_required
+@allows.requires(IsAdmin)
 def install_plugin(plugin):
 def install_plugin(plugin):
     plugin = get_plugin_from_all(plugin)
     plugin = get_plugin_from_all(plugin)
     if plugin.installable and not plugin.uninstallable:
     if plugin.installable and not plugin.uninstallable:

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

@@ -124,10 +124,12 @@
         </div>
         </div>
     </div>
     </div>
 
 
+    {% if current_user|can_moderate(forum) %}
     <div class="row controls-row">
     <div class="row controls-row">
         <a class="btn btn-default" href="{{ url_for('forum.manage_forum', forum_id=forum.id, slug=forum.slug) }}">
         <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 %}
             <span class="fa fa-tasks"></span> {% trans %}Moderation Mode{% endtrans %}
         </a>
         </a>
     </div>
     </div>
+    {% endif %}
 </div>
 </div>
 {% endblock %}
 {% 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.
         """Set to a unique key specific to the object in the database.
         Required for cache.memoize() to work across requests.
         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
     @classmethod
     def selectable_groups_choices(cls):
     def selectable_groups_choices(cls):
@@ -71,7 +71,8 @@ class Group(db.Model, CRUDMixin):
 
 
     @classmethod
     @classmethod
     def get_guest_group(cls):
     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):
 class User(db.Model, UserMixin, CRUDMixin):
@@ -349,33 +350,19 @@ class User(db.Model, UserMixin, CRUDMixin):
 
 
     @cache.memoize(timeout=max_integer)
     @cache.memoize(timeout=max_integer)
     def get_permissions(self, exclude=None):
     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 = {}
         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
         return perms
 
 
     @cache.memoize(timeout=max_integer)
     @cache.memoize(timeout=max_integer)
@@ -489,19 +476,30 @@ class Guest(AnonymousUserMixin):
     def permissions(self):
     def permissions(self):
         return self.get_permissions()
         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)
     @cache.memoize(timeout=max_integer)
     def get_permissions(self, exclude=None):
     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 = {}
         perms = {}
         # Get the Guest group
         # 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
         return perms
 
 
     @classmethod
     @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.extensions import redis_store
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.markup import markdown
 from flaskbb.utils.markup import markdown
-
+from flask_allows import Permission
 
 
 _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
 _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
                     For example, to unlock a topic, ``reverse`` should be
                     set to ``True``.
                     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.user.models import User
     from flaskbb.forum.models import Post
     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
     modified_topics = 0
     if action != "delete":
     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:
             if getattr(topic, action) and not reverse:
                 continue
                 continue
 
 
@@ -90,7 +92,7 @@ def do_topic_action(topics, user, action, reverse):
             topic.save()
             topic.save()
     elif action == "delete":
     elif action == "delete":
         for topic in topics:
         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 "
                 flash(_("You do not have the permissions to delete this "
                         "topic."), "danger")
                         "topic."), "danger")
                 return False
                 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
 cov-core==1.15.0
 coverage==3.7.1
 coverage==3.7.1
 Flask==0.10.1
 Flask==0.10.1
+flask-allows==0.1.0
 Flask-Cache==0.13.1
 Flask-Cache==0.13.1
 Flask-DebugToolbar==0.10.0
 Flask-DebugToolbar==0.10.0
 Flask-Login==0.2.11
 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):
 def forum(category, default_settings, default_groups):
     """A single forum in a category."""
     """A single forum in a category."""
     forum = Forum(title="Test Forum", category_id=category.id)
     forum = Forum(title="Test Forum", category_id=category.id)
-    forum.groups  = default_groups
+    forum.groups = default_groups
     forum.save()
     forum.save()
     return forum
     return forum
 
 

+ 19 - 5
tests/fixtures/user.py

@@ -1,7 +1,7 @@
 import pytest
 import pytest
-
 from flaskbb.user.models import User, Guest
 from flaskbb.user.models import User, Guest
 
 
+
 @pytest.fixture
 @pytest.fixture
 def guest():
 def guest():
     """Return a guest (not logged in) user."""
     """Return a guest (not logged in) user."""
@@ -12,7 +12,7 @@ def guest():
 def user(default_groups):
 def user(default_groups):
     """Creates a user with normal permissions."""
     """Creates a user with normal permissions."""
     user = User(username="test_normal", email="test_normal@example.org",
     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()
     user.save()
     return user
     return user
 
 
@@ -22,7 +22,7 @@ def moderator_user(user, forum, default_groups):
     """Creates a test user with moderator permissions."""
     """Creates a test user with moderator permissions."""
 
 
     user = User(username="test_mod", email="test_mod@example.org",
     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()
     user.save()
 
 
     forum.moderators.append(user)
     forum.moderators.append(user)
@@ -34,7 +34,7 @@ def moderator_user(user, forum, default_groups):
 def admin_user(default_groups):
 def admin_user(default_groups):
     """Creates a admin user."""
     """Creates a admin user."""
     user = User(username="test_admin", email="test_admin@example.org",
     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()
     user.save()
     return user
     return user
 
 
@@ -43,6 +43,20 @@ def admin_user(default_groups):
 def super_moderator_user(default_groups):
 def super_moderator_user(default_groups):
     """Creates a super moderator user."""
     """Creates a super moderator user."""
     user = User(username="test_super_mod", email="test_super@example.org",
     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()
     user.save()
     return user
     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)