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