Просмотр исходного кода

Merge pull request #471 from justanr/upgrade-allows-fixed

Bump flask-allows version to 0.6.0
Alec Nikolas Reiter 6 лет назад
Родитель
Сommit
3884a0bac6

+ 66 - 60
flaskbb/app.py

@@ -14,7 +14,6 @@ import os
 import sys
 import time
 from datetime import datetime
-from functools import partial
 
 from flask import Flask, request
 from flask_login import current_user
@@ -35,17 +34,18 @@ from flaskbb.plugins.utils import remove_zombie_plugins_from_db, template_hook
 from flaskbb.user.models import Guest, User
 # various helpers
 from flaskbb.utils.helpers import (app_config_from_env, crop_title,
-                                   format_date, forum_is_unread,
-                                   get_alembic_locations, get_flaskbb_config,
-                                   is_online, mark_online,
+                                   format_date, format_datetime,
+                                   forum_is_unread, get_alembic_locations,
+                                   get_flaskbb_config, is_online, mark_online,
                                    render_template, time_since, time_utcnow,
-                                   topic_is_unread, format_datetime)
+                                   topic_is_unread)
 # permission checks (here they are used for the jinja filters)
 from flaskbb.utils.requirements import (CanBanUser, CanEditUser, IsAdmin,
-                                        IsAtleastModerator, TplCanDeletePost,
-                                        TplCanDeleteTopic, TplCanEditPost,
-                                        TplCanModerate, TplCanPostReply,
-                                        TplCanPostTopic)
+                                        IsAtleastModerator, can_delete_topic,
+                                        can_edit_post, can_moderate,
+                                        can_post_reply, can_post_topic,
+                                        has_permission,
+                                        permission_with_identity)
 # whooshees
 from flaskbb.utils.search import (ForumWhoosheer, PostWhoosheer,
                                   TopicWhoosheer, UserWhoosheer)
@@ -96,7 +96,6 @@ def create_app(config=None, instance_path=None):
     configure_errorhandlers(app)
     configure_migrations(app)
     configure_translations(app)
-
     app.pluggy.hook.flaskbb_additional_setup(app=app, pluggy=app.pluggy)
 
     return app
@@ -105,7 +104,7 @@ def create_app(config=None, instance_path=None):
 def configure_app(app, config):
     """Configures FlaskBB."""
     # Use the default config and override it afterwards
-    app.config.from_object('flaskbb.configs.default.DefaultConfig')
+    app.config.from_object("flaskbb.configs.default.DefaultConfig")
     config = get_flaskbb_config(app, config)
     # Path
     if isinstance(config, string_types):
@@ -136,12 +135,12 @@ def configure_app(app, config):
 
     logger.info("Using config from: {}".format(config_name))
 
-    app.pluggy = FlaskBBPluginManager('flaskbb', implprefix='flaskbb_')
+    app.pluggy = FlaskBBPluginManager("flaskbb", implprefix="flaskbb_")
 
 
 def configure_celery_app(app, celery):
     """Configures the celery app."""
-    app.config.update({'BROKER_URL': app.config["CELERY_BROKER_URL"]})
+    app.config.update({"BROKER_URL": app.config["CELERY_BROKER_URL"]})
     celery.conf.update(app.config)
 
     TaskBase = celery.Task
@@ -161,6 +160,9 @@ def configure_blueprints(app):
 
 def configure_extensions(app):
     """Configures the extensions."""
+    # Flask-Allows
+    allows.init_app(app)
+    allows.identity_loader(lambda: current_user)
 
     # Flask-WTF CSRF
     csrf.init_app(app)
@@ -202,8 +204,9 @@ def configure_extensions(app):
     login_manager.login_view = app.config["LOGIN_VIEW"]
     login_manager.refresh_view = app.config["REAUTH_VIEW"]
     login_manager.login_message_category = app.config["LOGIN_MESSAGE_CATEGORY"]
-    login_manager.needs_refresh_message_category = \
-        app.config["REFRESH_MESSAGE_CATEGORY"]
+    login_manager.needs_refresh_message_category = app.config[
+        "REFRESH_MESSAGE_CATEGORY"
+    ]
     login_manager.anonymous_user = Guest
 
     @login_manager.user_loader
@@ -217,42 +220,39 @@ def configure_extensions(app):
 
     login_manager.init_app(app)
 
-    # Flask-Allows
-    allows.init_app(app)
-    allows.identity_loader(lambda: current_user)
-
 
 def configure_template_filters(app):
     """Configures the template filters."""
     filters = {}
 
-    filters['format_date'] = format_date
-    filters['format_datetime'] = format_datetime
-    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
+    filters["crop_title"] = crop_title
+    filters["format_date"] = format_date
+    filters["format_datetime"] = format_datetime
+    filters["forum_is_unread"] = forum_is_unread
+    filters["is_online"] = is_online
+    filters["time_since"] = time_since
+    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),
+        ("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]
+        (name, permission_with_identity(perm, name=name))
+        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)
+    filters["can_moderate"] = can_moderate
+    filters["post_reply"] = can_post_reply
+    filters["edit_post"] = can_edit_post
+    filters["delete_post"] = can_edit_post
+    filters["post_topic"] = can_post_topic
+    filters["delete_topic"] = can_delete_topic
+    filters["has_permission"] = has_permission
 
     app.jinja_env.filters.update(filters)
 
@@ -324,7 +324,7 @@ def configure_migrations(app):
     plugin_dirs = app.pluggy.hook.flaskbb_load_migrations()
     version_locations = get_alembic_locations(plugin_dirs)
 
-    app.config['ALEMBIC']['version_locations'] = version_locations
+    app.config["ALEMBIC"]["version_locations"] = version_locations
 
 
 def configure_translations(app):
@@ -337,8 +337,11 @@ def configure_translations(app):
     @babel.localeselector
     def get_locale():
         # if a user is logged in, use the locale from the user settings
-        if current_user and \
-                current_user.is_authenticated and current_user.language:
+        if (
+            current_user
+            and current_user.is_authenticated
+            and current_user.language
+        ):
             return current_user.language
         # otherwise we will just fallback to the default language
         return flaskbb_config["DEFAULT_LANGUAGE"]
@@ -346,27 +349,27 @@ def configure_translations(app):
 
 def configure_logging(app):
     """Configures logging."""
-    if app.config.get('USE_DEFAULT_LOGGING'):
+    if app.config.get("USE_DEFAULT_LOGGING"):
         configure_default_logging(app)
 
-    if app.config.get('LOG_CONF_FILE'):
+    if app.config.get("LOG_CONF_FILE"):
         logging.config.fileConfig(
-            app.config['LOG_CONF_FILE'], disable_existing_loggers=False
+            app.config["LOG_CONF_FILE"], disable_existing_loggers=False
         )
 
     if app.config["SQLALCHEMY_ECHO"]:
         # Ref: http://stackoverflow.com/a/8428546
         @event.listens_for(Engine, "before_cursor_execute")
         def before_cursor_execute(
-                conn, cursor, statement, parameters, context, executemany
+            conn, cursor, statement, parameters, context, executemany
         ):
-            conn.info.setdefault('query_start_time', []).append(time.time())
+            conn.info.setdefault("query_start_time", []).append(time.time())
 
         @event.listens_for(Engine, "after_cursor_execute")
         def after_cursor_execute(
-                conn, cursor, statement, parameters, context, executemany
+            conn, cursor, statement, parameters, context, executemany
         ):
-            total = time.time() - conn.info['query_start_time'].pop(-1)
+            total = time.time() - conn.info["query_start_time"].pop(-1)
             app.logger.debug("Total Time: %f", total)
 
 
@@ -380,13 +383,16 @@ def configure_default_logging(app):
 
 def configure_mail_logs(app, formatter):
     from logging.handlers import SMTPHandler
+
     formatter = logging.Formatter(
         "%(asctime)s %(levelname)-7s %(name)-25s %(message)s"
     )
     mail_handler = SMTPHandler(
-        app.config['MAIL_SERVER'], app.config['MAIL_DEFAULT_SENDER'],
-        app.config['ADMINS'], 'application error, no admins specified',
-        (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
+        app.config["MAIL_SERVER"],
+        app.config["MAIL_DEFAULT_SENDER"],
+        app.config["ADMINS"],
+        "application error, no admins specified",
+        (app.config["MAIL_USERNAME"], app.config["MAIL_PASSWORD"]),
     )
 
     mail_handler.setLevel(logging.ERROR)
@@ -403,8 +409,9 @@ def load_plugins(app):
     # we are not interested in duplicated plugins or invalid ones
     # ('None' - appears on py2) and thus using a set
     flaskbb_modules = set(
-        module for name, module in iteritems(sys.modules)
-        if name.startswith('flaskbb')
+        module
+        for name, module in iteritems(sys.modules)
+        if name.startswith("flaskbb")
     )
     for module in flaskbb_modules:
         app.pluggy.register(module, internal=True)
@@ -415,21 +422,20 @@ def load_plugins(app):
 
     except (OperationalError, ProgrammingError) as exc:
         logger.debug(
-            "Database is not setup correctly or has not been "
-            "setup yet.",
-            exc_info=exc
+            "Database is not setup correctly or has not been " "setup yet.",
+            exc_info=exc,
         )
         # load plugins even though the database isn't setup correctly
         # i.e. when creating the initial database and wanting to install
         # the plugins migration as well
-        app.pluggy.load_setuptools_entrypoints('flaskbb_plugins')
+        app.pluggy.load_setuptools_entrypoints("flaskbb_plugins")
         return
 
     for plugin in plugins:
         if not plugin.enabled:
             app.pluggy.set_blocked(plugin.name)
 
-    app.pluggy.load_setuptools_entrypoints('flaskbb_plugins')
+    app.pluggy.load_setuptools_entrypoints("flaskbb_plugins")
     app.pluggy.hook.flaskbb_extensions(app=app)
 
     loaded_names = set([p[0] for p in app.pluggy.list_name_plugin()])
@@ -438,7 +444,7 @@ def load_plugins(app):
         PluginRegistry(name=name)
         for name in loaded_names - registered_names
         # ignore internal FlaskBB modules
-        if not name.startswith('flaskbb.') and name != 'flaskbb'
+        if not name.startswith("flaskbb.") and name != "flaskbb"
     ]
     with app.app_context():
         db.session.add_all(unregistered)

+ 205 - 20
flaskbb/forum/views.py

@@ -29,13 +29,15 @@ from flaskbb.forum.models import (Category, Forum, ForumsRead, Post, Topic,
 from flaskbb.user.models import User
 from flaskbb.utils.helpers import (do_topic_action, format_quote,
                                    get_online_users, real, register_view,
-                                   render_template, time_diff, time_utcnow)
-from flaskbb.utils.requirements import (CanAccessForum, CanAccessTopic,
+                                   render_template, time_diff, time_utcnow,
+                                   FlashAndRedirect)
+from flaskbb.utils.requirements import (CanAccessForum,
                                         CanDeletePost, CanDeleteTopic,
                                         CanEditPost, CanPostReply,
                                         CanPostTopic, Has,
                                         IsAtleastModeratorInForum)
 from flaskbb.utils.settings import flaskbb_config
+from .locals import current_topic, current_forum, current_category
 
 impl = HookimplMarker('flaskbb')
 
@@ -90,7 +92,14 @@ class ViewCategory(MethodView):
 
 
 class ViewForum(MethodView):
-    decorators = [allows.requires(CanAccessForum())]
+    decorators = [allows.requires(
+        CanAccessForum(),
+        on_fail=FlashAndRedirect(
+            message=_("You are not allowed to access that forum"),
+            level="warning",
+            endpoint=lambda *a, **k: current_category.url
+        )
+    )]
 
     def get(self, forum_id, slug=None):
         page = request.args.get('page', 1, type=int)
@@ -118,6 +127,14 @@ class ViewForum(MethodView):
 
 
 class ViewPost(MethodView):
+    decorators = [allows.requires(
+        CanAccessForum(),
+        on_fail=FlashAndRedirect(
+            message=_("You are not allowed to access that topic"),
+            level="warning",
+            endpoint=lambda *a, **k: current_category.url
+        )
+    )]
 
     def get(self, post_id):
         '''Redirects to a post in a topic.'''
@@ -141,7 +158,14 @@ class ViewPost(MethodView):
 
 
 class ViewTopic(MethodView):
-    decorators = [allows.requires(CanAccessTopic())]
+    decorators = [allows.requires(
+        CanAccessForum(),
+        on_fail=FlashAndRedirect(
+            message=_("You are not allowed to access that topic"),
+            level="warning",
+            endpoint=lambda *a, **k: current_category.url
+        )
+    )]
 
     def get(self, topic_id, slug=None):
         page = request.args.get('page', 1, type=int)
@@ -185,7 +209,17 @@ class ViewTopic(MethodView):
             form=self.form()
         )
 
-    @allows.requires(CanPostReply)
+    @allows.requires(
+        CanPostReply,
+        on_fail=FlashAndRedirect(
+            message=_("You are not allowed to post a reply to this topic."),
+            level="warning",
+            endpoint=lambda *a, **k: url_for(
+                "forum.view_topic",
+                topic_id=k['topic_id'],
+            )
+        )
+    )
     def post(self, topic_id, slug=None):
         topic = Topic.get_topic(topic_id=topic_id, user=real(current_user))
         form = self.form()
@@ -212,7 +246,17 @@ class ViewTopic(MethodView):
 
 
 class NewTopic(MethodView):
-    decorators = [login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            CanAccessForum(), CanPostTopic,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to post a topic here"),
+                level="warning",
+                endpoint=lambda *a, **k: current_forum.url
+            )
+        ),
+    ]
 
     def get(self, forum_id, slug=None):
         forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
@@ -220,7 +264,6 @@ class NewTopic(MethodView):
             'forum/new_topic.html', forum=forum_instance, form=self.form()
         )
 
-    @allows.requires(CanPostTopic)
     def post(self, forum_id, slug=None):
         forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
         form = self.form()
@@ -246,7 +289,20 @@ class NewTopic(MethodView):
 
 
 class ManageForum(MethodView):
-    decorators = [allows.requires(IsAtleastModeratorInForum()), login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            IsAtleastModeratorInForum(),
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to manage this forum"),
+                level="danger",
+                endpoint=lambda *a, **k: url_for(
+                    "forum.view_forum",
+                    forum_id=k["forum_id"],
+                )
+            )
+        ),
+    ]
 
     def get(self, forum_id, slug=None):
 
@@ -276,6 +332,7 @@ class ManageForum(MethodView):
             forumsread=forumsread,
         )
 
+    # TODO(anr): Clean this up. @_@
     def post(self, forum_id, slug=None):
         forum_instance, __ = Forum.get_forum(
             forum_id=forum_id, user=real(current_user)
@@ -406,7 +463,20 @@ class ManageForum(MethodView):
 
 
 class NewPost(MethodView):
-    decorators = [allows.requires(CanPostReply), login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            CanAccessForum(), CanPostReply,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to post a reply"),
+                level="warning",
+                endpoint=lambda *a, **k: url_for(
+                    "forum.view_topic",
+                    topic_id=k["topic_id"],
+                )
+            )
+        ),
+    ]
 
     def get(self, topic_id, slug=None, post_id=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
@@ -448,7 +518,17 @@ class NewPost(MethodView):
 
 
 class EditPost(MethodView):
-    decorators = [allows.requires(CanEditPost), login_required]
+    decorators = [
+        allows.requires(
+            CanEditPost,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to edit that post"),
+                level="danger",
+                endpoint=lambda *a, **k: current_topic.url
+            )
+        ),
+        login_required
+    ]
 
     def get(self, post_id):
         post = Post.query.filter_by(id=post_id).first_or_404()
@@ -627,7 +707,18 @@ class Search(MethodView):
 
 
 class DeleteTopic(MethodView):
-    decorators = [allows.requires(CanDeleteTopic), login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            CanDeleteTopic,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to delete this topic"),
+                level="danger",
+                # TODO(anr): consider the referrer -- for now, back to topic
+                endpoint=lambda *a, **k: current_topic.url
+            )
+        ),
+    ]
 
     def post(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
@@ -639,7 +730,18 @@ class DeleteTopic(MethodView):
 
 
 class LockTopic(MethodView):
-    decorators = [allows.requires(IsAtleastModeratorInForum()), login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            IsAtleastModeratorInForum(),
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to lock this topic"),
+                level="danger",
+                # TODO(anr): consider the referrer -- for now, back to topic
+                endpoint=lambda *a, **k: current_topic.url
+            )
+        ),
+    ]
 
     def post(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
@@ -649,7 +751,18 @@ class LockTopic(MethodView):
 
 
 class UnlockTopic(MethodView):
-    decorators = [allows.requires(IsAtleastModeratorInForum()), login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            IsAtleastModeratorInForum(),
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to unlock this topic"),
+                level="danger",
+                # TODO(anr): consider the referrer -- for now, back to topic
+                endpoint=lambda *a, **k: current_topic.url
+            )
+        ),
+    ]
 
     def post(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
@@ -659,7 +772,18 @@ class UnlockTopic(MethodView):
 
 
 class HighlightTopic(MethodView):
-    decorators = [allows.requires(IsAtleastModeratorInForum()), login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            IsAtleastModeratorInForum(),
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to highlight this topic"),
+                level="danger",
+                # TODO(anr): consider the referrer -- for now, back to topic
+                endpoint=lambda *a, **k: current_topic.url
+            )
+        ),
+    ]
 
     def post(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
@@ -669,7 +793,18 @@ class HighlightTopic(MethodView):
 
 
 class TrivializeTopic(MethodView):
-    decorators = [allows.requires(IsAtleastModeratorInForum()), login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            IsAtleastModeratorInForum(),
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to trivialize this topic"),
+                level="danger",
+                # TODO(anr): consider the referrer -- for now, back to topic
+                endpoint=lambda *a, **k: current_topic.url
+            )
+        ),
+    ]
 
     def post(self, topic_id=None, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
@@ -679,7 +814,17 @@ class TrivializeTopic(MethodView):
 
 
 class DeletePost(MethodView):
-    decorators = [allows.requires(CanDeletePost), login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            CanDeletePost,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to delete this post"),
+                level="danger",
+                endpoint=lambda *a, **k: current_topic.url
+            )
+        ),
+    ]
 
     def post(self, post_id):
         post = Post.query.filter_by(id=post_id).first_or_404()
@@ -696,7 +841,17 @@ class DeletePost(MethodView):
 
 
 class RawPost(MethodView):
-    decorators = [login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            CanAccessForum(),
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to access that forum"),
+                level="warning",
+                endpoint=lambda *a, **k: current_category.url
+            )
+        ),
+    ]
 
     def get(self, post_id):
         post = Post.query.filter_by(id=post_id).first_or_404()
@@ -704,7 +859,17 @@ class RawPost(MethodView):
 
 
 class MarkRead(MethodView):
-    decorators = [login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            CanAccessForum(),
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to access that forum"),
+                level="warning",
+                endpoint=lambda *a, **k: current_category.url
+            )
+        ),
+    ]
 
     def post(self, forum_id=None, slug=None):
         # Mark a single forum as read
@@ -772,7 +937,17 @@ class WhoIsOnline(MethodView):
 
 
 class TrackTopic(MethodView):
-    decorators = [login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            CanAccessForum(),
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to access that forum"),
+                level="warning",
+                endpoint=lambda *a, **k: current_category.url
+            )
+        ),
+    ]
 
     def post(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
@@ -782,7 +957,17 @@ class TrackTopic(MethodView):
 
 
 class UntrackTopic(MethodView):
-    decorators = [login_required]
+    decorators = [
+        login_required,
+        allows.requires(
+            CanAccessForum(),
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to access that forum"),
+                level="warning",
+                endpoint=lambda *a, **k: current_category.url
+            )
+        ),
+    ]
 
     def post(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()

+ 305 - 31
flaskbb/management/views.py

@@ -34,7 +34,8 @@ from flaskbb.plugins.utils import validate_plugin
 from flaskbb.user.models import Group, Guest, User
 from flaskbb.utils.forms import populate_settings_dict, populate_settings_form
 from flaskbb.utils.helpers import (get_online_users, register_view,
-                                   render_template, time_diff, time_utcnow)
+                                   render_template, time_diff, time_utcnow,
+                                   FlashAndRedirect)
 from flaskbb.utils.requirements import (CanBanUser, CanEditUser, IsAdmin,
                                         IsAtleastModerator,
                                         IsAtleastSuperModerator)
@@ -46,7 +47,16 @@ logger = logging.getLogger(__name__)
 
 
 class ManagementSettings(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to access the management settings"),  # noqa
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def _determine_active_settings(self, slug, plugin):
         """Determines which settings are active.
@@ -126,7 +136,16 @@ class ManagementSettings(MethodView):
 
 
 class ManageUsers(MethodView):
-    decorators = [allows.requires(IsAtleastModerator)]
+    decorators = [
+        allows.requires(
+            IsAtleastModerator,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to manage users"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
     form = UserSearchForm
 
     def get(self):
@@ -162,7 +181,17 @@ class ManageUsers(MethodView):
 
 
 class EditUser(MethodView):
-    decorators = [allows.requires(IsAtleastModerator & CanEditUser)]
+    decorators = [
+        allows.requires(
+            IsAtleastModerator, CanEditUser,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to manage users"),
+                level="danger",
+                endpoint="management.overview"
+            )
+
+        )
+    ]
     form = EditUserForm
 
     def get(self, user_id):
@@ -244,7 +273,16 @@ class EditUser(MethodView):
 
 
 class DeleteUser(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to manage users"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def post(self, user_id=None):
         # ajax request
@@ -276,6 +314,7 @@ class DeleteUser(MethodView):
             )
 
         user = User.query.filter_by(id=user_id).first_or_404()
+
         if current_user.id == user.id:
             flash(_("You cannot delete yourself.", "danger"))
             return redirect(url_for("management.users"))
@@ -286,7 +325,16 @@ class DeleteUser(MethodView):
 
 
 class AddUser(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to manage users"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
     form = AddUserForm
 
     def get(self):
@@ -307,7 +355,16 @@ class AddUser(MethodView):
 
 
 class BannedUsers(MethodView):
-    decorators = [allows.requires(IsAtleastModerator)]
+    decorators = [
+        allows.requires(
+            IsAtleastModerator,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to manage users"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
     form = UserSearchForm
 
     def get(self):
@@ -350,7 +407,16 @@ class BannedUsers(MethodView):
 
 
 class BanUser(MethodView):
-    decorators = [allows.requires(IsAtleastModerator)]
+    decorators = [
+        allows.requires(
+            IsAtleastModerator,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to manage users"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def post(self, user_id=None):
         if not Permission(CanBanUser, identity=current_user):
@@ -412,7 +478,17 @@ class BanUser(MethodView):
 
 
 class UnbanUser(MethodView):
-    decorators = [allows.requires(IsAtleastModerator)]
+    decorators = [
+        allows.requires(
+            IsAtleastModerator,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to manage users"),
+                level="danger",
+                endpoint="management.overview"
+            )
+
+        )
+    ]
 
     def post(self, user_id=None):
 
@@ -459,7 +535,16 @@ class UnbanUser(MethodView):
 
 
 class Groups(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify groups."),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def get(self):
 
@@ -473,7 +558,16 @@ class Groups(MethodView):
 
 
 class AddGroup(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify groups."),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
     form = AddGroupForm
 
     def get(self):
@@ -496,7 +590,16 @@ class AddGroup(MethodView):
 
 
 class EditGroup(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify groups."),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
     form = EditGroupForm
 
     def get(self, group_id):
@@ -526,7 +629,16 @@ class EditGroup(MethodView):
 
 
 class DeleteGroup(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify groups."),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def post(self, group_id=None):
         if request.is_xhr:
@@ -579,7 +691,16 @@ class DeleteGroup(MethodView):
 
 
 class Forums(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify forums."),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def get(self):
         categories = Category.query.order_by(Category.position.asc()).all()
@@ -587,7 +708,16 @@ class Forums(MethodView):
 
 
 class EditForum(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify forums."),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
     form = EditForumForm
 
     def get(self, forum_id):
@@ -629,7 +759,16 @@ class EditForum(MethodView):
 
 
 class AddForum(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify forums."),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
     form = AddForumForm
 
     def get(self, category_id=None):
@@ -664,7 +803,16 @@ class AddForum(MethodView):
 
 
 class DeleteForum(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify forums"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def post(self, forum_id):
         forum = Forum.query.filter_by(id=forum_id).first_or_404()
@@ -680,7 +828,16 @@ class DeleteForum(MethodView):
 
 
 class AddCategory(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify categories"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
     form = CategoryForm
 
     def get(self):
@@ -704,7 +861,16 @@ class AddCategory(MethodView):
 
 
 class EditCategory(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify categories"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
     form = CategoryForm
 
     def get(self, category_id):
@@ -736,7 +902,16 @@ class EditCategory(MethodView):
 
 
 class DeleteCategory(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify categories"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def post(self, category_id):
         category = Category.query.filter_by(id=category_id).first_or_404()
@@ -752,7 +927,16 @@ class DeleteCategory(MethodView):
 
 
 class Reports(MethodView):
-    decorators = [allows.requires(IsAtleastModerator)]
+    decorators = [
+        allows.requires(
+            IsAtleastModerator,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to view reports."),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def get(self):
         page = request.args.get("page", 1, type=int)
@@ -764,7 +948,16 @@ class Reports(MethodView):
 
 
 class UnreadReports(MethodView):
-    decorators = [allows.requires(IsAtleastModerator)]
+    decorators = [
+        allows.requires(
+            IsAtleastModerator,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to view reports."),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def get(self):
         page = request.args.get("page", 1, type=int)
@@ -777,7 +970,16 @@ class UnreadReports(MethodView):
 
 
 class MarkReportRead(MethodView):
-    decorators = [allows.requires(IsAtleastModerator)]
+    decorators = [
+        allows.requires(
+            IsAtleastModerator,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to view reports."),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def post(self, report_id=None):
 
@@ -839,7 +1041,16 @@ class MarkReportRead(MethodView):
 
 
 class DeleteReport(MethodView):
-    decorators = [allows.requires(IsAtleastModerator)]
+    decorators = [
+        allows.requires(
+            IsAtleastModerator,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to view reports."),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def post(self, report_id=None):
 
@@ -873,7 +1084,16 @@ class DeleteReport(MethodView):
 
 
 class CeleryStatus(MethodView):
-    decorators = [allows.requires(IsAtleastModerator)]
+    decorators = [
+        allows.requires(
+            IsAtleastModerator,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to access the management settings"),  # noqa
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def get(self):
         celery_inspect = celery.control.inspect()
@@ -889,7 +1109,16 @@ class CeleryStatus(MethodView):
 
 
 class ManagementOverview(MethodView):
-    decorators = [allows.requires(IsAtleastModerator)]
+    decorators = [
+        allows.requires(
+            IsAtleastModerator,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to access the management panel"),
+                level="danger",
+                endpoint="forum.index"
+            )
+        )
+    ]
 
     def get(self):
         # user and group stats
@@ -935,7 +1164,16 @@ class ManagementOverview(MethodView):
 
 
 class PluginsView(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify plugins"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def get(self):
         plugins = PluginRegistry.query.all()
@@ -943,7 +1181,16 @@ class PluginsView(MethodView):
 
 
 class EnablePlugin(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify plugins"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def post(self, name):
         validate_plugin(name)
@@ -969,7 +1216,16 @@ class EnablePlugin(MethodView):
 
 
 class DisablePlugin(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify plugins"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def post(self, name):
         validate_plugin(name)
@@ -994,7 +1250,16 @@ class DisablePlugin(MethodView):
 
 
 class UninstallPlugin(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify plugins"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def post(self, name):
         validate_plugin(name)
@@ -1006,7 +1271,16 @@ class UninstallPlugin(MethodView):
 
 
 class InstallPlugin(MethodView):
-    decorators = [allows.requires(IsAdmin)]
+    decorators = [
+        allows.requires(
+            IsAdmin,
+            on_fail=FlashAndRedirect(
+                message=_("You are not allowed to modify plugins"),
+                level="danger",
+                endpoint="management.overview"
+            )
+        )
+    ]
 
     def post(self, name):
         plugin_module = validate_plugin(name)

+ 25 - 2
flaskbb/utils/helpers.py

@@ -110,8 +110,10 @@ def do_topic_action(topics, user, action, reverse):
                                             CanDeleteTopic, Has)
 
     if not Permission(IsAtleastModeratorInForum(forum=topics[0].forum)):
-        flash(_("You do not have the permissions to execute this "
-                "action."), "danger")
+        flash(
+            _("You do not have the permissions to execute this action."),
+            "danger"
+        )
         return False
 
     modified_topics = 0
@@ -814,3 +816,24 @@ def requires_unactivated(f):
 def register_view(bp_or_app, routes, view_func, *args, **kwargs):
     for route in routes:
         bp_or_app.add_url_rule(route, view_func=view_func, *args, **kwargs)
+
+
+class FlashAndRedirect(object):
+    def __init__(self, message, level, endpoint):
+        # need to reassign to avoid capturing the reassigned endpoint
+        # in the generated closure, otherwise bad things happen at resolution
+        if not callable(endpoint):
+            # discard args and kwargs and just go to the endpoint
+            # this probably isn't *100%* correct behavior in case we need
+            # to add query params on...
+            endpoint_ = lambda *a, **k: url_for(endpoint)  # noqa
+        else:
+            endpoint_ = endpoint
+
+        self._message = message
+        self._level = level
+        self._endpoint = endpoint_
+
+    def __call__(self, *a, **k):
+        flash(self._message, self._level)
+        return redirect(self._endpoint(*a, **k))

+ 164 - 130
flaskbb/utils/requirements.py

@@ -9,7 +9,7 @@
 """
 import logging
 
-from flask_allows import And, Or, Requirement
+from flask_allows import And, Or, Permission, Requirement
 
 from flaskbb.exceptions import FlaskBBError
 from flaskbb.forum.locals import current_forum, current_post, current_topic
@@ -19,86 +19,95 @@ logger = logging.getLogger(__name__)
 
 
 class Has(Requirement):
+
     def __init__(self, permission):
         self.permission = permission
 
     def __repr__(self):
         return "<Has({!s})>".format(self.permission)
 
-    def fulfill(self, user, request):
+    def fulfill(self, user):
         return user.permissions.get(self.permission, False)
 
 
 class IsAuthed(Requirement):
-    def fulfill(self, user, request):
+
+    def fulfill(self, user):
         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 fulfill(self, user):
+        moderators = self._get_forum_moderators()
+        return (
+            super(IsModeratorInForum, self).fulfill(user)
+            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_moderators(self):
+        return self._get_forum().moderators
 
-    def _get_forum(self, request):
+    def _get_forum(self):
         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)
+        return self._get_forum_from_request()
 
     def _get_forum_from_id(self):
         return Forum.query.get(self.forum_id)
 
-    def _get_forum_from_request(self, request):
+    def _get_forum_from_request(self):
         if not current_forum:
-            raise FlaskBBError('Could not load forum data')
+            raise FlaskBBError("Could not load forum data")
         return current_forum
 
 
 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 fulfill(self, user):
+        return (
+            super(IsSameUser, self).fulfill(user)
+            and user.id == self._determine_user()
+        )
 
-    def _determine_user(self, request):
+    def _determine_user(self):
         if self._topic_or_post is not None:
             return self._topic_or_post.user_id
-        return self._get_user_id_from_post(request)
+        return self._get_user_id_from_post()
 
-    def _get_user_id_from_post(self, request):
+    def _get_user_id_from_post(self):
         if current_post:
             return current_post.user_id
         elif current_topic:
             return current_topic.user_id
         else:
-            raise FlaskBBError
+            raise FlaskBBError("Could not determine user")
 
 
 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 fulfill(self, user):
+        return not any(self._determine_locked())
 
-    def _determine_locked(self, request):
+    def _determine_locked(self):
         """
         Returns a pair of booleans:
             * Is the topic locked?
@@ -113,15 +122,16 @@ class TopicNotLocked(Requirement):
             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()
+                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)
+            return self._get_topic_from_request()
 
-    def _get_topic_from_request(self, request):
+    def _get_topic_from_request(self):
         if current_topic:
             return current_topic.locked, current_forum.locked
         else:
@@ -129,176 +139,200 @@ class TopicNotLocked(Requirement):
 
 
 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 self._is_forum_locked(request)
+    def fulfill(self, user):
+        return self._is_forum_locked()
 
-    def _is_forum_locked(self, request):
-        forum = self._determine_forum(request)
+    def _is_forum_locked(self):
+        forum = self._determine_forum()
         return not forum.locked
 
-    def _determine_forum(self, request):
+    def _determine_forum(self):
         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)
+            return self._get_forum_from_request()
 
-    def _get_forum_from_request(self, request):
+    def _get_forum_from_request(self):
         if current_forum:
             return current_forum
-        raise FlaskBBError
+        raise FlaskBBError("Could not determine forum")
 
 
 class CanAccessForum(Requirement):
 
-    def fulfill(self, user, request):
+    def fulfill(self, user):
         if not current_forum:
             raise FlaskBBError("Could not load forum data")
 
         forum_groups = {g.id for g in current_forum.groups}
         user_groups = {g.id for g in user.groups}
-        return forum_groups & user_groups
-
-
-class CanAccessTopic(Requirement):
-
-    def fulfill(self, user, request):
-        if not current_forum:
-            raise FlaskBBError("Could not load topic data")
-
-        forum_groups = {g.id for g in current_forum.groups}
-        user_groups = {g.id for g in user.groups}
-        return forum_groups & user_groups
+        return bool(forum_groups & user_groups)
 
 
 def IsAtleastModeratorInForum(forum_id=None, forum=None):
-    return Or(IsAtleastSuperModerator, IsModeratorInForum(forum_id=forum_id,
-                                                          forum=forum))
+    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'))
+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'))
+CanBanUser = Or(IsAtleastSuperModerator, Has("mod_banuser"))
 
-CanEditUser = Or(IsAtleastSuperModerator, Has('mod_edituser'))
+CanEditUser = Or(IsAtleastSuperModerator, Has("mod_edituser"))
 
-CanEditPost = Or(IsAtleastSuperModerator,
-                 And(IsModeratorInForum(), Has('editpost')),
-                 And(IsSameUser(), Has('editpost'), TopicNotLocked()))
+CanEditPost = Or(
+    IsAtleastSuperModerator,
+    And(IsModeratorInForum(), Has("editpost")),
+    And(CanAccessForum(), IsSameUser(), Has("editpost"), TopicNotLocked()),
+)
 
 CanDeletePost = CanEditPost
 
-CanPostReply = Or(And(Has('postreply'), TopicNotLocked()),
-                  IsModeratorInForum(),
-                  IsAtleastSuperModerator)
+CanPostReply = Or(
+    And(CanAccessForum(), Has("postreply"), TopicNotLocked()),
+    IsModeratorInForum(),
+    IsAtleastSuperModerator,
+)
 
-CanPostTopic = Or(And(Has('posttopic'), ForumNotLocked()),
-                  IsAtleastSuperModerator,
-                  IsModeratorInForum())
+CanPostTopic = Or(
+    And(CanAccessForum(), Has("posttopic"), ForumNotLocked()),
+    IsAtleastSuperModerator,
+    IsModeratorInForum(),
+)
 
-CanDeleteTopic = Or(IsAtleastSuperModerator,
-                    And(IsModeratorInForum(), Has('deletetopic')),
-                    And(IsSameUser(), Has('deletetopic'), TopicNotLocked()))
+CanDeleteTopic = Or(
+    IsAtleastSuperModerator,
+    And(IsModeratorInForum(), Has("deletetopic")),
+    And(CanAccessForum(), IsSameUser(), Has("deletetopic"), TopicNotLocked()),
+)
 
 
-# Template Allowances -- gross, I know
+def permission_with_identity(requirement, name=None):
+    """
+    Permission instance factory that can set a user at construction time
+    can optionally name the closure for nicer debugging
+    """
 
-def TplCanModerate(request):
-    def _(user, forum):
-        kwargs = {}
+    def _(user):
+        return Permission(requirement, identity=user)
 
-        if isinstance(forum, int):
-            kwargs['forum_id'] = forum
-        elif isinstance(forum, Forum):
-            kwargs['forum'] = forum
+    if name is not None:
+        _.__name__ = name
 
-        return IsAtleastModeratorInForum(**kwargs)(user, request)
     return _
 
 
-def TplCanPostReply(request):
-    def _(user, topic=None):
-        kwargs = {}
+# Template Requirements
+
+
+def has_permission(permission):
 
-        if isinstance(topic, int):
-            kwargs['topic_id'] = topic
-        elif isinstance(topic, Topic):
-            kwargs['topic'] = topic
+    def _(user):
+        return Permission(Has(permission), identity=user)
 
-        return Or(
+    _.__name__ = "Has_{}".format(permission)
+    return _
+
+
+def can_moderate(user, forum):
+    kwargs = {}
+
+    if isinstance(forum, int):
+        kwargs["forum_id"] = forum
+    elif isinstance(forum, Forum):
+        kwargs["forum"] = forum
+
+    return Permission(IsAtleastModeratorInForum(**kwargs), identity=user)
+
+
+def can_post_reply(user, topic=None):
+    kwargs = {}
+
+    if isinstance(topic, int):
+        kwargs["topic_id"] = topic
+    elif isinstance(topic, Topic):
+        kwargs["topic"] = topic
+
+    return Permission(
+        Or(
             IsAtleastSuperModerator,
             IsModeratorInForum(),
-            And(Has('postreply'), TopicNotLocked(**kwargs))
-        )(user, request)
-    return _
+            And(Has("postreply"), TopicNotLocked(**kwargs)),
+        ),
+        identity=user,
+    )
 
 
-def TplCanEditPost(request):
-    def _(user, topic_or_post=None):
-        kwargs = {}
+def can_edit_post(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
+    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(
+    return Permission(
+        Or(
             IsAtleastSuperModerator,
-            And(IsModeratorInForum(), Has('editpost')),
+            And(IsModeratorInForum(), Has("editpost")),
             And(
                 IsSameUser(topic_or_post),
-                Has('editpost'),
-                TopicNotLocked(**kwargs)
+                Has("editpost"),
+                TopicNotLocked(**kwargs),
             ),
-        )(user, request)
-    return _
-
+        ),
+        identity=user,
+    )
 
-TplCanDeletePost = TplCanEditPost
 
+def can_post_topic(user, forum):
+    kwargs = {}
 
-def TplCanPostTopic(request):
-    def _(user, forum):
-        kwargs = {}
+    if isinstance(forum, int):
+        kwargs["forum_id"] = forum
+    elif isinstance(forum, Forum):
+        kwargs["forum"] = forum
 
-        if isinstance(forum, int):
-            kwargs['forum_id'] = forum
-        elif isinstance(forum, Forum):
-            kwargs['forum'] = forum
-
-        return Or(
+    return Permission(
+        Or(
             IsAtleastSuperModerator,
             IsModeratorInForum(**kwargs),
-            And(Has('posttopic'), ForumNotLocked(**kwargs))
-        )(user, request)
-    return _
+            And(Has("posttopic"), ForumNotLocked(**kwargs)),
+        ),
+        identity=user,
+    )
 
 
-def TplCanDeleteTopic(request):
-    def _(user, topic=None):
-        kwargs = {}
+def can_delete_topic(user, topic=None):
+    kwargs = {}
 
-        if isinstance(topic, int):
-            kwargs['topic_id'] = topic
-        elif isinstance(topic, Topic):
-            kwargs['topic'] = topic
+    if isinstance(topic, int):
+        kwargs["topic_id"] = topic
+    elif isinstance(topic, Topic):
+        kwargs["topic"] = topic
 
-        return Or(
+    return Permission(
+        Or(
             IsAtleastSuperModerator,
-            And(IsModeratorInForum(), Has('deletetopic')),
-            And(IsSameUser(), Has('deletetopic'), TopicNotLocked(**kwargs))
-        )(user, request)
-    return _
+            And(IsModeratorInForum(), Has("deletetopic")),
+            And(IsSameUser(), Has("deletetopic"), TopicNotLocked(**kwargs)),
+        ),
+        identity=user,
+    )

+ 1 - 1
requirements.txt

@@ -12,7 +12,7 @@ click-log==0.2.1
 enum34==1.1.6
 Flask==1.0.2
 Flask-Alembic==2.0.1
-flask-allows==0.4.0
+flask-allows==0.6.0
 Flask-BabelPlus==2.1.1
 Flask-Caching==1.4.0
 Flask-DebugToolbar==0.10.1

+ 11 - 6
tests/endtoend/test_auth_views.py

@@ -1,15 +1,20 @@
 from flask_login import login_user
 from flaskbb.management import views
-from flaskbb.exceptions import AuthorizationRequired
-import pytest
+from flask import get_flashed_messages
 
 
 def test_overview_not_authorized(application, default_settings):
     view = views.ManagementOverview.as_view('overview')
-    with application.test_request_context(), pytest.raises(AuthorizationRequired) as excinfo:  # noqa
-        view()
-
-    assert "Authorization is required to access this area." == excinfo.value.description  # noqa
+    with application.test_request_context():
+        result = view()
+        messages = get_flashed_messages(with_categories=True)
+
+    expected = (
+        'danger',
+        'You are not allowed to access the management panel'
+    )
+    assert result.status_code == 302
+    assert messages[0] == expected
 
 
 def test_overview_with_authorized(admin_user, application, default_settings):