From 2ea5356438f22ccbe7fd52db084bf2218b0e2b0d Mon Sep 17 00:00:00 2001
From: kaiyou <pierre@jaury.eu>
Date: Sat, 5 Oct 2019 17:44:14 +0200
Subject: [PATCH] Add a very basic permission system

---
 .../{b2fe21a1da94_.py => cfb466a78348_.py}    | 16 ++++----
 trurt/account/login.py                        |  3 +-
 trurt/account/profiles.py                     | 12 ++----
 trurt/account/settings.py                     |  8 ++--
 trurt/models.py                               |  1 +
 trurt/security.py                             | 41 +++++++++++++++++++
 trurt/service/admin.py                        |  7 +++-
 trurt/sso/saml.py                             |  2 +-
 trurt/templates/sidebar.html                  |  6 ++-
 trurt/utils.py                                |  1 +
 10 files changed, 70 insertions(+), 27 deletions(-)
 rename migrations/versions/{b2fe21a1da94_.py => cfb466a78348_.py} (89%)
 create mode 100644 trurt/security.py

diff --git a/migrations/versions/b2fe21a1da94_.py b/migrations/versions/cfb466a78348_.py
similarity index 89%
rename from migrations/versions/b2fe21a1da94_.py
rename to migrations/versions/cfb466a78348_.py
index 91a95e59..2c1cfad2 100644
--- a/migrations/versions/b2fe21a1da94_.py
+++ b/migrations/versions/cfb466a78348_.py
@@ -1,26 +1,28 @@
 """ empty message
 
-Revision ID: b2fe21a1da94
+Revision ID: cfb466a78348
 Revises:
-Create Date: 2019-09-14 13:27:34.971323
+Create Date: 2019-10-05 17:05:31.015711
 """
 from alembic import op
 import sqlalchemy as sa
 
 
-revision = 'b2fe21a1da94'
+revision = 'cfb466a78348'
 down_revision = None
 branch_labels = None
 depends_on = None
 
 
 def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
     op.create_table('service',
     sa.Column('protocol', sa.String(length=25), nullable=True),
     sa.Column('name', sa.String(length=255), nullable=True),
+    sa.Column('provider', sa.String(length=255), nullable=True),
+    sa.Column('application', sa.String(length=255), nullable=True),
     sa.Column('description', sa.String(), nullable=True),
-    sa.Column('max_profiles', sa.Integer(), nullable=True),
+    sa.Column('policy', sa.String(length=255), nullable=True),
+    sa.Column('max_profiles', sa.Integer(), nullable=False),
     sa.Column('config', sa.String(), nullable=True),
     sa.Column('uuid', sa.String(length=36), nullable=False),
     sa.Column('created_at', sa.DateTime(), nullable=False),
@@ -30,6 +32,7 @@ def upgrade():
     )
     op.create_table('user',
     sa.Column('username', sa.String(length=255), nullable=False),
+    sa.Column('is_admin', sa.Boolean(), nullable=False),
     sa.Column('uuid', sa.String(length=36), nullable=False),
     sa.Column('created_at', sa.DateTime(), nullable=False),
     sa.Column('updated_at', sa.DateTime(), nullable=True),
@@ -78,14 +81,11 @@ def upgrade():
     sa.ForeignKeyConstraint(['user_uuid'], ['user.uuid'], ),
     sa.PrimaryKeyConstraint('uuid')
     )
-    # ### end Alembic commands ###
 
 
 def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
     op.drop_table('history')
     op.drop_table('profile')
     op.drop_table('auth')
     op.drop_table('user')
     op.drop_table('service')
-    # ### end Alembic commands ###
diff --git a/trurt/account/login.py b/trurt/account/login.py
index fc9e8406..c2cca964 100644
--- a/trurt/account/login.py
+++ b/trurt/account/login.py
@@ -1,4 +1,4 @@
-from trurt import models, utils
+from trurt import models, utils, security
 from trurt.account import blueprint, forms
 
 import flask_login
@@ -21,6 +21,7 @@ def signin():
 
 
 @blueprint.route("/signout")
+@security.authentication_required()
 def signout():
     flask_login.logout_user()
     return flask.redirect(flask.url_for(".signin"))
diff --git a/trurt/account/profiles.py b/trurt/account/profiles.py
index f7cda945..b65c390a 100644
--- a/trurt/account/profiles.py
+++ b/trurt/account/profiles.py
@@ -1,8 +1,7 @@
 from trurt.account import blueprint, forms
 from trurt.sso import forms as sso_forms
-from trurt import models, utils
+from trurt import models, utils, security
 
-import flask_login
 import flask
 
 
@@ -17,13 +16,8 @@ def pick_profile(service, **redirect_args):
     utils.force_redirect(utils.url_for("account.pick", **redirect_args))
 
 
-@blueprint.route("/profiles")
-def profiles():
-    return flask.render_template("account_profiles.html")
-
-
 @blueprint.route("/pick")
-@flask_login.login_required
+@security.authentication_required()
 def pick():
     service_uuid = flask.request.args.get("service_uuid") or flask.abort(404)
     service = models.Service.query.get(service_uuid) or flask.abort(404)
@@ -39,7 +33,7 @@ def pick():
 
 
 @blueprint.route("/profile/create", methods=["GET", "POST"])
-@flask_login.login_required
+@security.admin_required()
 def create_profile():
     service_uuid = flask.request.args.get("service_uuid") or flask.abort(404)
     service = models.Service.query.get(service_uuid) or flask.abort(404)
diff --git a/trurt/account/settings.py b/trurt/account/settings.py
index b4208a6e..fb6fc3c8 100644
--- a/trurt/account/settings.py
+++ b/trurt/account/settings.py
@@ -1,19 +1,19 @@
 from trurt.account import blueprint, forms
-from trurt import models
+from trurt import models, security
 
-import flask_login
 import flask
+import flask_login
 
 
 @blueprint.route("/home")
-@flask_login.login_required
+@security.authentication_required()
 def home():
     history = flask_login.current_user.history
     return flask.render_template("account_home.html", history=history)
 
 
 @blueprint.route("/password", methods=["GET", "POST"])
-@flask_login.login_required
+@security.authentication_required()
 def password():
     form = forms.PasswordForm()
     if form.validate_on_submit():
diff --git a/trurt/models.py b/trurt/models.py
index 020d2b33..7f94c2ec 100644
--- a/trurt/models.py
+++ b/trurt/models.py
@@ -73,6 +73,7 @@ class User(db.Model):
     __tablename__ = "user"
 
     username = db.Column(db.String(255), nullable=False, unique=True)
+    is_admin = db.Column(db.Boolean(), nullable=False, default=False)
 
     # Flask-login attributes
     is_authenticated = True
diff --git a/trurt/security.py b/trurt/security.py
new file mode 100644
index 00000000..b7bd1d29
--- /dev/null
+++ b/trurt/security.py
@@ -0,0 +1,41 @@
+import flask_login
+import flask
+import functools
+
+
+def permissions_wrapper(handler):
+    """ Decorator that produces a decorator for checking permissions.
+    """
+    def callback(function, args, kwargs, wrapper_args, wrapper_kwargs):
+        authorized = handler(args, kwargs, *wrapper_args, **wrapper_kwargs)
+        if not authorized:
+            flask.abort(403)
+        elif type(authorized) is int:
+            flask.abort(authorized)
+        else:
+            return function(*args, **kwargs)
+
+    def decorator(*wrapper_args, **wrapper_kwargs):
+        def inner(decorated):
+            @functools.wraps(decorated)
+            def wrapper(*args, **kwargs):
+                return callback(
+                    decorated, args, kwargs, wrapper_args, wrapper_kwargs
+                )
+            return flask_login.login_required(wrapper)
+        return inner
+    return decorator
+
+
+@permissions_wrapper
+def admin_required(args, kwargs):
+    """ The view is only available to global administrators.
+    """
+    return flask_login.current_user.is_admin
+
+
+@permissions_wrapper
+def authentication_required(args, kwargs):
+    """ The view is only available to logged in users.
+    """
+    return True
diff --git a/trurt/service/admin.py b/trurt/service/admin.py
index 5ad5610a..1aca12fc 100644
--- a/trurt/service/admin.py
+++ b/trurt/service/admin.py
@@ -1,24 +1,26 @@
-from trurt import models, utils
+from trurt import models, utils, security
 from trurt.service import blueprint, forms
 from trurt.sso import protocols
 
-import flask_login
 import flask
 import uuid
 
 
 @blueprint.route("/list")
+@security.admin_required()
 def list():
     services = models.Service.query.all()
     return flask.render_template("service_list.html", services=services)
 
 
 @blueprint.route("/create")
+@security.admin_required()
 def create():
     return flask.render_template("service_create.html", protocols=protocols)
 
 
 @blueprint.route("/create/<protocol_name>", methods=["GET", "POST"])
+@security.admin_required()
 def create_protocol(protocol_name):
     protocol = protocols.get(protocol_name, None) or flask.abort(404)
     form = protocol.Config.derive_form(forms.ServiceForm)()
@@ -38,6 +40,7 @@ def create_protocol(protocol_name):
 
 
 @blueprint.route("/details/<service_uuid>")
+@security.admin_required()
 def details(service_uuid):
     service = models.Service.query.get(service_uuid) or flask.abort(404)
     return flask.render_template("service_details.html", service=service)
diff --git a/trurt/sso/saml.py b/trurt/sso/saml.py
index 0cd3abf9..f9191da3 100644
--- a/trurt/sso/saml.py
+++ b/trurt/sso/saml.py
@@ -6,7 +6,7 @@ from saml2 import sigver
 sigver.security_context = security_context
 
 from trurt.sso import blueprint, forms
-from trurt import models, utils, account
+from trurt import models, utils, account, security
 from saml2 import server, saml, config, mdstore, assertion
 from cryptography import x509
 from cryptography.hazmat import primitives, backends
diff --git a/trurt/templates/sidebar.html b/trurt/templates/sidebar.html
index 8118b16d..54eaa135 100644
--- a/trurt/templates/sidebar.html
+++ b/trurt/templates/sidebar.html
@@ -28,12 +28,14 @@
 </li>
 {% endif %}
 
-<li class="header">Management</li>
+{% if current_user.is_admin %}
+<li class="header">Admin</li>
 <li>
   <a href="{{ url_for("service.list") }}">
-    <i class="fa fa-book"></i> <span>Services</span>
+    <i class="fa fa-th-large"></i> <span>Services</span>
   </a>
 </li>
+{% endif %}
 
 
 <li class="header">About</li>
diff --git a/trurt/utils.py b/trurt/utils.py
index 2cb79642..1aa17c3b 100644
--- a/trurt/utils.py
+++ b/trurt/utils.py
@@ -13,6 +13,7 @@ login = flask_login.LoginManager()
 login.login_view = "account.login"
 INTENTS = "intents"
 
+
 @login.unauthorized_handler
 def handle_needs_login():
     return flask.redirect(
-- 
GitLab