From ec40299f695c954e4edb0e5806ddbed40841b4e4 Mon Sep 17 00:00:00 2001 From: Arie Peterson <arie@greenhost.nl> Date: Fri, 9 Dec 2022 11:22:27 +0100 Subject: [PATCH] Only list apps that user has access to --- backend/app.py | 19 +++++++++ backend/areas/apps/apps.py | 2 +- backend/areas/apps/apps_service.py | 26 ++++++++++++ backend/areas/roles/models.py | 1 + backend/areas/roles/role_service.py | 2 +- backend/helpers/access_control.py | 43 ++++++++++++++++++++ backend/helpers/kratos_user.py | 2 - backend/web/login/login.py | 5 ++- backend/web/templates/settings.html | 2 +- frontend/src/modules/login/LoginCallback.tsx | 2 +- 10 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 backend/helpers/access_control.py diff --git a/backend/app.py b/backend/app.py index 97f6ffbb..9d5aec62 100644 --- a/backend/app.py +++ b/backend/app.py @@ -35,6 +35,24 @@ from helpers import ( from config import * import logging +# Configure logging. +from logging.config import dictConfig +dictConfig({ + 'version': 1, + 'formatters': {'default': { + 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', + }}, + 'handlers': {'wsgi': { + 'class': 'logging.StreamHandler', + 'stream': 'ext://flask.logging.wsgi_errors_stream', + 'formatter': 'default', + }}, + 'root': { + 'level': 'INFO', + 'handlers': ['wsgi'], + } +}) + app = Flask(__name__) app.config["SECRET_KEY"] = SECRET_KEY @@ -47,6 +65,7 @@ db.init_app(app) app.logger.setLevel(logging.INFO) +app.logger.info("Starting dashboard backend.") app.register_blueprint(api_v1) app.register_blueprint(web) diff --git a/backend/areas/apps/apps.py b/backend/areas/apps/apps.py index b6774118..1bd55644 100644 --- a/backend/areas/apps/apps.py +++ b/backend/areas/apps/apps.py @@ -23,7 +23,7 @@ CONFIG_DATA = [ @cross_origin() def get_apps(): """Return data about all apps""" - apps = AppsService.get_all_apps() + apps = AppsService.get_accessible_apps() return jsonify(apps) diff --git a/backend/areas/apps/apps_service.py b/backend/areas/apps/apps_service.py index 665b4fed..289529de 100644 --- a/backend/areas/apps/apps_service.py +++ b/backend/areas/apps/apps_service.py @@ -1,4 +1,12 @@ +from flask import current_app +from flask_jwt_extended import get_jwt +import ory_kratos_client +from ory_kratos_client.api import v0alpha2_api as kratos_api + from .models import App, AppRole +from config import * +from helpers.access_control import user_has_access +from helpers.kratos_user import KratosUser class AppsService: @staticmethod @@ -6,6 +14,24 @@ class AppsService: apps = App.query.all() return [app.to_dict() for app in apps] + @staticmethod + def get_accessible_apps(): + apps = App.query.all() + + kratos_admin_api_configuration = ory_kratos_client.Configuration(host=KRATOS_ADMIN_URL, discard_unknown_keys=True) + KRATOS_ADMIN = kratos_api.V0alpha2Api(ory_kratos_client.ApiClient(kratos_admin_api_configuration)) + + user_id = get_jwt()['user_id'] + current_app.logger.info(f"user_id: {user_id}") + # Get the related user object + current_app.logger.info(f"Info: Getting user from admin {user_id}") + user = KratosUser(KRATOS_ADMIN, user_id) + if not user: + current_app.logger.error(f"User not found in database: {user_id}") + return [] + + return [app.to_dict() for app in apps if user_has_access(user, app)] + @staticmethod def get_app(slug): app = App.query.filter_by(slug=slug).first() diff --git a/backend/areas/roles/models.py b/backend/areas/roles/models.py index d822901c..b761bb57 100644 --- a/backend/areas/roles/models.py +++ b/backend/areas/roles/models.py @@ -3,6 +3,7 @@ from database import db class Role(db.Model): + ADMIN_ROLE_ID = 1 NO_ACCESS_ROLE_ID = 3 id = db.Column(Integer, primary_key=True) diff --git a/backend/areas/roles/role_service.py b/backend/areas/roles/role_service.py index 90ad064f..3520b273 100644 --- a/backend/areas/roles/role_service.py +++ b/backend/areas/roles/role_service.py @@ -15,4 +15,4 @@ class RoleService: @staticmethod def is_user_admin(userId): dashboard_role_id = AppRole.query.filter_by(user_id=userId, app_id=1).first().role_id - return dashboard_role_id == 1 \ No newline at end of file + return dashboard_role_id == 1 diff --git a/backend/helpers/access_control.py b/backend/helpers/access_control.py new file mode 100644 index 00000000..347230d5 --- /dev/null +++ b/backend/helpers/access_control.py @@ -0,0 +1,43 @@ +from flask import current_app + +from areas.apps.models import App, AppRole +from areas.roles.models import Role +from config import * +from database import db + +def user_has_access(user, app): + # Get role on dashboard + dashboard_app = db.session.query(App).filter( + App.slug == 'dashboard').first() + if not dashboard_app: + current_app.logger.error("Dashboard app not found in database.") + return False + role_object = ( + db.session.query(AppRole) + .filter(AppRole.app_id == dashboard_app.id) + .filter(AppRole.user_id == user.uuid) + .first() + ) + if role_object is None: + current_app.logger.info(f"No dashboard role set for user {user.uuid}.") + return False + + # If the user is dashboard admin, they have access to everything. + if role_object.role_id == Role.ADMIN_ROLE_ID: + current_app.logger.info(f"User {user.uuid} has admin dashboard role") + return True + + # Get role for app. + role_object = ( + db.session.query(AppRole) + .filter(AppRole.app_id == app.id) + .filter(AppRole.user_id == user.uuid) + .first() + ) + # Role ID 3 is always "No access" due to migration b514cca2d47b + if role_object is None or role_object.role_id is None or role_object.role_id == Role.NO_ACCESS_ROLE_ID: + current_app.logger.info(f"User {user.uuid} has no access for: {app.name}") + return False + + # In all other cases, access is granted. + return True diff --git a/backend/helpers/kratos_user.py b/backend/helpers/kratos_user.py index 523f67b0..dee31b4c 100644 --- a/backend/helpers/kratos_user.py +++ b/backend/helpers/kratos_user.py @@ -9,8 +9,6 @@ import urllib.request from typing import Dict from urllib.request import Request -# Some imports commented out to satisfy pylint. They will be used once more -# functions are migrated to this model from ory_kratos_client.model.admin_create_identity_body import AdminCreateIdentityBody from ory_kratos_client.model.admin_create_self_service_recovery_link_body \ import AdminCreateSelfServiceRecoveryLinkBody diff --git a/backend/web/login/login.py b/backend/web/login/login.py index acd61288..94ee4c37 100644 --- a/backend/web/login/login.py +++ b/backend/web/login/login.py @@ -289,8 +289,9 @@ def consent(): ) # Resolve to which app the client_id belongs. - app_obj = db.session.query(OAuthClientApp).filter(OAuthClientApp.oauthclient_id == client_id).first().app - if not app_obj: + try: + app_obj = db.session.query(OAuthClientApp).filter(OAuthClientApp.oauthclient_id == client_id).first().app + except AttributeError: current_app.logger.error(f"Could not find app for client {client_id}") return redirect( consent_request.reject( diff --git a/backend/web/templates/settings.html b/backend/web/templates/settings.html index 60ad5953..8cb290af 100644 --- a/backend/web/templates/settings.html +++ b/backend/web/templates/settings.html @@ -19,7 +19,7 @@ <div id="contentMessages"></div> <div id="contentProfileSaved" class='alert alert-success' - style='display:none'>Successfuly saved new settings.</div> + style='display:none'>Successfully saved new settings.</div> <div id="contentProfileSaveFailed" class='alert alert-danger' style='display:none'>Your changes are not saved. Please check the fields for errors.</div> diff --git a/frontend/src/modules/login/LoginCallback.tsx b/frontend/src/modules/login/LoginCallback.tsx index aa3d9920..54a55edf 100644 --- a/frontend/src/modules/login/LoginCallback.tsx +++ b/frontend/src/modules/login/LoginCallback.tsx @@ -50,7 +50,7 @@ export function LoginCallback() { /> </svg> </div> - <p className="text-lg text-primary-600 mt-2">Logging You in, just a moment.</p> + <p className="text-lg text-primary-600 mt-2">Logging you in, just a moment.</p> </div> </div> </div> -- GitLab