diff --git a/backend/app.py b/backend/app.py index 97f6ffbbe18701768f8e96a8958883d047587c0d..9d5aec625a14e7cf570894c18eb0d5a2d665b205 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 b677411827ee220f198c2b66fe3c0733ff7463f5..1bd55644f0a7b985062e8897423ed430318b247c 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 665b4fed6fdae2a98b48b871344135767bc240bb..289529de75435c538e4796a36945a9ddaeab9d83 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 d822901c933568dd62e575675e0afa68565aa29f..b761bb579ebc9133b86c8822c724f1eda2112a97 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 90ad064f40f5149054bfd5287ef918a2d719911a..3520b273e75026410d095ccbf7df26a6b097728b 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 0000000000000000000000000000000000000000..347230d5904f633364eb76621f672847fc3362d4 --- /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 523f67b0f6736a274c2dfae910b0ff7a4eea7471..dee31b4c229a513154c55eb12d72c0c1d7a65766 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 acd61288c56146acb907648c3583949ee6ae991d..94ee4c37036fce38e47dcfe7c70e8ba87f4aac2d 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 60ad59534bd36e20ca2579a60e6cf0550b1ccda4..8cb290afa9f0d5e09b34177b1e0ec74529e2b2cd 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 aa3d992094b2261ceff91c8854f2639e1839d4e7..54a55edfb7132a5825e9949bd6b89c88901bce0f 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>