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