From dfe82a15bfaf6428e8de45c57094c9f1bf0168e9 Mon Sep 17 00:00:00 2001
From: Robin Sonnabend <robin@fsmpi.rwth-aachen.de>
Date: Sun, 5 Mar 2017 02:34:03 +0100
Subject: [PATCH] Refactored authentication

---
 decorators.py                |  48 ++++++++-
 models/database.py           | 131 ++++++++++++++++--------
 server.py                    | 187 ++++++++---------------------------
 templates/protocol-show.html |   6 +-
 views/tables.py              |  14 +--
 5 files changed, 188 insertions(+), 198 deletions(-)

diff --git a/decorators.py b/decorators.py
index a51c214..cdc3e19 100644
--- a/decorators.py
+++ b/decorators.py
@@ -1,20 +1,28 @@
-from flask import redirect, flash, request
+from flask import redirect, flash, request, url_for
 
 from functools import wraps
 
+from models.database import ALL_MODELS
+from shared import db, current_user
+
 ID_KEY = "id"
 KEY_NOT_PRESENT_MESSAGE = "Missing {}_id."
 OBJECT_DOES_NOT_EXIST_MESSAGE = "There is no {} with id {}."
 
+MISSING_VIEW_RIGHT = "Dir fehlenden die nötigen Zugriffsrechte."
+
 def default_redirect():
     return redirect(request.args.get("next") or url_for("index"))
 
+def login_redirect():
+    return redirect(request.args.get("next") or url_for("login"))
+
 def db_lookup(*models, check_exists=True):
     def _decorator(function):
         @wraps(function)
         def _decorated_function(*args, **kwargs):
             for model in models:
-                key = model.__object_name__
+                key = model.__model_name__
                 id_key = "{}_{}".format(key, ID_KEY)
                 if id_key not in kwargs:
                     flash(KEY_NOT_PRESENT_MESSAGE.format(key), "alert-error")
@@ -31,3 +39,39 @@ def db_lookup(*models, check_exists=True):
             return function(*args, **kwargs)
         return _decorated_function
     return _decorator
+
+def require_right(right, require_exist):
+    necessary_right_name = "has_{}_right".format(right)
+    def _decorator(function):
+        @wraps(function)
+        def _decorated_function(*args, **kwargs):
+            user = current_user()
+            for model in ALL_MODELS:
+                model_name = model.__model_name__
+                if model_name in kwargs:
+                    model = kwargs[model_name]
+                    if model is None:
+                        if require_exist:
+                            flash(MISSING_VIEW_RIGHT, "alert-error")
+                            return login_redirect()
+                        else:
+                            continue
+                    necessary_right = getattr(model, necessary_right_name)
+                    if not necessary_right(user):
+                        flash(MISSING_VIEW_RIGHT, "alert-error")
+                        return login_redirect()
+            return function(*args, **kwargs)
+        return _decorated_function
+    return _decorator
+
+def require_public_view_right(require_exist=True):
+    return require_right("public_view", require_exist)
+
+def require_private_view_right(require_exist=True):
+    return require_right("private_view", require_exist)
+
+def require_modify_right(require_exist=True):
+    return require_right("modify", require_exist)
+
+def require_admin_right(require_exist=True):
+    return require_right("admin", require_exist)
diff --git a/models/database.py b/models/database.py
index a2b89b6..c329bbe 100644
--- a/models/database.py
+++ b/models/database.py
@@ -18,9 +18,25 @@ from sqlalchemy.ext.hybrid import hybrid_method
 import config
 from todostates import make_states
 
-class ProtocolType(db.Model):
+class DatabaseModel(db.Model):
+    __abstract__ = True
+
+    def has_public_view_right(self, user):
+        print("DBModel")
+        return self.get_parent().has_public_view_right(user)
+
+    def has_private_view_right(self, user):
+        return self.get_parent().has_private_view_right(user)
+
+    def has_modify_right(self, user):
+        return self.get_parent().has_modify_right(user)
+
+    def has_admin_right(self, user):
+        return self.get_parent().has_admin_right(user)
+
+class ProtocolType(DatabaseModel):
     __tablename__ = "protocoltypes"
-    __object_name__ = "protocoltype"
+    __model_name__ = "protocoltype"
     id = db.Column(db.Integer, primary_key=True)
     name = db.Column(db.String, unique=True)
     short_name = db.Column(db.String, unique=True)
@@ -91,7 +107,8 @@ class ProtocolType(db.Model):
 
     def has_public_view_right(self, user):
         return (self.has_public_anonymous_view_right()
-            or (user is not None and self.has_public_authenticated_view_right(user)))
+            or (user is not None and self.has_public_authenticated_view_right(user))
+            or self.has_admin_right(user))
 
     def has_public_anonymous_view_right(self):
         return (self.is_public
@@ -103,10 +120,15 @@ class ProtocolType(db.Model):
             or (self.private_group != "" and self.private_group in user.groups))
 
     def has_private_view_right(self, user):
-        return (user is not None and self.private_group != "" and self.private_group in user.groups)
+        return ((user is not None
+            and (self.private_group != "" and self.private_group in user.groups))
+            or self.has_admin_right(user))
+            
 
     def has_modify_right(self, user):
-        return (user is not None and self.modify_group != "" and self.modify_group in user.groups)
+        return ((user is not None
+            and (self.modify_group != "" and self.modify_group in user.groups))
+            or self.has_admin_right(user))
 
     def has_admin_right(self, user):
         return (user is not None and config.ADMIN_GROUP in user.groups)
@@ -138,9 +160,9 @@ class ProtocolType(db.Model):
     def get_wiki_infobox_title(self):
         return "Vorlage:{}".format(self.get_wiki_infobox())
 
-class Protocol(db.Model):
+class Protocol(DatabaseModel):
     __tablename__ = "protocols"
-    __object_name__ = "protocol"
+    __model_name__ = "protocol"
     id = db.Column(db.Integer, primary_key=True)
     protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
     source = db.Column(db.String)
@@ -173,6 +195,9 @@ class Protocol(db.Model):
         return "<Protocol(id={}, protocoltype_id={})>".format(
             self.id, self.protocoltype_id)
 
+    def get_parent(self):
+        return self.protocoltype
+
     def create_error(self, action, name, description):
         now = datetime.now()
         return Error(self.id, action, name, now, description)
@@ -225,12 +250,6 @@ class Protocol(db.Model):
             or self.protocoltype.has_private_view_right(user)
         )
 
-    def has_private_view_right(self, user):
-        return self.protocoltype.has_private_view_right(user)
-
-    def has_modify_right(self, user):
-        return self.protocoltype.has_modify_right(user)
-
     def is_done(self):
         return self.done
 
@@ -303,9 +322,9 @@ def on_protocol_delete(mapper, connection, protocol):
     protocol.delete_orphan_todos()
 
 
-class DefaultTOP(db.Model):
+class DefaultTOP(DatabaseModel):
     __tablename__ = "defaulttops"
-    __object_name__ = "defaulttop"
+    __model_name__ = "defaulttop"
     id = db.Column(db.Integer, primary_key=True)
     protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
     name = db.Column(db.String)
@@ -320,12 +339,15 @@ class DefaultTOP(db.Model):
         return "<DefaultTOP(id={}, protocoltype_id={}, name={}, number={})>".format(
             self.id, self.protocoltype_id, self.name, self.number)
 
+    def get_parent(self):
+        return self.protocoltype
+
     def is_at_end(self):
         return self.number > 0
 
-class TOP(db.Model):
+class TOP(DatabaseModel):
     __tablename__ = "tops"
-    __object_name__ = "top"
+    __model_name__ = "top"
     id = db.Column(db.Integer, primary_key=True)
     protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
     name = db.Column(db.String)
@@ -344,9 +366,12 @@ class TOP(db.Model):
         return "<TOP(id={}, protocol_id={}, name={}, number={}, planned={})>".format(
             self.id, self.protocol_id, self.name, self.number, self.planned)
 
-class Document(db.Model):
+    def get_parent(self):
+        return self.protocol
+
+class Document(DatabaseModel):
     __tablename__ = "documents"
-    __object_name__ = "document"
+    __model_name__ = "document"
     id = db.Column(db.Integer, primary_key=True)
     protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
     name = db.Column(db.String)
@@ -365,6 +390,9 @@ class Document(db.Model):
         return "<Document(id={}, protocol_id={}, name={}, filename={}, is_compiled={}, is_private={})>".format(
             self.id, self.protocol_id, self.name, self.filename, self.is_compiled, self.is_private)
 
+    def get_parent(self):
+        return self.protocol
+
     def get_filename(self):
         return os.path.join(config.DOCUMENTS_PATH, self.filename)
 
@@ -379,9 +407,9 @@ def on_document_delete(mapper, connection, document):
         if os.path.isfile(document_path):
             os.remove(document_path)
 
-class DecisionDocument(db.Model):
+class DecisionDocument(DatabaseModel):
     __tablename__ = "decisiondocuments"
-    __object_name__ = "decisiondocument"
+    __model_name__ = "decisiondocument"
     id = db.Column(db.Integer, primary_key=True)
     decision_id = db.Column(db.Integer, db.ForeignKey("decisions.id"))
     name = db.Column(db.String)
@@ -396,6 +424,9 @@ class DecisionDocument(db.Model):
         return "<DecisionDocument(id={}, decision_id={}, name={}, filename={})>".format(
             self.id, self.decision_id, self.name, self.filename)
 
+    def get_parent(self):
+        return self.decision
+
     def get_filename(self):
         return os.path.join(config.DOCUMENTS_PATH, self.filename)
 
@@ -487,9 +518,9 @@ class TodoState(Enum):
         return state, date
 
 
-class Todo(db.Model):
+class Todo(DatabaseModel):
     __tablename__ = "todos"
-    __object_name__ = "todo"
+    __model_name__ = "todo"
     id = db.Column(db.Integer, primary_key=True)
     protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
     number = db.Column(db.Integer)
@@ -512,6 +543,9 @@ class Todo(db.Model):
         return "<Todo(id={}, number={}, who={}, description={}, state={}, date={})>".format(
             self.id, self.number, self.who, self.description, self.state, self.date)
 
+    def get_parent(self):
+        return self.protocoltype
+
     def is_done(self):
         if self.state.needs_date():
             if self.state == TodoState.after:
@@ -581,15 +615,14 @@ class Todo(db.Model):
         parts.append("id {}".format(self.get_id()))
         return "[{}]".format(";".join(parts))
 
-
-class TodoProtocolAssociation(db.Model):
+class TodoProtocolAssociation(DatabaseModel):
     __tablename__ = "todoprotocolassociations"
     todo_id = db.Column(db.Integer, db.ForeignKey("todos.id"), primary_key=True)
     protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"), primary_key=True)
 
-class Decision(db.Model):
+class Decision(DatabaseModel):
     __tablename__ = "decisions"
-    __object_name__ = "decision"
+    __model_name__ = "decision"
     id = db.Column(db.Integer, primary_key=True)
     protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
     content = db.Column(db.String)
@@ -604,9 +637,12 @@ class Decision(db.Model):
         return "<Decision(id={}, protocol_id={}, content='{}')>".format(
             self.id, self.protocol_id, self.content)
 
-class MeetingReminder(db.Model):
+    def get_parent(self):
+        return self.protocol
+
+class MeetingReminder(DatabaseModel):
     __tablename__ = "meetingreminders"
-    __object_name__ = "meetingreminder"
+    __model_name__ = "meetingreminder"
     id = db.Column(db.Integer, primary_key=True)
     protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
     days_before = db.Column(db.Integer)
@@ -625,9 +661,12 @@ class MeetingReminder(db.Model):
         return "<MeetingReminder(id={}, protocoltype_id={}, days_before={}, send_public={}, send_private={})>".format(
             self.id, self.protocoltype_id, self.days_before, self.send_public, self.send_private)
 
-class Error(db.Model):
+    def get_parent(self):
+        return self.protocoltype
+
+class Error(DatabaseModel):
     __tablename__ = "errors"
-    __object_name__ = "error"
+    __model_name__ = "error"
     id = db.Column(db.Integer, primary_key=True)
     protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
     action = db.Column(db.String)
@@ -646,15 +685,18 @@ class Error(db.Model):
         return "<Error(id={}, protocol_id={}, action={}, name={}, datetime={})>".format(
             self.id, self.protocol_id, self.action, self.name, self.datetime)
 
+    def get_parent(self):
+        return self.protocol
+
     def get_short_description(self):
         lines = self.description.splitlines()
         if len(lines) <= 4:
             return "\n".join(lines)
         return "\n".join([*lines[:2], "…", *lines[-2:]])
 
-class TodoMail(db.Model):
+class TodoMail(DatabaseModel):
     __tablename__ = "todomails"
-    __object_name__ = "todomail"
+    __model_name__ = "todomail"
     id = db.Column(db.Integer, primary_key=True)
     name = db.Column(db.String, unique=True)
     mail = db.Column(db.String)
@@ -670,9 +712,9 @@ class TodoMail(db.Model):
     def get_formatted_mail(self):
         return "{} <{}>".format(self.name, self.mail)
 
-class OldTodo(db.Model):
+class OldTodo(DatabaseModel):
     __tablename__ = "oldtodos"
-    __object_name__ = "oldtodo"
+    __model_name__ = "oldtodo"
     id = db.Column(db.Integer, primary_key=True)
     old_id = db.Column(db.Integer)
     who = db.Column(db.String)
@@ -690,9 +732,9 @@ class OldTodo(db.Model):
             "protocol={}".format(self.id, self.old_id, self.who,
             self.description, self.protocol_key))
 
-class DefaultMeta(db.Model):
+class DefaultMeta(DatabaseModel):
     __tablename__ = "defaultmetas"
-    __object_name__ = "defaultmeta"
+    __model_name__ = "defaultmeta"
     id = db.Column(db.Integer, primary_key=True)
     protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
     key = db.Column(db.String)
@@ -707,9 +749,12 @@ class DefaultMeta(db.Model):
         return ("<DefaultMeta(id={}, protocoltype_id={}, key='{}', "
             "name='{}')>".format(self.id, self.protocoltype_id, self.key))
 
-class Meta(db.Model):
+    def get_parent(self):
+        return self.protocoltype
+
+class Meta(DatabaseModel):
     __tablename__ = "metas"
-    __object_name__ = "meta"
+    __model_name__ = "meta"
     id = db.Column(db.Integer, primary_key=True)
     protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
     name = db.Column(db.String)
@@ -724,3 +769,11 @@ class Meta(db.Model):
         return "<Meta(id={}, protocoltype_id={}, name={}, value={})>".format(
             self.id, self.protocoltype_id, self.name, self.value)
 
+    def get_parent(self):
+        return self.protocol
+
+ALL_MODELS = [
+    ProtocolType, Protocol, DefaultTOP, TOP, Document, DecisionDocument,
+    Todo, Decision, MeetingReminder, Error, DefaultMeta, Meta
+]
+    
diff --git a/server.py b/server.py
index 22fd908..a84c97b 100755
--- a/server.py
+++ b/server.py
@@ -22,7 +22,7 @@ import mimetypes
 import config
 from shared import db, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, ldap_manager, security_manager, current_user, check_login, login_required, group_required, class_filter, needs_date_test, todostate_name_filter, code_filter, indent_tab_filter
 from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg
-from decorators import db_lookup
+from decorators import db_lookup, require_public_view_right, require_private_view_right, require_modify_right, require_admin_right
 from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta
 from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm
 from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable
@@ -189,11 +189,9 @@ def new_type():
 @app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(ProtocolType)
+@require_private_view_right()
 def edit_type(protocoltype):
     user = current_user()
-    if not protocoltype.has_private_view_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     form = ProtocolTypeForm(obj=protocoltype)
     if form.validate_on_submit():
         if form.private_group.data not in user.groups:
@@ -207,11 +205,8 @@ def edit_type(protocoltype):
 @app.route("/type/show/<int:protocoltype_id>")
 @login_required
 @db_lookup(ProtocolType)
+@require_private_view_right()
 def show_type(protocoltype):
-    user = current_user()
-    if not protocoltype.has_private_view_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     protocoltype_table = ProtocolTypeTable(protocoltype)
     default_tops_table = DefaultTOPsTable(protocoltype.default_tops, protocoltype)
     reminders_table = MeetingRemindersTable(protocoltype.reminders, protocoltype)
@@ -222,11 +217,8 @@ def show_type(protocoltype):
 @login_required
 @group_required(config.ADMIN_GROUP)
 @db_lookup(ProtocolType)
+@require_modify_right()
 def delete_type(protocoltype):
-    user = current_user()
-    if not protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(reqeust.args.get("next") or url_for("index"))
     name = protocoltype.name
     db.session.delete(protocoltype) 
     db.session.commit()
@@ -236,11 +228,8 @@ def delete_type(protocoltype):
 @app.route("/type/reminders/new/<int:protocoltype_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(ProtocolType)
+@require_modify_right()
 def new_reminder(protocoltype):
-    user = current_user()
-    if not protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     form = MeetingReminderForm()
     if form.validate_on_submit():
         meetingreminder = MeetingReminder(protocoltype.id, form.days_before.data, form.send_public.data, form.send_private.data, form.additional_text.data)
@@ -252,11 +241,8 @@ def new_reminder(protocoltype):
 @app.route("/type/reminder/edit/<int:meetingreminder_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(MeetingReminder)
+@require_modify_right()
 def edit_reminder(meetingreminder):
-    user = current_user()
-    if not meetingreminder.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     form = MeetingReminderForm(obj=meetingreminder)
     if form.validate_on_submit():
         form.populate_obj(meetingreminder)
@@ -267,11 +253,8 @@ def edit_reminder(meetingreminder):
 @app.route("/type/reminder/delete/<int:meetingreminder_id>")
 @login_required
 @db_lookup(MeetingReminder)
+@require_modify_right()
 def delete_reminder(meetingreminder):
-    user = current_user()
-    if not meetingreminder.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     protocoltype = meetingreminder.protocoltype
     db.session.delete(meetingreminder)
     db.session.commit()
@@ -280,11 +263,8 @@ def delete_reminder(meetingreminder):
 @app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(ProtocolType)
+@require_modify_right()
 def new_default_top(protocoltype):
-    user = current_user()
-    if not protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     form = DefaultTopForm()
     if form.validate_on_submit():
         defaulttop = DefaultTOP(protocoltype.id, form.name.data, form.number.data)
@@ -297,11 +277,8 @@ def new_default_top(protocoltype):
 @app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(ProtocolType, DefaultTOP)
+@require_modify_right()
 def edit_default_top(protocoltype, defaulttop):
-    user = current_user()
-    if not protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     form = DefaultTopForm(obj=defaulttop)
     if form.validate_on_submit():
         form.populate_obj(defaulttop)
@@ -312,11 +289,8 @@ def edit_default_top(protocoltype, defaulttop):
 @app.route("/type/tops/delete/<int:defaulttop_id>")
 @login_required
 @db_lookup(DefaultTOP)
+@require_modify_right()
 def delete_default_top(defaulttop):
-    user = current_user()
-    if not defaulttop.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     db.session.delete(defaulttop)
     db.session.commit()
     return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
@@ -324,11 +298,8 @@ def delete_default_top(defaulttop):
 @app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
 @login_required
 @db_lookup(DefaultTOP)
+@require_modify_right()
 def move_default_top(defaulttop, diff):
-    user = current_user()
-    if not defaulttop.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     try:
         defaulttop.number += int(diff)
         db.session.commit()
@@ -461,10 +432,10 @@ def new_protocol():
 @db_lookup(Protocol)
 def show_protocol(protocol):
     user = current_user()
-    if not protocol.protocoltype.has_public_view_right(user):
-        flash("Die fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     errors_table = ErrorsTable(protocol.errors)
+    if not protocol.protocoltype.has_public_view_right(user): # yes, feature
+        flash("Die fehlen die nötigen Zugriffsrechte.", "alert-error")
+        return redirect(request.args.get("next") or url_for("login"))
     visible_documents = [
         document for document in protocol.documents
         if (not document.is_private and document.protocol.has_public_view_right(user))
@@ -479,11 +450,8 @@ def show_protocol(protocol):
 @login_required
 @group_required(config.ADMIN_GROUP)
 @db_lookup(Protocol)
+@require_modify_right()
 def delete_protocol(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     name = protocol.get_identifier()
     protocol.delete_orphan_todos()
     db.session.delete(protocol)
@@ -494,11 +462,8 @@ def delete_protocol(protocol):
 @app.route("/protocol/etherpull/<int:protocol_id>")
 @login_required
 @db_lookup(Protocol)
+@require_modify_right()
 def etherpull_protocol(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Die fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     if not config.ETHERPAD_ACTIVE:
         flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
         return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
@@ -511,12 +476,8 @@ def etherpull_protocol(protocol):
 @app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
 @login_required
 @db_lookup(Protocol)
+@require_modify_right()
 def upload_source_to_known_protocol(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
-    form = KnownProtocolSourceUploadForm()
     if form.validate_on_submit():
         if form.source.data is None:
             flash("Es wurde keine Datei ausgewählt.", "alert-error")
@@ -595,44 +556,32 @@ def upload_new_protocol_by_file():
 @login_required
 @group_required(config.ADMIN_GROUP)
 @db_lookup(Protocol)
+@require_modify_right()
 def recompile_protocol(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Die fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     tasks.parse_protocol(protocol)
     return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
 
 @app.route("/protocol/source/<int:protocol_id>")
 @login_required
 @db_lookup(Protocol)
+@require_modify_right()
 def get_protocol_source(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     file_like = BytesIO(protocol.source.encode("utf-8"))
     return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename="{}.txt".format(protocol.get_identifier()))
 
 @app.route("/protocol/template/<int:protocol_id>")
 @login_required
 @db_lookup(Protocol)
+@require_modify_right()
 def get_protocol_template(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     file_like = BytesIO(protocol.get_template().encode("utf-8"))
     return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename="{}-template.txt".format(protocol.get_identifier()))
 
 @app.route("/protocol/etherpush/<int:protocol_id>")
 @login_required
 @db_lookup(Protocol)
+@require_modify_right()
 def etherpush_protocol(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Invalides Protokoll oder keine Berechtigung.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     if not config.ETHERPAD_ACTIVE:
         flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
         return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
@@ -643,11 +592,8 @@ def etherpush_protocol(protocol):
 @app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(Protocol)
+@require_modify_right()
 def update_protocol(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     upload_form = KnownProtocolSourceUploadForm()
     edit_form = ProtocolForm(obj=protocol)
     if edit_form.validate_on_submit():
@@ -660,11 +606,8 @@ def update_protocol(protocol):
 @app.route("/protocol/publish/<int:protocol_id>")
 @login_required
 @db_lookup(Protocol)
+@require_modify_right()
 def publish_protocol(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     protocol.public = True
     db.session.commit()
     return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
@@ -672,11 +615,8 @@ def publish_protocol(protocol):
 @app.route("/prococol/send/<int:protocol_id>")
 @login_required
 @db_lookup(Protocol)
+@require_modify_right()
 def send_protocol(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     if not config.MAIL_ACTIVE:
         flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
         return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
@@ -687,11 +627,8 @@ def send_protocol(protocol):
 @app.route("/protocol/tops/new/<int:protocol_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(Protocol)
+@require_modify_right()
 def new_top(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen dir nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     form = TopForm()
     if form.validate_on_submit():
         top = TOP(protocol_id=protocol.id, name=form.name.data, number=form.number.data, planned=True)
@@ -708,11 +645,8 @@ def new_top(protocol):
 @app.route("/protocol/top/edit/<int:top_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(TOP)
+@require_modify_right()
 def edit_top(top):
-    user = current_user()
-    if not top.protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     form = TopForm(obj=top)
     if form.validate_on_submit():
         form.populate_obj(top)
@@ -724,11 +658,8 @@ def edit_top(top):
 @app.route("/protocol/top/delete/<int:top_id>")
 @login_required
 @db_lookup(TOP)
+@require_modify_right()
 def delete_top(top):
-    user = current_user()
-    if not top.protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     name = top.name
     protocol = top.protocol
     db.session.delete(top)
@@ -740,11 +671,8 @@ def delete_top(top):
 @app.route("/protocol/top/move/<int:top_id>/<diff>")
 @login_required
 @db_lookup(TOP)
+@require_modify_right()
 def move_top(top, diff):
-    user = current_user()
-    if not top.protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     try:
         top.number += int(diff)
         db.session.commit()
@@ -850,11 +778,8 @@ def new_todo():
 @app.route("/todo/edit/<int:todo_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(Todo)
+@require_modify_right()
 def edit_todo(todo):
-    user = current_user()
-    if not todo.protocoltype.has_modify_right(user):
-        flash("Invalides Todo oder unzureichende Berechtigung.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     form = TodoForm(obj=todo)
     if form.validate_on_submit():
         form.populate_obj(todo)
@@ -865,22 +790,16 @@ def edit_todo(todo):
 @app.route("/todo/show/<int:todo_id>")
 @login_required
 @db_lookup(Todo)
+@require_private_view_right()
 def show_todo(todo):
-    user = current_user()
-    if not todo.protocoltype.has_private_view_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     todo_table = TodoTable(todo)
     return render_template("todo-show.html", todo=todo, todo_table=todo_table)
 
 @app.route("/todo/delete/<int:todo_id>")
 @login_required
 @db_lookup(Todo)
+@require_private_view_right()
 def delete_todo(todo):
-    user = current_user()
-    if not todo.protocoltype.has_private_view_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     type_id = todo.protocoltype.id
     db.session.delete(todo)
     db.session.commit()
@@ -946,11 +865,8 @@ def download_document(document):
 @app.route("/document/upload/<int:protocol_id>", methods=["POST"])
 @login_required
 @db_lookup(Protocol)
+@require_modify_right()
 def upload_document(protocol):
-    user = current_user()
-    if not protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     form = DocumentUploadForm()
     if form.document.data is None:
         flash("Es wurde keine Datei ausgewählt.", "alert-error")
@@ -977,11 +893,8 @@ def upload_document(protocol):
 @login_required
 @group_required(config.ADMIN_GROUP)
 @db_lookup(Document)
+@require_modify_right()
 def delete_document(document):
-    user = current_user()
-    if not document.protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     name = document.name
     protocol = document.protocol
     db.session.delete(document)
@@ -992,11 +905,8 @@ def delete_document(document):
 @app.route("/document/print/<int:document_id>")
 @login_required
 @db_lookup(Document)
+@require_modify_right()
 def print_document(document):
-    user = current_user()
-    if not document.protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     if not config.PRINTING_ACTIVE:
         flash("Die Druckfunktion ist nicht aktiviert.", "alert-error")
         return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=document.protocol.id))
@@ -1007,11 +917,9 @@ def print_document(document):
 @app.route("/decision/print/<int:decisiondocument_id>")
 @login_required
 @db_lookup(DecisionDocument)
+@require_modify_right()
 def print_decision(decisiondocument):
     user = current_user()
-    if not decisiondocument.decision.protocol.protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     if not config.PRINTING_ACTIVE:
         flash("Die Druckfunktion ist nicht aktiviert.", "alert-error")
         return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=decisiondocument.decision.protocol.id))
@@ -1033,22 +941,16 @@ def list_errors():
 @app.route("/error/show/<int:error_id>")
 @login_required
 @db_lookup(Error)
+@require_modify_right()
 def show_error(error):
-    user = current_user()
-    if not error.protocol.protocoltype.has_modify_right(user):
-        flash("Die fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     error_table = ErrorTable(error)
     return render_template("error-show.html", error=error, error_table=error_table)
 
 @app.route("/error/delete/<int:error_id>")
 @login_required
 @db_lookup(Error)
+@require_modify_right()
 def delete_error(error):
-    user = current_user()
-    if not error.protocol.protocoltype.has_modify_right(user):
-        flash("Invalider Fehler oder fehlende Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     name = error.name
     db.session.delete(error)
     db.session.commit()
@@ -1099,11 +1001,8 @@ def delete_todomail(todomail):
 @app.route("/defaultmeta/new/<int:protocoltype_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(ProtocolType)
+@require_modify_right()
 def new_defaultmeta(protocoltype):
-    user = current_user()
-    if not protocoltype.has_modify_right(user):
-        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     form = DefaultMetaForm()
     if form.validate_on_submit():
         meta = DefaultMeta(protocoltype_id=protocoltype.id, key=form.key.data,
@@ -1117,11 +1016,8 @@ def new_defaultmeta(protocoltype):
 @app.route("/defaultmeta/edit/<int:defaultmeta_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(DefaultMeta)
+@require_modify_right()
 def edit_defaultmeta(defaultmeta):
-    user = current_user()
-    if not defaultmeta.protocoltype.has_modify_right(user):
-        flash("Invalider Protokolltyp oder unzureichende Rechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     form = DefaultMetaForm(obj=defaultmeta)
     if form.validate_on_submit():
         form.populate_obj(defaultmeta)
@@ -1133,11 +1029,8 @@ def edit_defaultmeta(defaultmeta):
 @login_required
 @group_required(config.ADMIN_GROUP)
 @db_lookup(DefaultMeta)
+@require_modify_right()
 def delete_defaultmeta(defaultmeta):
-    user = current_user()
-    if not meta.protocoltype.has_modify_right(user):
-        flash("Invalider Protokolltyp oder unzureichende Rechte.", "alert-error")
-        return redirect(request.args.get("next") or url_for("index"))
     name = defaultmeta.name
     type_id = defaultmeta.protocoltype.id
     db.session.delete(meta)
diff --git a/templates/protocol-show.html b/templates/protocol-show.html
index 1821308..97ef0b5 100644
--- a/templates/protocol-show.html
+++ b/templates/protocol-show.html
@@ -8,7 +8,7 @@
 {% set has_public_view_right = protocol.has_public_view_right(user) %}
 {% set has_private_view_right = protocol.has_private_view_right(user) %}
 {% set has_modify_right = protocol.has_modify_right(user) %}
-{% set has_admin_right = protocol.protocoltype.has_admin_right(user) %}
+{% set has_admin_right = protocol.has_admin_right(user) %}
 
 {% block content %}
 <div class="container">
@@ -114,8 +114,8 @@
                 {% endif %}
             {% endif %}
             {% if has_modify_right %}
-            {{render_form(source_upload_form, action_url=url_for("upload_source_to_known_protocol", protocol_id=protocol.id, next=url_for("show_protocol", protocol_id=protocol.id)), action_text="Hochladen", enctype="multipart/form-data")}}
-            {{render_form(document_upload_form, action_url=url_for("upload_document", protocol_id=protocol.id, next=url_for("show_protocol", protocol_id=protocol.id)), action_text="Hochladen", enctype="multipart/form-data")}}
+                {{render_form(source_upload_form, action_url=url_for("upload_source_to_known_protocol", protocol_id=protocol.id, next=url_for("show_protocol", protocol_id=protocol.id)), action_text="Hochladen", enctype="multipart/form-data")}}
+                {{render_form(document_upload_form, action_url=url_for("upload_document", protocol_id=protocol.id, next=url_for("show_protocol", protocol_id=protocol.id)), action_text="Hochladen", enctype="multipart/form-data")}}
             {% endif %}
         </div>
     </div>
diff --git a/views/tables.py b/views/tables.py
index 11e6ad3..0faf603 100644
--- a/views/tables.py
+++ b/views/tables.py
@@ -174,7 +174,7 @@ class ProtocolTypeTable(SingleValueTable):
 
 class DefaultTOPsTable(Table):
     def __init__(self, tops, protocoltype=None):
-        super().__init__("Standard-TOPs", tops, newlink=url_for("new_default_top", type_id=protocoltype.id) if protocoltype is not None else None)
+        super().__init__("Standard-TOPs", tops, newlink=url_for("new_default_top", protocoltype_id=protocoltype.id) if protocoltype is not None else None)
         self.protocoltype = protocoltype
 
     def headers(self):
@@ -185,10 +185,10 @@ class DefaultTOPsTable(Table):
             top.name,
             top.number,
             Table.concat([
-                Table.link(url_for("move_default_top", type_id=self.protocoltype.id, top_id=top.id, diff=1), "Runter"),
-                Table.link(url_for("move_default_top", type_id=self.protocoltype.id, top_id=top.id, diff=-1), "Hoch"),
-                Table.link(url_for("edit_default_top", type_id=self.protocoltype.id, top_id=top.id), "Ändern"),
-                Table.link(url_for("delete_default_top", type_id=self.protocoltype.id, top_id=top.id), "Löschen", confirm="Bist du dir sicher, dass du den Standard-TOP {} löschen willst?".format(top.name))
+                Table.link(url_for("move_default_top", protocoltype_id=self.protocoltype.id, defaulttop_id=top.id, diff=1), "Runter"),
+                Table.link(url_for("move_default_top", protocoltype_id=self.protocoltype.id, defaulttop_id=top.id, diff=-1), "Hoch"),
+                Table.link(url_for("edit_default_top", protocoltype_id=self.protocoltype.id, defaulttop_id=top.id), "Ändern"),
+                Table.link(url_for("delete_default_top", protocoltype_id=self.protocoltype.id, defaulttop_id=top.id), "Löschen", confirm="Bist du dir sicher, dass du den Standard-TOP {} löschen willst?".format(top.name))
             ])
         ]
 
@@ -386,8 +386,8 @@ class DefaultMetasTable(Table):
             meta.key,
         ]
         links = [
-            Table.link(url_for("edit_defaultmeta", meta_id=meta.id), "Ändern"),
-            Table.link(url_for("delete_defaultmeta", meta_id=meta.id, confirm="Bist du dir sicher, dass du das Metadatenfeld {} löschen willst?".format(meta.name)), "Löschen")
+            Table.link(url_for("edit_defaultmeta", defaultmeta_id=meta.id), "Ändern"),
+            Table.link(url_for("delete_defaultmeta", defaultmeta_id=meta.id, confirm="Bist du dir sicher, dass du das Metadatenfeld {} löschen willst?".format(meta.name)), "Löschen")
         ]
         link_part = [Table.concat(links)]
         return general_part + link_part
-- 
GitLab