From ca0508d96a0a01d16a9e6b5b605cd6c2c7c17e95 Mon Sep 17 00:00:00 2001 From: Tim Lorsbach Date: Wed, 15 Apr 2026 21:14:47 +0200 Subject: [PATCH] More on PES --- bayer/epdb_hooks.py | 15 ++ .../actions/collections/new_pes.html | 9 + .../modals/collections/new_package_modal.html | 6 +- .../modals/collections/new_pes_modal.html | 173 ++++++++++++++++++ bayer/templates/static/login.html | 149 +++++++++++++++ bayer/urls.py | 8 + bayer/views.py | 60 +++++- epauth/views.py | 107 ++++++++--- epdb/legacy_api.py | 42 ++++- epdb/views.py | 3 + templates/components/navbar.html | 6 + 11 files changed, 544 insertions(+), 34 deletions(-) create mode 100644 bayer/epdb_hooks.py create mode 100644 bayer/templates/actions/collections/new_pes.html create mode 100644 bayer/templates/modals/collections/new_pes_modal.html create mode 100644 bayer/templates/static/login.html create mode 100644 bayer/urls.py diff --git a/bayer/epdb_hooks.py b/bayer/epdb_hooks.py new file mode 100644 index 00000000..b843782b --- /dev/null +++ b/bayer/epdb_hooks.py @@ -0,0 +1,15 @@ +import logging + +from epdb.template_registry import register_template + +logger = logging.getLogger(__name__) + +register_template( + "epdb.actions.collections.compound", + "actions/collections/new_pes.html", +) +register_template( + "modals.collections.compound", + "modals/collections/new_pes_modal.html", +) + diff --git a/bayer/templates/actions/collections/new_pes.html b/bayer/templates/actions/collections/new_pes.html new file mode 100644 index 00000000..bdae544f --- /dev/null +++ b/bayer/templates/actions/collections/new_pes.html @@ -0,0 +1,9 @@ +{% if meta.can_edit %} + +{% endif %} \ No newline at end of file diff --git a/bayer/templates/modals/collections/new_package_modal.html b/bayer/templates/modals/collections/new_package_modal.html index 8d7cf5ab..54e03fae 100644 --- a/bayer/templates/modals/collections/new_package_modal.html +++ b/bayer/templates/modals/collections/new_package_modal.html @@ -133,10 +133,8 @@ class="select select-bordered w-full" > - {% for obj in meta.available_groups %} - {% if obj.secret %} - - {% endif %} + {% for obj in meta.secret_groups %} + {% endfor %} diff --git a/bayer/templates/modals/collections/new_pes_modal.html b/bayer/templates/modals/collections/new_pes_modal.html new file mode 100644 index 00000000..2cf11949 --- /dev/null +++ b/bayer/templates/modals/collections/new_pes_modal.html @@ -0,0 +1,173 @@ +{% load static %} + + + + + + + \ No newline at end of file diff --git a/bayer/templates/static/login.html b/bayer/templates/static/login.html new file mode 100644 index 00000000..3f3fdf28 --- /dev/null +++ b/bayer/templates/static/login.html @@ -0,0 +1,149 @@ +{% extends "static/login_base.html" %} + +{% block title %}enviPath - Sign In{% endblock %} + +{% block extra_styles %} + +{% endblock %} + +{% block content %} +
+
+

Welcome to the new enviPath!

+
+
+ +
+
+ + + + + +
+
+ + +
+ +
+ + +
+
+ {% csrf_token %} + + +
+ + +
+ +
+ + +
+ + + + + + +
+
+ +{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/bayer/urls.py b/bayer/urls.py new file mode 100644 index 00000000..ebe5fc0d --- /dev/null +++ b/bayer/urls.py @@ -0,0 +1,8 @@ +from django.urls import re_path + +from . import views as v + + +urlpatterns = [ + re_path(r"^depict_pes$", v.visualize_pes, name="depict_pes"), +] diff --git a/bayer/views.py b/bayer/views.py index 91ea44a2..41657eb1 100644 --- a/bayer/views.py +++ b/bayer/views.py @@ -1,3 +1,59 @@ -from django.shortcuts import render +import requests +from django.conf import settings as s +from django.http import HttpResponse +from pydantic import BaseModel -# Create your views here. +from utilities.chem import FormatConverter + + +class PES(BaseModel): + pass + + +def fetch_pes(request, pes_url) -> dict: + proxies = { + "http": "http://10.185.190.100:8080", + "https": "http://10.185.190.100:8080", + } + + from epauth.views import get_access_token_from_request + token = get_access_token_from_request(request) + + if token: + for k, v in s.PES_API_MAPPING.items(): + if pes_url.startsWith(k): + pes_id = pes_url.split('/')[-1] + headers = {"Authorization": f"Bearer {token['access_token']}"} + params = {"pes_reg_entity_corporate_id": pes_id} + + res = requests.get(v, headers=headers, params=params, proxies=proxies) + + try: + res.raise_for_status() + pes_data = res.json() + + if len(pes_data) == 0: + raise ValueError(f"PES with id {pes_id} not found") + + res_data = pes_data[0] + res_data["pes_url"] = pes_url + return res_data + + except requests.exceptions.HTTPError as e: + raise ValueError(f"Error fetching PES with id {pes_id}: {e}") + else: + raise ValueError(f"Unknown URL {pes_url}") + else: + raise ValueError("Could not fetch access token from request.") + + +def visualize_pes(request): + pes_link = request.GET.get('pesLink') + + if pes_link: + pes_data = fetch_pes(request, pes_link) + print(pes_data) + + return HttpResponse( + FormatConverter.to_png("c1ccccc1"), content_type="image/png" + ) diff --git a/epauth/views.py b/epauth/views.py index b4dbc64b..b419d465 100644 --- a/epauth/views.py +++ b/epauth/views.py @@ -1,12 +1,33 @@ import msal from django.conf import settings as s +from django.contrib.auth import get_user_model from django.contrib.auth import login from django.shortcuts import redirect -from django.contrib.auth import get_user_model from epdb.logic import UserManager, GroupManager from epdb.models import Group + +def get_msal_app_with_cache(request): + """ + Create MSAL app with session-based token cache. + """ + cache = msal.SerializableTokenCache() + + # Load cache from session if it exists + if request.session.get("msal_token_cache"): + cache.deserialize(request.session["msal_token_cache"]) + + msal_app = msal.ConfidentialClientApplication( + client_id=s.MS_ENTRA_CLIENT_ID, + client_credential=s.MS_ENTRA_CLIENT_SECRET, + authority=s.MS_ENTRA_AUTHORITY, + token_cache=cache + ) + + return msal_app, cache + + def entra_login(request): msal_app = msal.ConfidentialClientApplication( client_id=s.MS_ENTRA_CLIENT_ID, @@ -23,11 +44,7 @@ def entra_login(request): def entra_callback(request): - msal_app = msal.ConfidentialClientApplication( - client_id=s.MS_ENTRA_CLIENT_ID, - client_credential=s.MS_ENTRA_CLIENT_SECRET, - authority=s.MS_ENTRA_AUTHORITY, - ) + msal_app, cache = get_msal_app_with_cache(request) flow = request.session.pop("msal_auth_flow", None) if not flow: @@ -35,24 +52,10 @@ def entra_callback(request): # Acquire token using the flow and callback request result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET) - print(result) - # if "error" in result: - # {'correlation_id': '626f511b-5230-4d06-9ffd-d89a764082c6', - # 'error': 'invalid_client', - # 'error_codes': [7000222], - # 'error_description': 'AADSTS7000222: The provided client secret keys for app ' - # "'35c75dfb-bd15-493d-b4e9-af847f2df894' are expired. " - # 'Visit the Azure portal to create new keys for your app: ' - # 'https://aka.ms/NewClientSecret, or consider using ' - # 'certificate credentials for added security: ' - # 'https://aka.ms/certCreds. Trace ID: ' - # '30ba1c58-c949-4432-9ed6-3b6136856700 Correlation ID: ' - # '626f511b-5230-4d06-9ffd-d89a764082c6 Timestamp: ' - # '2026-04-15 08:21:15Z', - # 'error_uri': 'https://login.microsoftonline.com/error?code=7000222', - # 'timestamp': '2026-04-15 08:21:15Z', - # 'trace_id': '30ba1c58-c949-4432-9ed6-3b6136856700'} - # return redirect("/") + + # Save the token cache to session + if cache.has_state_changed: + request.session["msal_token_cache"] = cache.serialize() claims = result["id_token_claims"] @@ -79,7 +82,8 @@ def entra_callback(request): # Ensure groups exists in eP for id, name in s.ENTRA_SECRET_GROUPS.items(): if not Group.objects.filter(uuid=id).exists(): - g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", uuid=id) + g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", + uuid=id) else: g = Group.objects.get(uuid=id) # Ensure its secret @@ -88,7 +92,8 @@ def entra_callback(request): for id, name in s.ENTRA_GROUPS.items(): if not Group.objects.filter(uuid=id).exists(): - g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", uuid=id) + g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", + uuid=id) else: g = Group.objects.get(uuid=id) @@ -100,3 +105,53 @@ def entra_callback(request): # EDIT END return redirect(s.SERVER_URL) # Handle errors + + +def get_access_token_from_request(request, scopes=None): + """ + Get an access token from the request using MSAL token cache. + """ + if scopes is None: + scopes = s.MS_ENTRA_SCOPES + + # Get user from request (must be authenticated) + if not request.user.is_authenticated: + return None + + # Create MSAL app with persistent cache + msal_app, cache = get_msal_app_with_cache(request) + + # Try to get accounts from cache + accounts = msal_app.get_accounts() + + if not accounts: + return None + + # Find the account that matches the current user + user_account = None + for account in accounts: + if account.get("local_account_id") == str(request.user.uuid): + user_account = account + break + + # If no matching account found, use the first available account + if not user_account and accounts: + user_account = accounts[0] + + if not user_account: + return None + + # Try to acquire token silently from cache + result = msal_app.acquire_token_silent( + scopes=scopes, + account=user_account + ) + + # Save cache changes back to session + if cache.has_state_changed: + request.session["msal_token_cache"] = cache.serialize() + + if result and "access_token" in result: + return result + + return None diff --git a/epdb/legacy_api.py b/epdb/legacy_api.py index 079ea3d0..23e1b1f9 100644 --- a/epdb/legacy_api.py +++ b/epdb/legacy_api.py @@ -160,8 +160,46 @@ class SimpleModel(SimpleObject): def login(request, loginusername: Form[str], loginpassword: Form[str]): from django.contrib.auth import authenticate, login - email = User.objects.get(username=loginusername).email - user = authenticate(username=email, password=loginpassword) + if request.headers.get("Authorization"): + import jwt + import requests + + TENANT_ID = s.MS_ENTRA_TENANT_ID + CLIENT_ID = s.MS_ENTRA_CLIENT_ID + + def validate_token(token: str) -> dict: + # Fetch Microsoft's public keys + jwks_uri = f"https://login.microsoftonline.com/{TENANT_ID}/discovery/v2.0/keys" + jwks = requests.get(jwks_uri).json() + + header = jwt.get_unverified_header(token) + + public_key = jwt.algorithms.RSAAlgorithm.from_jwk( + next(k for k in jwks["keys"] if k["kid"] == header["kid"]) + ) + + claims = jwt.decode( + token, + public_key, + algorithms=["RS256"], + audience=[CLIENT_ID, f"api://{CLIENT_ID}"], + issuer=f"https://sts.windows.net/{TENANT_ID}/", + ) + return claims + + token = request.headers.get("Authorization").split(" ")[1] + + claims = validate_token(token) + + if not User.objects.filter(uuid=claims['oid']).exists(): + user = None + else: + user = User.objects.get(uuid=claims['oid']) + + else: + email = User.objects.get(username=loginusername).email + user = authenticate(username=email, password=loginpassword) + if user: login(request, user) return user diff --git a/epdb/views.py b/epdb/views.py index 26d3a4f1..ad643475 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -388,6 +388,9 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]: "debug": s.DEBUG, "external_databases": ExternalDatabase.get_databases(), "site_id": s.MATOMO_SITE_ID, + # EDIT START + "secret_groups": Group.objects.filter(secret=True), + # EDIT END }, } diff --git a/templates/components/navbar.html b/templates/components/navbar.html index 12f57597..2960c314 100644 --- a/templates/components/navbar.html +++ b/templates/components/navbar.html @@ -88,6 +88,12 @@ >Scenario +
+
  • + Group +