From 67100144e10e89a5dd814249588d46a79fbc3d78 Mon Sep 17 00:00:00 2001 From: Dawn Date: Tue, 2 Jun 2026 16:24:42 +0200 Subject: [PATCH 01/28] add Kerberos authentication support with proxy headers --- pyproject.toml | 3 ++- src/aare/daq/auth.py | 35 ++++++++++++++++++++++++++++++----- src/aare/daq/server.py | 17 +++++++++++++---- src/aare/gui/auth.py | 2 ++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 343c86b6..e6219caf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "opencv-python-headless==4.11.0.86", "PySide6==6.9.0", "requests==2.32.4", + "requests-gssapi>=1.3.0", "pyepics==3.5.8", "redis==6.2.0", "python-redis-lock==4.0.0", @@ -33,7 +34,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "pytest==9.0.3", +# "pytest==9.0.3", "pytest-cov==7.1.0", "pytest-mock==3.14.0", "pytest-qt==4.4.0", diff --git a/src/aare/daq/auth.py b/src/aare/daq/auth.py index 33ed853d..a8e36b92 100644 --- a/src/aare/daq/auth.py +++ b/src/aare/daq/auth.py @@ -7,7 +7,7 @@ from typing import List import time import jwt -from fastapi import Depends, HTTPException, status +from fastapi import Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer from pydantic import BaseModel @@ -46,15 +46,40 @@ def create_access_token(token: TokenData): return encoded_jwt -def authenticate_user(cfg: BeamlineConfig, form_data: OAuth2PasswordRequestForm) -> str: - user_info = pwd.getpwnam(form_data.username) +def authenticate_from_proxy_header(request: Request) -> str: + """ + Extract the pre-authenticated username from the trusted Apache proxy header. + Only trust this header when the request originates from localhost (127.0.0.1 / ::1), + meaning only the Apache proxy running on the same host can supply it. + """ + client_host = request.client.host if request.client else None + if client_host not in ("127.0.0.1", "::1"): + raise AuthenticationException( + message="X-Remote-User header is only trusted from the localhost proxy", + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + code=AuthErrorCode.INVALID_TOKEN, + ) + remote_user = request.headers.get("X-Remote-User") + if not remote_user: + raise AuthenticationException( + message="Missing X-Remote-User header from proxy", + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + code=AuthErrorCode.INVALID_TOKEN, + ) + return remote_user + + +def authenticate_user(cfg: BeamlineConfig, username: str) -> str: + user_info = pwd.getpwnam(username) groups = os.getgrouplist(user_info.pw_name, user_info.pw_gid) supplementary_groups = [grp.getgrgid(g).gr_name.lower() for g in groups] - super_user = form_data.username in SUPER_USERS + super_user = username in SUPER_USERS pgroups = [group for group in supplementary_groups if group.startswith('p')] staff = "unx-mxgroup" in supplementary_groups or "unx-sls_mx" in supplementary_groups or super_user - token = TokenData(sub=form_data.username, + token = TokenData(sub=username, pgroups=pgroups, staff=staff, session=cfg.generate_session()) diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index 01d58e79..3d9c1d63 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -24,7 +24,7 @@ from aare.common.automation_models import AutomationProgress from aare.common.raster_grid import RasterGridRequest, CompletedRasterGrid from aare.common.rotation_scan import RotationScanRequest, CompletedRotationScan from aare.common.sample_geometry import SampleGeometryModel -from fastapi import FastAPI, Depends +from fastapi import FastAPI, Depends, Request from fastapi.concurrency import run_in_threadpool from fastapi import HTTPException from fastapi import status as api_status @@ -256,17 +256,26 @@ async def automation_progress_event_stream() -> AsyncGenerator[str, None]: return @app.post("/token") -async def login(form_data: OAuth2PasswordRequestForm = Depends()): +async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()): """ Authenticate a user and return an access token. + When the request carries an X-Remote-User header (set by the Apache Kerberos + proxy), the username is taken from that header and the proxy origin is verified. + Otherwise the username from the form data is used (local / dev access). + Args: + request: The incoming HTTP request (used to inspect headers and client IP). form_data: OAuth2 password request form containing username and password. Returns: A dictionary containing the access token and token type. """ - data = await run_in_threadpool(auth.authenticate_user, cfg, form_data) + if request.headers.get("X-Remote-User"): + username = auth.authenticate_from_proxy_header(request) + else: + username = form_data.username + data = await run_in_threadpool(auth.authenticate_user, cfg, username) return {"access_token": data, "token_type": "bearer"} @app.get("/meta/error-codes") @@ -2253,7 +2262,7 @@ def main(): urllib3.disable_warnings() # Run the application using uvicorn - uvicorn.run("aare.daq.server:app", host="0.0.0.0", port=5210, workers=2, log_config=get_uvicorn_logging_config()) + uvicorn.run("aare.daq.server:app", host="127.0.0.1", port=5210, workers=2, log_config=get_uvicorn_logging_config()) if __name__ == "__main__": diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 2cabc77a..fa25edbe 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -1,6 +1,7 @@ import os import jwt import requests +from requests_gssapi import HTTPSPNEGOAuth from aare.common.models import TokenData from aare.common.auth_models import get_user @@ -28,6 +29,7 @@ def auth(base_url: str | None) -> str: headers={ "Content-Type": "application/x-www-form-urlencoded" }, + auth=HTTPSPNEGOAuth(), # Kerberos SPNEGO negotiation via user's TGT timeout=(3.0, 15.0), # (connect timeout, read timeout) ) except requests.RequestException as e: From de0781053a0fe2ac19a101bcd0fbb7f3da397e90 Mon Sep 17 00:00:00 2001 From: Dawn Date: Tue, 2 Jun 2026 16:24:42 +0200 Subject: [PATCH 02/28] add Kerberos authentication support with proxy headers --- pyproject.toml | 3 ++- src/aare/daq/auth.py | 35 ++++++++++++++++++++++++++++++----- src/aare/daq/server.py | 17 +++++++++++++---- src/aare/gui/auth.py | 2 ++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f29dcdba..d495f04a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "opencv-python-headless==4.11.0.86", "PySide6==6.9.0", "requests==2.32.4", + "requests-gssapi>=1.3.0", "pyepics==3.5.8", "redis==6.2.0", "python-redis-lock==4.0.0", @@ -33,7 +34,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "pytest==9.0.3", +# "pytest==9.0.3", "pytest-cov==7.1.0", "pytest-mock==3.14.0", "pytest-qt==4.4.0", diff --git a/src/aare/daq/auth.py b/src/aare/daq/auth.py index 33ed853d..a8e36b92 100644 --- a/src/aare/daq/auth.py +++ b/src/aare/daq/auth.py @@ -7,7 +7,7 @@ from typing import List import time import jwt -from fastapi import Depends, HTTPException, status +from fastapi import Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer from pydantic import BaseModel @@ -46,15 +46,40 @@ def create_access_token(token: TokenData): return encoded_jwt -def authenticate_user(cfg: BeamlineConfig, form_data: OAuth2PasswordRequestForm) -> str: - user_info = pwd.getpwnam(form_data.username) +def authenticate_from_proxy_header(request: Request) -> str: + """ + Extract the pre-authenticated username from the trusted Apache proxy header. + Only trust this header when the request originates from localhost (127.0.0.1 / ::1), + meaning only the Apache proxy running on the same host can supply it. + """ + client_host = request.client.host if request.client else None + if client_host not in ("127.0.0.1", "::1"): + raise AuthenticationException( + message="X-Remote-User header is only trusted from the localhost proxy", + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + code=AuthErrorCode.INVALID_TOKEN, + ) + remote_user = request.headers.get("X-Remote-User") + if not remote_user: + raise AuthenticationException( + message="Missing X-Remote-User header from proxy", + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + code=AuthErrorCode.INVALID_TOKEN, + ) + return remote_user + + +def authenticate_user(cfg: BeamlineConfig, username: str) -> str: + user_info = pwd.getpwnam(username) groups = os.getgrouplist(user_info.pw_name, user_info.pw_gid) supplementary_groups = [grp.getgrgid(g).gr_name.lower() for g in groups] - super_user = form_data.username in SUPER_USERS + super_user = username in SUPER_USERS pgroups = [group for group in supplementary_groups if group.startswith('p')] staff = "unx-mxgroup" in supplementary_groups or "unx-sls_mx" in supplementary_groups or super_user - token = TokenData(sub=form_data.username, + token = TokenData(sub=username, pgroups=pgroups, staff=staff, session=cfg.generate_session()) diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index 62c817b4..064b3cf8 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -24,7 +24,7 @@ from aare.common.automation_models import AutomationProgress from aare.common.raster_grid import RasterGridRequest, CompletedRasterGrid from aare.common.rotation_scan import RotationScanRequest, CompletedRotationScan from aare.common.sample_geometry import SampleGeometryModel -from fastapi import FastAPI, Depends +from fastapi import FastAPI, Depends, Request from fastapi.concurrency import run_in_threadpool from fastapi import HTTPException from fastapi import status as api_status @@ -256,17 +256,26 @@ async def automation_progress_event_stream() -> AsyncGenerator[str, None]: return @app.post("/token") -async def login(form_data: OAuth2PasswordRequestForm = Depends()): +async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()): """ Authenticate a user and return an access token. + When the request carries an X-Remote-User header (set by the Apache Kerberos + proxy), the username is taken from that header and the proxy origin is verified. + Otherwise the username from the form data is used (local / dev access). + Args: + request: The incoming HTTP request (used to inspect headers and client IP). form_data: OAuth2 password request form containing username and password. Returns: A dictionary containing the access token and token type. """ - data = await run_in_threadpool(auth.authenticate_user, cfg, form_data) + if request.headers.get("X-Remote-User"): + username = auth.authenticate_from_proxy_header(request) + else: + username = form_data.username + data = await run_in_threadpool(auth.authenticate_user, cfg, username) return {"access_token": data, "token_type": "bearer"} @app.get("/meta/error-codes") @@ -2377,7 +2386,7 @@ def main(): urllib3.disable_warnings() # Run the application using uvicorn - uvicorn.run("aare.daq.server:app", host="0.0.0.0", port=5210, workers=2, log_config=get_uvicorn_logging_config()) + uvicorn.run("aare.daq.server:app", host="127.0.0.1", port=5210, workers=2, log_config=get_uvicorn_logging_config()) if __name__ == "__main__": diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 2cabc77a..fa25edbe 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -1,6 +1,7 @@ import os import jwt import requests +from requests_gssapi import HTTPSPNEGOAuth from aare.common.models import TokenData from aare.common.auth_models import get_user @@ -28,6 +29,7 @@ def auth(base_url: str | None) -> str: headers={ "Content-Type": "application/x-www-form-urlencoded" }, + auth=HTTPSPNEGOAuth(), # Kerberos SPNEGO negotiation via user's TGT timeout=(3.0, 15.0), # (connect timeout, read timeout) ) except requests.RequestException as e: From 6bed4102e7bbc21d1bff5acdcaca8cf03742d45a Mon Sep 17 00:00:00 2001 From: Dawn Date: Tue, 9 Jun 2026 15:44:42 +0200 Subject: [PATCH 03/28] remove dependency on gssapi --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d495f04a..78bfe0ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "opencv-python-headless==4.11.0.86", "PySide6==6.9.0", "requests==2.32.4", - "requests-gssapi>=1.3.0", "pyepics==3.5.8", "redis==6.2.0", "python-redis-lock==4.0.0", From 8dc1ecacb2f71d16f7761f5d17871b3687546b3f Mon Sep 17 00:00:00 2001 From: Dawn Date: Tue, 9 Jun 2026 15:45:33 +0200 Subject: [PATCH 04/28] remove dependency on gssapi directly using curl to negotiate with https from GUI --- src/aare/gui/auth.py | 56 ++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index fa25edbe..f87e0709 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -1,7 +1,6 @@ -import os +import json +import subprocess import jwt -import requests -from requests_gssapi import HTTPSPNEGOAuth from aare.common.models import TokenData from aare.common.auth_models import get_user @@ -19,38 +18,45 @@ def auth(base_url: str | None) -> str: return jwt.encode(token_data.model_dump(), "ABC123") url = f"{base_url}/token" + apache_url = f"https://mx-x10sa-queue-01.psi.ch/" try: - response = requests.post( - url, - data={ - "username": curr_user, - "password": "" - }, - headers={ - "Content-Type": "application/x-www-form-urlencoded" - }, - auth=HTTPSPNEGOAuth(), # Kerberos SPNEGO negotiation via user's TGT - timeout=(3.0, 15.0), # (connect timeout, read timeout) + result = subprocess.run( + [ + 'curl', '-sk', '--cacert', '/my/top/secret/path', + '--negotiate', '-u', ':', + apache_url, + ], + capture_output=True, + text=True, + timeout=18.0, ) - except requests.RequestException as e: - logger.error(f"Authentication request failed (network): {e}") + except subprocess.TimeoutExpired as e: + logger.error(f"Authentication curl timed out: {e}") raise RuntimeError( - "Cannot reach AareDAQ server (network error). " + "Cannot reach AareDAQ server (timeout). " + "Please check the server is running and your connection." + ) from e + except FileNotFoundError as e: + logger.error(f"curl not found: {e}") + raise RuntimeError("curl not found on this system.") from e + except OSError as e: + logger.error(f"Authentication curl failed (OS error): {e}") + raise RuntimeError( + "Cannot reach AareDAQ server (OS error). " "Please check the server is running and your connection." ) from e - if response.status_code != 200: - # Avoid dumping full HTML/tracebacks into the GUI; keep it short and actionable - logger.error(f"Authentication request failed: HTTP {response.status_code}. Body: {response.text[:500]}") + if result.returncode != 0: + logger.error(f"Authentication curl exited {result.returncode}. stderr: {result.stderr[:500]}") raise RuntimeError( - f"Authentication failed (HTTP {response.status_code}). " - "The server may be starting up or unavailable." + f"Authentication failed (curl exit {result.returncode}). " + "Check server is running and Kerberos ticket is valid (kinit)." ) try: - response_json = response.json() - except ValueError as e: - logger.error(f"Authentication response was not JSON. Body: {response.text[:500]}") + response_json = json.loads(result.stdout) + except json.JSONDecodeError as e: + logger.error(f"Authentication response not JSON. stdout: {result.stdout[:500]}") raise RuntimeError( "Authentication failed (invalid server response). " "The server may be starting up or misconfigured." From 6351265698b1494416319e0bf5662171eed3bf89 Mon Sep 17 00:00:00 2001 From: Dawn Date: Tue, 9 Jun 2026 16:16:11 +0200 Subject: [PATCH 05/28] add Apache authentication and token handling using curl in GUI and access log parsing in DAQ --- src/aare/daq/auth.py | 34 ++++++++++++++++++ src/aare/daq/server.py | 2 +- src/aare/gui/auth.py | 81 +++++++++++++++++++++++++++++++----------- 3 files changed, 96 insertions(+), 21 deletions(-) diff --git a/src/aare/daq/auth.py b/src/aare/daq/auth.py index a8e36b92..e0495098 100644 --- a/src/aare/daq/auth.py +++ b/src/aare/daq/auth.py @@ -1,6 +1,7 @@ import grp import os import pwd +import re import uuid from datetime import datetime, timedelta, UTC from typing import List @@ -30,6 +31,10 @@ BATON_REQUEST_TIMEOUT_SECONDS = 30 STAFF_GROUP = "unx-MXgroup" SUPER_USERS = ["e10019", "e11206", "e18147"] +APACHE_ACCESS_LOG = "/var/log/httpd/daq-access.log" +# Common Log Format: IP - username [timestamp] "request" status bytes ... +_LOG_PATTERN = re.compile(r'^\S+ \S+ (\S+) \[.*?\] ".*?" (\d+)') + oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") class TokenData(BaseModel): @@ -46,6 +51,35 @@ def create_access_token(token: TokenData): return encoded_jwt +def authenticate_from_apache_log() -> str: + """ + Read the Apache access log and return the username from the most recent + 200 response. Apache and the DAQ server are co-located on the same machine. + """ + try: + with open(APACHE_ACCESS_LOG, 'r') as f: + lines = f.readlines() + except OSError as e: + raise AuthenticationException( + message=f"Cannot read Apache access log: {e}", + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + code=AuthErrorCode.INVALID_TOKEN, + ) from e + + for line in reversed(lines): + m = _LOG_PATTERN.match(line) + if m and m.group(2) == '200' and m.group(1) != '-': + return m.group(1) + + raise AuthenticationException( + message="No authenticated user found in Apache access log", + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + code=AuthErrorCode.INVALID_TOKEN, + ) + + def authenticate_from_proxy_header(request: Request) -> str: """ Extract the pre-authenticated username from the trusted Apache proxy header. diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index 064b3cf8..a7877315 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -274,7 +274,7 @@ async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends if request.headers.get("X-Remote-User"): username = auth.authenticate_from_proxy_header(request) else: - username = form_data.username + username = auth.authenticate_from_apache_log() data = await run_in_threadpool(auth.authenticate_user, cfg, username) return {"access_token": data, "token_type": "bearer"} diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index f87e0709..a2f9c39b 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -8,6 +8,9 @@ from aare.common.logger_config import setup_logger logger = setup_logger('aareGUI') +APACHE_URL = "https://mx-x10sa-queue-01.psi.ch/" +CACERT = "/sls/x10sa/misc/.cert/10s.crt" + def auth(base_url: str | None) -> str: curr_user = get_user() if base_url is None: @@ -17,46 +20,84 @@ def auth(base_url: str | None) -> str: pgroups=["p16371", "p22233"]) return jwt.encode(token_data.model_dump(), "ABC123") - url = f"{base_url}/token" - apache_url = f"https://mx-x10sa-queue-01.psi.ch/" + # Step 1: SPNEGO against Apache, dump response headers, discard body try: - result = subprocess.run( - [ - 'curl', '-sk', '--cacert', '/my/top/secret/path', - '--negotiate', '-u', ':', - apache_url, - ], + apache_result = subprocess.run( + ['curl', '-sk', '--cacert', CACERT, + '--negotiate', '-u', ':', + '-D', '-', '-o', '/dev/null', + APACHE_URL], capture_output=True, text=True, timeout=18.0, ) except subprocess.TimeoutExpired as e: - logger.error(f"Authentication curl timed out: {e}") + logger.error(f"Apache auth curl timed out: {e}") raise RuntimeError( - "Cannot reach AareDAQ server (timeout). " + "Cannot reach Apache server (timeout). " "Please check the server is running and your connection." ) from e except FileNotFoundError as e: logger.error(f"curl not found: {e}") raise RuntimeError("curl not found on this system.") from e except OSError as e: - logger.error(f"Authentication curl failed (OS error): {e}") + logger.error(f"Apache auth curl OS error: {e}") + raise RuntimeError( + "Cannot reach Apache server (OS error). " + "Please check the server is running and your connection." + ) from e + + if apache_result.returncode != 0: + logger.error(f"Apache curl exited {apache_result.returncode}. stderr: {apache_result.stderr[:500]}") + raise RuntimeError( + f"Apache authentication failed (curl exit {apache_result.returncode}). " + "Check Kerberos ticket is valid (kinit)." + ) + + # Parse X-Remote-User from response headers (stdout = headers due to -D -) + remote_user = None + for line in apache_result.stdout.splitlines(): + if line.lower().startswith('x-remote-user:'): + remote_user = line.split(':', 1)[1].strip() + break + + if not remote_user: + logger.error(f"X-Remote-User not found in Apache response headers. Headers: {apache_result.stdout[:500]}") + raise RuntimeError("Apache authentication failed (no X-Remote-User in response).") + + # Step 2: Call FastAPI /token with the authenticated username + url = f"{base_url}/token" + try: + token_result = subprocess.run( + ['curl', '-sk', '--cacert', CACERT, + '-X', 'POST', url, + '-d', f'username={remote_user}&password=', + '-H', 'Content-Type: application/x-www-form-urlencoded'], + capture_output=True, + text=True, + timeout=18.0, + ) + except subprocess.TimeoutExpired as e: + logger.error(f"Token request curl timed out: {e}") + raise RuntimeError( + "Cannot reach AareDAQ server (timeout). " + "Please check the server is running and your connection." + ) from e + except OSError as e: + logger.error(f"Token request curl OS error: {e}") raise RuntimeError( "Cannot reach AareDAQ server (OS error). " "Please check the server is running and your connection." ) from e - if result.returncode != 0: - logger.error(f"Authentication curl exited {result.returncode}. stderr: {result.stderr[:500]}") - raise RuntimeError( - f"Authentication failed (curl exit {result.returncode}). " - "Check server is running and Kerberos ticket is valid (kinit)." - ) + if token_result.returncode != 0: + logger.error(f"Token curl exited {token_result.returncode}. stderr: {token_result.stderr[:500]}") + raise RuntimeError(f"Token request failed (curl exit {token_result.returncode}).") try: - response_json = json.loads(result.stdout) + response_json = json.loads(token_result.stdout) except json.JSONDecodeError as e: - logger.error(f"Authentication response not JSON. stdout: {result.stdout[:500]}") + logger.error(f"Token response not JSON. stdout: {token_result.stdout[:500]}") raise RuntimeError( "Authentication failed (invalid server response). " "The server may be starting up or misconfigured." @@ -64,7 +105,7 @@ def auth(base_url: str | None) -> str: token = response_json.get("access_token") if not token or not isinstance(token, str): - logger.error(f"Authentication response missing access_token. Keys: {list(response_json.keys())}") + logger.error(f"Missing access_token. Keys: {list(response_json.keys())}") raise RuntimeError( "Authentication failed (missing token in server response). " "The server may be starting up." From f0ecba9a8a9bd6b01db47e9c45b39fd99c463b92 Mon Sep 17 00:00:00 2001 From: Dawn Date: Tue, 2 Jun 2026 16:24:42 +0200 Subject: [PATCH 06/28] add Kerberos authentication support with proxy headers --- pyproject.toml | 3 ++- src/aare/daq/auth.py | 35 ++++++++++++++++++++++++++++++----- src/aare/daq/server.py | 17 +++++++++++++---- src/aare/gui/auth.py | 2 ++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f29dcdba..d495f04a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "opencv-python-headless==4.11.0.86", "PySide6==6.9.0", "requests==2.32.4", + "requests-gssapi>=1.3.0", "pyepics==3.5.8", "redis==6.2.0", "python-redis-lock==4.0.0", @@ -33,7 +34,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "pytest==9.0.3", +# "pytest==9.0.3", "pytest-cov==7.1.0", "pytest-mock==3.14.0", "pytest-qt==4.4.0", diff --git a/src/aare/daq/auth.py b/src/aare/daq/auth.py index 33ed853d..a8e36b92 100644 --- a/src/aare/daq/auth.py +++ b/src/aare/daq/auth.py @@ -7,7 +7,7 @@ from typing import List import time import jwt -from fastapi import Depends, HTTPException, status +from fastapi import Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer from pydantic import BaseModel @@ -46,15 +46,40 @@ def create_access_token(token: TokenData): return encoded_jwt -def authenticate_user(cfg: BeamlineConfig, form_data: OAuth2PasswordRequestForm) -> str: - user_info = pwd.getpwnam(form_data.username) +def authenticate_from_proxy_header(request: Request) -> str: + """ + Extract the pre-authenticated username from the trusted Apache proxy header. + Only trust this header when the request originates from localhost (127.0.0.1 / ::1), + meaning only the Apache proxy running on the same host can supply it. + """ + client_host = request.client.host if request.client else None + if client_host not in ("127.0.0.1", "::1"): + raise AuthenticationException( + message="X-Remote-User header is only trusted from the localhost proxy", + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + code=AuthErrorCode.INVALID_TOKEN, + ) + remote_user = request.headers.get("X-Remote-User") + if not remote_user: + raise AuthenticationException( + message="Missing X-Remote-User header from proxy", + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + code=AuthErrorCode.INVALID_TOKEN, + ) + return remote_user + + +def authenticate_user(cfg: BeamlineConfig, username: str) -> str: + user_info = pwd.getpwnam(username) groups = os.getgrouplist(user_info.pw_name, user_info.pw_gid) supplementary_groups = [grp.getgrgid(g).gr_name.lower() for g in groups] - super_user = form_data.username in SUPER_USERS + super_user = username in SUPER_USERS pgroups = [group for group in supplementary_groups if group.startswith('p')] staff = "unx-mxgroup" in supplementary_groups or "unx-sls_mx" in supplementary_groups or super_user - token = TokenData(sub=form_data.username, + token = TokenData(sub=username, pgroups=pgroups, staff=staff, session=cfg.generate_session()) diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index 62c817b4..064b3cf8 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -24,7 +24,7 @@ from aare.common.automation_models import AutomationProgress from aare.common.raster_grid import RasterGridRequest, CompletedRasterGrid from aare.common.rotation_scan import RotationScanRequest, CompletedRotationScan from aare.common.sample_geometry import SampleGeometryModel -from fastapi import FastAPI, Depends +from fastapi import FastAPI, Depends, Request from fastapi.concurrency import run_in_threadpool from fastapi import HTTPException from fastapi import status as api_status @@ -256,17 +256,26 @@ async def automation_progress_event_stream() -> AsyncGenerator[str, None]: return @app.post("/token") -async def login(form_data: OAuth2PasswordRequestForm = Depends()): +async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()): """ Authenticate a user and return an access token. + When the request carries an X-Remote-User header (set by the Apache Kerberos + proxy), the username is taken from that header and the proxy origin is verified. + Otherwise the username from the form data is used (local / dev access). + Args: + request: The incoming HTTP request (used to inspect headers and client IP). form_data: OAuth2 password request form containing username and password. Returns: A dictionary containing the access token and token type. """ - data = await run_in_threadpool(auth.authenticate_user, cfg, form_data) + if request.headers.get("X-Remote-User"): + username = auth.authenticate_from_proxy_header(request) + else: + username = form_data.username + data = await run_in_threadpool(auth.authenticate_user, cfg, username) return {"access_token": data, "token_type": "bearer"} @app.get("/meta/error-codes") @@ -2377,7 +2386,7 @@ def main(): urllib3.disable_warnings() # Run the application using uvicorn - uvicorn.run("aare.daq.server:app", host="0.0.0.0", port=5210, workers=2, log_config=get_uvicorn_logging_config()) + uvicorn.run("aare.daq.server:app", host="127.0.0.1", port=5210, workers=2, log_config=get_uvicorn_logging_config()) if __name__ == "__main__": diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 2cabc77a..fa25edbe 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -1,6 +1,7 @@ import os import jwt import requests +from requests_gssapi import HTTPSPNEGOAuth from aare.common.models import TokenData from aare.common.auth_models import get_user @@ -28,6 +29,7 @@ def auth(base_url: str | None) -> str: headers={ "Content-Type": "application/x-www-form-urlencoded" }, + auth=HTTPSPNEGOAuth(), # Kerberos SPNEGO negotiation via user's TGT timeout=(3.0, 15.0), # (connect timeout, read timeout) ) except requests.RequestException as e: From 96d9e602e4c1f7d7f1f31235d72b8a12b64c4fd4 Mon Sep 17 00:00:00 2001 From: Dawn Date: Tue, 9 Jun 2026 15:44:42 +0200 Subject: [PATCH 07/28] remove dependency on gssapi --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d495f04a..78bfe0ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "opencv-python-headless==4.11.0.86", "PySide6==6.9.0", "requests==2.32.4", - "requests-gssapi>=1.3.0", "pyepics==3.5.8", "redis==6.2.0", "python-redis-lock==4.0.0", From fca7c588c5daf642257a08ecb7ed80e3d02507b1 Mon Sep 17 00:00:00 2001 From: Dawn Date: Tue, 9 Jun 2026 15:45:33 +0200 Subject: [PATCH 08/28] remove dependency on gssapi directly using curl to negotiate with https from GUI --- src/aare/gui/auth.py | 56 ++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index fa25edbe..f87e0709 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -1,7 +1,6 @@ -import os +import json +import subprocess import jwt -import requests -from requests_gssapi import HTTPSPNEGOAuth from aare.common.models import TokenData from aare.common.auth_models import get_user @@ -19,38 +18,45 @@ def auth(base_url: str | None) -> str: return jwt.encode(token_data.model_dump(), "ABC123") url = f"{base_url}/token" + apache_url = f"https://mx-x10sa-queue-01.psi.ch/" try: - response = requests.post( - url, - data={ - "username": curr_user, - "password": "" - }, - headers={ - "Content-Type": "application/x-www-form-urlencoded" - }, - auth=HTTPSPNEGOAuth(), # Kerberos SPNEGO negotiation via user's TGT - timeout=(3.0, 15.0), # (connect timeout, read timeout) + result = subprocess.run( + [ + 'curl', '-sk', '--cacert', '/my/top/secret/path', + '--negotiate', '-u', ':', + apache_url, + ], + capture_output=True, + text=True, + timeout=18.0, ) - except requests.RequestException as e: - logger.error(f"Authentication request failed (network): {e}") + except subprocess.TimeoutExpired as e: + logger.error(f"Authentication curl timed out: {e}") raise RuntimeError( - "Cannot reach AareDAQ server (network error). " + "Cannot reach AareDAQ server (timeout). " + "Please check the server is running and your connection." + ) from e + except FileNotFoundError as e: + logger.error(f"curl not found: {e}") + raise RuntimeError("curl not found on this system.") from e + except OSError as e: + logger.error(f"Authentication curl failed (OS error): {e}") + raise RuntimeError( + "Cannot reach AareDAQ server (OS error). " "Please check the server is running and your connection." ) from e - if response.status_code != 200: - # Avoid dumping full HTML/tracebacks into the GUI; keep it short and actionable - logger.error(f"Authentication request failed: HTTP {response.status_code}. Body: {response.text[:500]}") + if result.returncode != 0: + logger.error(f"Authentication curl exited {result.returncode}. stderr: {result.stderr[:500]}") raise RuntimeError( - f"Authentication failed (HTTP {response.status_code}). " - "The server may be starting up or unavailable." + f"Authentication failed (curl exit {result.returncode}). " + "Check server is running and Kerberos ticket is valid (kinit)." ) try: - response_json = response.json() - except ValueError as e: - logger.error(f"Authentication response was not JSON. Body: {response.text[:500]}") + response_json = json.loads(result.stdout) + except json.JSONDecodeError as e: + logger.error(f"Authentication response not JSON. stdout: {result.stdout[:500]}") raise RuntimeError( "Authentication failed (invalid server response). " "The server may be starting up or misconfigured." From c633891c4a3ba952155bebed7fb1b37ba624b956 Mon Sep 17 00:00:00 2001 From: Dawn Date: Tue, 9 Jun 2026 16:16:11 +0200 Subject: [PATCH 09/28] add Apache authentication and token handling using curl in GUI and access log parsing in DAQ --- src/aare/daq/auth.py | 34 ++++++++++++++++++ src/aare/daq/server.py | 2 +- src/aare/gui/auth.py | 81 +++++++++++++++++++++++++++++++----------- 3 files changed, 96 insertions(+), 21 deletions(-) diff --git a/src/aare/daq/auth.py b/src/aare/daq/auth.py index a8e36b92..e0495098 100644 --- a/src/aare/daq/auth.py +++ b/src/aare/daq/auth.py @@ -1,6 +1,7 @@ import grp import os import pwd +import re import uuid from datetime import datetime, timedelta, UTC from typing import List @@ -30,6 +31,10 @@ BATON_REQUEST_TIMEOUT_SECONDS = 30 STAFF_GROUP = "unx-MXgroup" SUPER_USERS = ["e10019", "e11206", "e18147"] +APACHE_ACCESS_LOG = "/var/log/httpd/daq-access.log" +# Common Log Format: IP - username [timestamp] "request" status bytes ... +_LOG_PATTERN = re.compile(r'^\S+ \S+ (\S+) \[.*?\] ".*?" (\d+)') + oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") class TokenData(BaseModel): @@ -46,6 +51,35 @@ def create_access_token(token: TokenData): return encoded_jwt +def authenticate_from_apache_log() -> str: + """ + Read the Apache access log and return the username from the most recent + 200 response. Apache and the DAQ server are co-located on the same machine. + """ + try: + with open(APACHE_ACCESS_LOG, 'r') as f: + lines = f.readlines() + except OSError as e: + raise AuthenticationException( + message=f"Cannot read Apache access log: {e}", + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + code=AuthErrorCode.INVALID_TOKEN, + ) from e + + for line in reversed(lines): + m = _LOG_PATTERN.match(line) + if m and m.group(2) == '200' and m.group(1) != '-': + return m.group(1) + + raise AuthenticationException( + message="No authenticated user found in Apache access log", + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + code=AuthErrorCode.INVALID_TOKEN, + ) + + def authenticate_from_proxy_header(request: Request) -> str: """ Extract the pre-authenticated username from the trusted Apache proxy header. diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index 064b3cf8..a7877315 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -274,7 +274,7 @@ async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends if request.headers.get("X-Remote-User"): username = auth.authenticate_from_proxy_header(request) else: - username = form_data.username + username = auth.authenticate_from_apache_log() data = await run_in_threadpool(auth.authenticate_user, cfg, username) return {"access_token": data, "token_type": "bearer"} diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index f87e0709..a2f9c39b 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -8,6 +8,9 @@ from aare.common.logger_config import setup_logger logger = setup_logger('aareGUI') +APACHE_URL = "https://mx-x10sa-queue-01.psi.ch/" +CACERT = "/sls/x10sa/misc/.cert/10s.crt" + def auth(base_url: str | None) -> str: curr_user = get_user() if base_url is None: @@ -17,46 +20,84 @@ def auth(base_url: str | None) -> str: pgroups=["p16371", "p22233"]) return jwt.encode(token_data.model_dump(), "ABC123") - url = f"{base_url}/token" - apache_url = f"https://mx-x10sa-queue-01.psi.ch/" + # Step 1: SPNEGO against Apache, dump response headers, discard body try: - result = subprocess.run( - [ - 'curl', '-sk', '--cacert', '/my/top/secret/path', - '--negotiate', '-u', ':', - apache_url, - ], + apache_result = subprocess.run( + ['curl', '-sk', '--cacert', CACERT, + '--negotiate', '-u', ':', + '-D', '-', '-o', '/dev/null', + APACHE_URL], capture_output=True, text=True, timeout=18.0, ) except subprocess.TimeoutExpired as e: - logger.error(f"Authentication curl timed out: {e}") + logger.error(f"Apache auth curl timed out: {e}") raise RuntimeError( - "Cannot reach AareDAQ server (timeout). " + "Cannot reach Apache server (timeout). " "Please check the server is running and your connection." ) from e except FileNotFoundError as e: logger.error(f"curl not found: {e}") raise RuntimeError("curl not found on this system.") from e except OSError as e: - logger.error(f"Authentication curl failed (OS error): {e}") + logger.error(f"Apache auth curl OS error: {e}") + raise RuntimeError( + "Cannot reach Apache server (OS error). " + "Please check the server is running and your connection." + ) from e + + if apache_result.returncode != 0: + logger.error(f"Apache curl exited {apache_result.returncode}. stderr: {apache_result.stderr[:500]}") + raise RuntimeError( + f"Apache authentication failed (curl exit {apache_result.returncode}). " + "Check Kerberos ticket is valid (kinit)." + ) + + # Parse X-Remote-User from response headers (stdout = headers due to -D -) + remote_user = None + for line in apache_result.stdout.splitlines(): + if line.lower().startswith('x-remote-user:'): + remote_user = line.split(':', 1)[1].strip() + break + + if not remote_user: + logger.error(f"X-Remote-User not found in Apache response headers. Headers: {apache_result.stdout[:500]}") + raise RuntimeError("Apache authentication failed (no X-Remote-User in response).") + + # Step 2: Call FastAPI /token with the authenticated username + url = f"{base_url}/token" + try: + token_result = subprocess.run( + ['curl', '-sk', '--cacert', CACERT, + '-X', 'POST', url, + '-d', f'username={remote_user}&password=', + '-H', 'Content-Type: application/x-www-form-urlencoded'], + capture_output=True, + text=True, + timeout=18.0, + ) + except subprocess.TimeoutExpired as e: + logger.error(f"Token request curl timed out: {e}") + raise RuntimeError( + "Cannot reach AareDAQ server (timeout). " + "Please check the server is running and your connection." + ) from e + except OSError as e: + logger.error(f"Token request curl OS error: {e}") raise RuntimeError( "Cannot reach AareDAQ server (OS error). " "Please check the server is running and your connection." ) from e - if result.returncode != 0: - logger.error(f"Authentication curl exited {result.returncode}. stderr: {result.stderr[:500]}") - raise RuntimeError( - f"Authentication failed (curl exit {result.returncode}). " - "Check server is running and Kerberos ticket is valid (kinit)." - ) + if token_result.returncode != 0: + logger.error(f"Token curl exited {token_result.returncode}. stderr: {token_result.stderr[:500]}") + raise RuntimeError(f"Token request failed (curl exit {token_result.returncode}).") try: - response_json = json.loads(result.stdout) + response_json = json.loads(token_result.stdout) except json.JSONDecodeError as e: - logger.error(f"Authentication response not JSON. stdout: {result.stdout[:500]}") + logger.error(f"Token response not JSON. stdout: {token_result.stdout[:500]}") raise RuntimeError( "Authentication failed (invalid server response). " "The server may be starting up or misconfigured." @@ -64,7 +105,7 @@ def auth(base_url: str | None) -> str: token = response_json.get("access_token") if not token or not isinstance(token, str): - logger.error(f"Authentication response missing access_token. Keys: {list(response_json.keys())}") + logger.error(f"Missing access_token. Keys: {list(response_json.keys())}") raise RuntimeError( "Authentication failed (missing token in server response). " "The server may be starting up." From 11a56addee030e04c416f2c306ef5bb542c83727 Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 10 Jun 2026 10:18:31 +0200 Subject: [PATCH 10/28] migrate DAQ URLs to HTTPS and simplify Kerberos SPNEGO token authentication --- src/aare/common/config/x06da.yaml | 2 +- src/aare/common/config/x06sa.yaml | 2 +- src/aare/common/config/x10sa.yaml | 2 +- src/aare/gui/auth.py | 60 ++++++------------------------- src/aare/gui/gui.py | 6 ++-- 5 files changed, 17 insertions(+), 55 deletions(-) diff --git a/src/aare/common/config/x06da.yaml b/src/aare/common/config/x06da.yaml index 4af162f2..a61eefe6 100644 --- a/src/aare/common/config/x06da.yaml +++ b/src/aare/common/config/x06da.yaml @@ -10,7 +10,7 @@ gui: gonio_camera_id: "1" daq: - daq_url: "http://mx-x06da-queue-01.psi.ch:5210" + daq_url: "https://mx-x06da-queue-01.psi.ch" shared: jfjoch: diff --git a/src/aare/common/config/x06sa.yaml b/src/aare/common/config/x06sa.yaml index 49073f43..d9502c6b 100644 --- a/src/aare/common/config/x06sa.yaml +++ b/src/aare/common/config/x06sa.yaml @@ -8,7 +8,7 @@ gui: gonio_camera_id: "" daq: - daq_url: "http://mx-x06sa-queue-01.psi.ch:5210" + daq_url: "https://mx-x06sa-queue-01.psi.ch" shared: jfjoch: diff --git a/src/aare/common/config/x10sa.yaml b/src/aare/common/config/x10sa.yaml index 52166687..83194a47 100644 --- a/src/aare/common/config/x10sa.yaml +++ b/src/aare/common/config/x10sa.yaml @@ -9,7 +9,7 @@ gui: gonio_camera_id: "1" daq: - daq_url: "http://mx-x10sa-queue-01.psi.ch:5210" + daq_url: "https://mx-x10sa-queue-01.psi.ch" shared: jfjoch: diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index a2f9c39b..55297b48 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -8,7 +8,6 @@ from aare.common.logger_config import setup_logger logger = setup_logger('aareGUI') -APACHE_URL = "https://mx-x10sa-queue-01.psi.ch/" CACERT = "/sls/x10sa/misc/.cert/10s.crt" def auth(base_url: str | None) -> str: @@ -20,58 +19,15 @@ def auth(base_url: str | None) -> str: pgroups=["p16371", "p22233"]) return jwt.encode(token_data.model_dump(), "ABC123") - # Step 1: SPNEGO against Apache, dump response headers, discard body - try: - apache_result = subprocess.run( - ['curl', '-sk', '--cacert', CACERT, - '--negotiate', '-u', ':', - '-D', '-', '-o', '/dev/null', - APACHE_URL], - capture_output=True, - text=True, - timeout=18.0, - ) - except subprocess.TimeoutExpired as e: - logger.error(f"Apache auth curl timed out: {e}") - raise RuntimeError( - "Cannot reach Apache server (timeout). " - "Please check the server is running and your connection." - ) from e - except FileNotFoundError as e: - logger.error(f"curl not found: {e}") - raise RuntimeError("curl not found on this system.") from e - except OSError as e: - logger.error(f"Apache auth curl OS error: {e}") - raise RuntimeError( - "Cannot reach Apache server (OS error). " - "Please check the server is running and your connection." - ) from e - - if apache_result.returncode != 0: - logger.error(f"Apache curl exited {apache_result.returncode}. stderr: {apache_result.stderr[:500]}") - raise RuntimeError( - f"Apache authentication failed (curl exit {apache_result.returncode}). " - "Check Kerberos ticket is valid (kinit)." - ) - - # Parse X-Remote-User from response headers (stdout = headers due to -D -) - remote_user = None - for line in apache_result.stdout.splitlines(): - if line.lower().startswith('x-remote-user:'): - remote_user = line.split(':', 1)[1].strip() - break - - if not remote_user: - logger.error(f"X-Remote-User not found in Apache response headers. Headers: {apache_result.stdout[:500]}") - raise RuntimeError("Apache authentication failed (no X-Remote-User in response).") - - # Step 2: Call FastAPI /token with the authenticated username + # Single call: Kerberos SPNEGO through Apache, which proxies to the DAQ server. + # Apache injects X-Remote-User; the DAQ server reads it from the localhost-proxied request. url = f"{base_url}/token" try: token_result = subprocess.run( ['curl', '-sk', '--cacert', CACERT, + '--negotiate', '-u', ':', '-X', 'POST', url, - '-d', f'username={remote_user}&password=', + '-d', 'username=&password=', '-H', 'Content-Type: application/x-www-form-urlencoded'], capture_output=True, text=True, @@ -83,6 +39,9 @@ def auth(base_url: str | None) -> str: "Cannot reach AareDAQ server (timeout). " "Please check the server is running and your connection." ) from e + except FileNotFoundError as e: + logger.error(f"curl not found: {e}") + raise RuntimeError("curl not found on this system.") from e except OSError as e: logger.error(f"Token request curl OS error: {e}") raise RuntimeError( @@ -92,7 +51,10 @@ def auth(base_url: str | None) -> str: if token_result.returncode != 0: logger.error(f"Token curl exited {token_result.returncode}. stderr: {token_result.stderr[:500]}") - raise RuntimeError(f"Token request failed (curl exit {token_result.returncode}).") + raise RuntimeError( + f"Token request failed (curl exit {token_result.returncode}). " + "Check Kerberos ticket is valid (kinit) and server is reachable." + ) try: response_json = json.loads(token_result.stdout) diff --git a/src/aare/gui/gui.py b/src/aare/gui/gui.py index 51995574..18baa785 100644 --- a/src/aare/gui/gui.py +++ b/src/aare/gui/gui.py @@ -45,21 +45,21 @@ def main(): #TODO if zmq and pred stream come from same source, do not need images from both streams, can combine match mx_beamline(): case MXBeamline.X06DA: - default_url = cfg_get("gui.daq.daq_url", "http://mx-x06da-queue-01.psi.ch:5210") + default_url = cfg_get("gui.daq.daq_url", "https://mx-x06da-queue-01.psi.ch") default_zmq_addr = cfg_get("gui.cameras.sample_camera_zmq_url", "tcp://x06da-pserv-01:9089") default_pred_zmq_addr = cfg_get("gui.cameras.prediction_zmq_url", "tcp://mx-ml:9091") default_beamline_cam_addr = cfg_get("gui.cameras.beamline_camera_url", "x06da-axis-1.psi.ch") default_gonio_cam_addr = cfg_get("gui.cameras.gonio_camera_url", "axis-accc8ed2972e.psi.ch") default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 3)) case MXBeamline.X10SA: - default_url = cfg_get("gui.daq.daq_url", "http://mx-x10sa-queue-01.psi.ch:5210") + default_url = cfg_get("gui.daq.daq_url", "https://mx-x10sa-queue-01.psi.ch") default_zmq_addr = cfg_get("gui.cameras.sample_camera_zmq_url", "tcp://x10sa-spark-01:9091") default_pred_zmq_addr = cfg_get("gui.cameras.prediction_zmq_url", "tcp://x10sa-spark-01:9091") default_beamline_cam_addr = cfg_get("gui.cameras.beamline_camera_url", "axis-accc8eb02488.psi.ch") default_gonio_cam_addr = cfg_get("gui.cameras.gonio_camera_url", "axis-accc8ea5e463.psi.ch") default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 1)) case MXBeamline.X06SA: - default_url = cfg_get("gui.daq.daq_url", "http://mx-x06sa-queue-01.psi.ch:5210") + default_url = cfg_get("gui.daq.daq_url", "https://mx-x06sa-queue-01.psi.ch") default_zmq_addr = cfg_get("gui.cameras.sample_camera_zmq_url", "") default_pred_zmq_addr = cfg_get("gui.cameras.prediction_zmq_url", "") default_beamline_cam_addr = cfg_get("gui.cameras.beamline_camera_url", "") From 4c0c86b8ec99503622b50678b491dc7f698cad08 Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 10 Jun 2026 10:24:28 +0200 Subject: [PATCH 11/28] validate proxy header authentication by checking loopback addresses --- src/aare/daq/auth.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/aare/daq/auth.py b/src/aare/daq/auth.py index e0495098..d4a25b89 100644 --- a/src/aare/daq/auth.py +++ b/src/aare/daq/auth.py @@ -1,4 +1,5 @@ import grp +import ipaddress import os import pwd import re @@ -80,14 +81,27 @@ def authenticate_from_apache_log() -> str: ) +def _is_loopback(host: str | None) -> bool: + if host is None: + return False + try: + addr = ipaddress.ip_address(host) + # Unwrap IPv4-mapped IPv6 (e.g. ::ffff:127.0.0.1) before checking + if isinstance(addr, ipaddress.IPv6Address) and addr.ipv4_mapped is not None: + return addr.ipv4_mapped.is_loopback + return addr.is_loopback + except ValueError: + return False + + def authenticate_from_proxy_header(request: Request) -> str: """ Extract the pre-authenticated username from the trusted Apache proxy header. - Only trust this header when the request originates from localhost (127.0.0.1 / ::1), - meaning only the Apache proxy running on the same host can supply it. + Only trust this header when the request originates from localhost, meaning + only the Apache proxy running on the same host can supply it. """ client_host = request.client.host if request.client else None - if client_host not in ("127.0.0.1", "::1"): + if not _is_loopback(client_host): raise AuthenticationException( message="X-Remote-User header is only trusted from the localhost proxy", status_code=401, From 7c19d80e20cff11f2b7c6a0439d24d14e9a4f679 Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 10 Jun 2026 10:27:37 +0200 Subject: [PATCH 12/28] add logging for client host validation in authentication module --- src/aare/daq/auth.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/aare/daq/auth.py b/src/aare/daq/auth.py index d4a25b89..027d0c82 100644 --- a/src/aare/daq/auth.py +++ b/src/aare/daq/auth.py @@ -1,5 +1,6 @@ import grp import ipaddress +import logging import os import pwd import re @@ -8,6 +9,8 @@ from datetime import datetime, timedelta, UTC from typing import List import time +logger = logging.getLogger("aareDAQ") + import jwt from fastapi import Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer @@ -101,7 +104,9 @@ def authenticate_from_proxy_header(request: Request) -> str: only the Apache proxy running on the same host can supply it. """ client_host = request.client.host if request.client else None + logger.debug(f"[auth] /token client_host={client_host!r} request.client={request.client!r}") if not _is_loopback(client_host): + logger.warning(f"[auth] Rejecting X-Remote-User: client_host {client_host!r} is not loopback") raise AuthenticationException( message="X-Remote-User header is only trusted from the localhost proxy", status_code=401, From 7b60d67ebe8c3d5b60e7d90df2c911aab08ce046 Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 10 Jun 2026 10:35:36 +0200 Subject: [PATCH 13/28] disable proxy headers in uvicorn configuration --- src/aare/daq/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index a7877315..df4bd402 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -2386,7 +2386,7 @@ def main(): urllib3.disable_warnings() # Run the application using uvicorn - uvicorn.run("aare.daq.server:app", host="127.0.0.1", port=5210, workers=2, log_config=get_uvicorn_logging_config()) + uvicorn.run("aare.daq.server:app", host="127.0.0.1", port=5210, workers=2, proxy_headers=False, log_config=get_uvicorn_logging_config()) if __name__ == "__main__": From 78cb1603773b51e6ca99c0b17062aa8bacb1e805 Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 10 Jun 2026 11:26:04 +0200 Subject: [PATCH 14/28] make sure GUI checks the certificate for HTTPS requests --- src/aare/gui/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 55297b48..374a4ff9 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -24,7 +24,7 @@ def auth(base_url: str | None) -> str: url = f"{base_url}/token" try: token_result = subprocess.run( - ['curl', '-sk', '--cacert', CACERT, + ['curl', '-s', '--cacert', CACERT, '--negotiate', '-u', ':', '-X', 'POST', url, '-d', 'username=&password=', From 37ef7e8381eafb6c9afcf95d339f217fa9b96feb Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 10 Jun 2026 11:28:07 +0200 Subject: [PATCH 15/28] handle SSL errors for self-signed certificates in SSEClient and DAQWorker --- src/aare/gui/threads/daq_worker.py | 15 ++++++++++++++- src/aare/gui/threads/sse_client.py | 14 +++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/aare/gui/threads/daq_worker.py b/src/aare/gui/threads/daq_worker.py index f50fe798..4fb8965f 100644 --- a/src/aare/gui/threads/daq_worker.py +++ b/src/aare/gui/threads/daq_worker.py @@ -10,7 +10,7 @@ from collections import deque from typing import cast, Literal from PySide6.QtCore import Signal, QUrl, Slot, QTimer, QObject, QByteArray -from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QSslError from jfjoch_client import ScanResult, ScanResultImagesInner from aare.common.auth_models import BatonStatus @@ -120,6 +120,7 @@ class DAQWorker(QObject): self.__token = token self.__base_url = base_url self.__net_manager = QNetworkAccessManager() + self.__net_manager.sslErrors.connect(self._handle_ssl_errors) self.__timer = QTimer() self.__timer.setInterval(500) self.__timer.timeout.connect(self.regular_update) @@ -326,6 +327,18 @@ class DAQWorker(QObject): reply = self.__net_manager.get(request) reply.finished.connect(lambda: self.handle_status_response(reply)) + _SELF_SIGNED_ERRORS = { + QSslError.SslError.SelfSignedCertificate, + QSslError.SslError.SelfSignedCertificateInChain, + } + + @staticmethod + def _handle_ssl_errors(reply: QNetworkReply, errors: list): + self_signed = [e for e in errors if e.error() in DAQWorker._SELF_SIGNED_ERRORS] + if self_signed: + reply.ignoreSslErrors(self_signed) + + @staticmethod def handle_response(reply: QNetworkReply): """ diff --git a/src/aare/gui/threads/sse_client.py b/src/aare/gui/threads/sse_client.py index be1db929..05221c10 100644 --- a/src/aare/gui/threads/sse_client.py +++ b/src/aare/gui/threads/sse_client.py @@ -1,6 +1,6 @@ from typing import Optional, Dict from PySide6.QtCore import QObject, Signal, Slot, QUrl, QTimer, QByteArray -from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QSslError class SSEClient(QObject): @@ -14,6 +14,7 @@ class SSEClient(QObject): def __init__(self, parent=None): super().__init__(parent) self._network_manager = QNetworkAccessManager(self) + self._network_manager.sslErrors.connect(self._handle_ssl_errors) self._reply: Optional[QNetworkReply] = None self._reconnect_timer = QTimer(self) self._reconnect_timer.setSingleShot(True) @@ -60,6 +61,17 @@ class SSEClient(QObject): """Check if connected to SSE""" return self._connected and self._reply and self._reply.isOpen() + _SELF_SIGNED_ERRORS = { + QSslError.SslError.SelfSignedCertificate, + QSslError.SslError.SelfSignedCertificateInChain, + } + + @staticmethod + def _handle_ssl_errors(reply: QNetworkReply, errors: list): + self_signed = [e for e in errors if e.error() in SSEClient._SELF_SIGNED_ERRORS] + if self_signed: + reply.ignoreSslErrors(self_signed) + def _start_connection(self): """Start SSE connection""" request = QNetworkRequest(self._url) From 1919c25874b92bbdbac5969eb77438349396f51f Mon Sep 17 00:00:00 2001 From: appleb_m Date: Wed, 10 Jun 2026 11:40:30 +0200 Subject: [PATCH 16/28] error handling in daq --- src/aare/daq/daq.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index ae16e630..db5ca148 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -1372,7 +1372,7 @@ class AareDAQ: event_type=SampleEventType.COLLECTIONFAILED, additional_comment=f"JFJoch communication error: {e}" ) - return None + raise except Exception as e: logger.error(f"Rotation sequence failed: {e}") self._handle_operation_error( @@ -1381,7 +1381,7 @@ class AareDAQ: error=e, event_type=SampleEventType.COLLECTIONFAILED ) - return None + raise def _append_smargon_trace(self, *, sample_id: int | None, event: str) -> None: try: @@ -2075,14 +2075,19 @@ class AareDAQ: result = self._execute_rotation_sequence(request) if result is None: + logger.error("Rotation scan failed, no result returned") raise DataCollectionException("Rotation scan failed, no result returned") self.__set_state(BeamlineStateEnum.SampleAlignment) self.__cfg.state_busy = False return result - except Exception as e: - self.__set_state(BeamlineStateEnum.SampleAlignment) - self.__cfg.state_busy = False - raise + finally: + try: + if self.__cfg.state_busy and self.__cfg.state != BeamlineStateEnum.Maintenance: + self.__set_state(BeamlineStateEnum.SampleAlignment) + except Exception as cleanup_error: + logger.exception(f"Failed to restore SampleAlignment after rotation: {cleanup_error}") + finally: + self.__cfg.state_busy = False @property def dtz(self) -> float: From 7613fec52c60fb0408d1c7b14acb6b6f3139f113 Mon Sep 17 00:00:00 2001 From: appleb_m Date: Wed, 10 Jun 2026 11:40:43 +0200 Subject: [PATCH 17/28] auth: printing base_url for debugging --- src/aare/gui/auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 374a4ff9..8beb2e7c 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -11,6 +11,7 @@ logger = setup_logger('aareGUI') CACERT = "/sls/x10sa/misc/.cert/10s.crt" def auth(base_url: str | None) -> str: + print(f"base_url: {base_url}") curr_user = get_user() if base_url is None: token_data = TokenData(sub=curr_user, @@ -22,6 +23,7 @@ def auth(base_url: str | None) -> str: # Single call: Kerberos SPNEGO through Apache, which proxies to the DAQ server. # Apache injects X-Remote-User; the DAQ server reads it from the localhost-proxied request. url = f"{base_url}/token" + # url = f"{base_url}" try: token_result = subprocess.run( ['curl', '-s', '--cacert', CACERT, From 5e00773160afd589fa223d5c23f60ba5c09dabb3 Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 10 Jun 2026 14:09:22 +0200 Subject: [PATCH 18/28] pass cert_path for each beamline yaml --- src/aare/common/config/x06da.yaml | 2 +- src/aare/common/config/x06sa.yaml | 1 + src/aare/common/config/x10sa.yaml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/aare/common/config/x06da.yaml b/src/aare/common/config/x06da.yaml index a61eefe6..9b17f3e5 100644 --- a/src/aare/common/config/x06da.yaml +++ b/src/aare/common/config/x06da.yaml @@ -11,7 +11,7 @@ gui: daq: daq_url: "https://mx-x06da-queue-01.psi.ch" - + cert_path: "/sls/x06da/misc/.cert/6d.crt" shared: jfjoch: jfjoch_url: "http://sls-gpu-001:8080" diff --git a/src/aare/common/config/x06sa.yaml b/src/aare/common/config/x06sa.yaml index d9502c6b..7e022dc7 100644 --- a/src/aare/common/config/x06sa.yaml +++ b/src/aare/common/config/x06sa.yaml @@ -9,6 +9,7 @@ gui: daq: daq_url: "https://mx-x06sa-queue-01.psi.ch" + cert_path: "/sls/x06sa/misc/.cert/6s.crt" shared: jfjoch: diff --git a/src/aare/common/config/x10sa.yaml b/src/aare/common/config/x10sa.yaml index 83194a47..cc370a2d 100644 --- a/src/aare/common/config/x10sa.yaml +++ b/src/aare/common/config/x10sa.yaml @@ -10,6 +10,7 @@ gui: daq: daq_url: "https://mx-x10sa-queue-01.psi.ch" + cert_path: "/sls/x10sa/misc/.cert/10s.crt" shared: jfjoch: From 0212f5c29ecbda0ef6ddebb5dde49236ce84f793 Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 10 Jun 2026 14:23:03 +0200 Subject: [PATCH 19/28] remove hard coded CACERT path --- src/aare/gui/auth.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 8beb2e7c..45d15f25 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -8,10 +8,7 @@ from aare.common.logger_config import setup_logger logger = setup_logger('aareGUI') -CACERT = "/sls/x10sa/misc/.cert/10s.crt" - -def auth(base_url: str | None) -> str: - print(f"base_url: {base_url}") +def auth(base_url: str | None, cert_path: str | None) -> str: curr_user = get_user() if base_url is None: token_data = TokenData(sub=curr_user, @@ -23,7 +20,6 @@ def auth(base_url: str | None) -> str: # Single call: Kerberos SPNEGO through Apache, which proxies to the DAQ server. # Apache injects X-Remote-User; the DAQ server reads it from the localhost-proxied request. url = f"{base_url}/token" - # url = f"{base_url}" try: token_result = subprocess.run( ['curl', '-s', '--cacert', CACERT, From a0dee43a37ce4831f58f221a24e043909eaa58d4 Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 10 Jun 2026 14:23:28 +0200 Subject: [PATCH 20/28] load cacert path from funtion parameters --- src/aare/gui/auth.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 45d15f25..30c3e923 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -20,6 +20,7 @@ def auth(base_url: str | None, cert_path: str | None) -> str: # Single call: Kerberos SPNEGO through Apache, which proxies to the DAQ server. # Apache injects X-Remote-User; the DAQ server reads it from the localhost-proxied request. url = f"{base_url}/token" + cacert = f"{cert_path}" try: token_result = subprocess.run( ['curl', '-s', '--cacert', CACERT, From 0c0ebe2ff152407c2abc83acddea37ddb309bde2 Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 10 Jun 2026 14:23:44 +0200 Subject: [PATCH 21/28] use cacert path from function parameters --- src/aare/gui/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 30c3e923..379433b0 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -23,7 +23,7 @@ def auth(base_url: str | None, cert_path: str | None) -> str: cacert = f"{cert_path}" try: token_result = subprocess.run( - ['curl', '-s', '--cacert', CACERT, + ['curl', '-s', '--cacert', cacert, '--negotiate', '-u', ':', '-X', 'POST', url, '-d', 'username=&password=', From c3bc745210fd121976351122676e93b853222016 Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 10 Jun 2026 14:24:08 +0200 Subject: [PATCH 22/28] add support for configurable certificate paths per beamline in GUI --- src/aare/gui/gui.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/aare/gui/gui.py b/src/aare/gui/gui.py index 18baa785..aceb5334 100644 --- a/src/aare/gui/gui.py +++ b/src/aare/gui/gui.py @@ -46,6 +46,7 @@ def main(): match mx_beamline(): case MXBeamline.X06DA: default_url = cfg_get("gui.daq.daq_url", "https://mx-x06da-queue-01.psi.ch") + default_cert_path = cfg_get("gui.daq.cert_path", "/sls/x06da/misc/.cert/6d.crt") default_zmq_addr = cfg_get("gui.cameras.sample_camera_zmq_url", "tcp://x06da-pserv-01:9089") default_pred_zmq_addr = cfg_get("gui.cameras.prediction_zmq_url", "tcp://mx-ml:9091") default_beamline_cam_addr = cfg_get("gui.cameras.beamline_camera_url", "x06da-axis-1.psi.ch") @@ -53,6 +54,7 @@ def main(): default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 3)) case MXBeamline.X10SA: default_url = cfg_get("gui.daq.daq_url", "https://mx-x10sa-queue-01.psi.ch") + default_cert_path = cfg_get("gui.daq.cert_path", "/sls/x10sa/misc/.cert/10s.crt") default_zmq_addr = cfg_get("gui.cameras.sample_camera_zmq_url", "tcp://x10sa-spark-01:9091") default_pred_zmq_addr = cfg_get("gui.cameras.prediction_zmq_url", "tcp://x10sa-spark-01:9091") default_beamline_cam_addr = cfg_get("gui.cameras.beamline_camera_url", "axis-accc8eb02488.psi.ch") @@ -60,6 +62,7 @@ def main(): default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 1)) case MXBeamline.X06SA: default_url = cfg_get("gui.daq.daq_url", "https://mx-x06sa-queue-01.psi.ch") + default_cert_path = cfg_get("gui.daq.cert_path", "/sls/x06sa/misc/.cert/6s.crt") default_zmq_addr = cfg_get("gui.cameras.sample_camera_zmq_url", "") default_pred_zmq_addr = cfg_get("gui.cameras.prediction_zmq_url", "") default_beamline_cam_addr = cfg_get("gui.cameras.beamline_camera_url", "") @@ -67,6 +70,7 @@ def main(): default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 1)) case _: default_url = cfg_get("gui.daq.daq_url", "") + default_cert_path = cfg_get("gui.daq.cert_path", "") default_zmq_addr = cfg_get("gui.cameras.sample_camera_zmq_url", "") default_pred_zmq_addr = cfg_get("gui.cameras.prediction_zmq_url", "") default_beamline_cam_addr = cfg_get("gui.cameras.beamline_camera_url", "") @@ -79,6 +83,11 @@ def main(): "url", default_url) parser.addOption(urlOption) + certPath = QCommandLineOption(["u", "aaredaq-cert-path"], + "Server Certificate Path (for self-signed certificates)", + "cert", default_cert_path) + parser.addOption(certPath) + defaultImage = QCommandLineOption(["i", "image"], "Default image to display in absence of the ZMQ stream", "image") @@ -105,6 +114,10 @@ def main(): if base_url == "" or base_url.lower() == "none": base_url = None + cert_path = parser.value(certPath) + if cert_path == "" or cert_path.lower() == "none": + cert_path = None + zmq_addr = parser.value(cameraZeroMQ) if zmq_addr == "": zmq_addr = None @@ -123,7 +136,7 @@ def main(): try: splash.set_progress(50, f"Connecting to {base_url or 'backend'}...") - token = auth(base_url) + token = auth(base_url, cert_path) if not token or token.count(".") != 2: raise RuntimeError( "Authentication did not return a valid token. " From 076c796b4df259a46edaa2a7a32d3d4bd27cb6ee Mon Sep 17 00:00:00 2001 From: appleb_m Date: Wed, 10 Jun 2026 13:17:28 +0200 Subject: [PATCH 23/28] tests: updated tests for new authentication protocols and for new sample short info --- .../common/test_data_collection_parameters.py | 28 ++++++- tests/unit/common/test_models_extra.py | 68 ++++++++------- tests/unit/daq/test_auth.py | 21 +++-- tests/unit/daq/test_server.py | 10 ++- tests/unit/daq/test_spreadsheetupdater.py | 40 +++++---- tests/unit/gui/test_auth_mock.py | 84 +++++++------------ 6 files changed, 134 insertions(+), 117 deletions(-) diff --git a/tests/unit/common/test_data_collection_parameters.py b/tests/unit/common/test_data_collection_parameters.py index b5ea0a1f..41e5f887 100644 --- a/tests/unit/common/test_data_collection_parameters.py +++ b/tests/unit/common/test_data_collection_parameters.py @@ -46,4 +46,30 @@ def test_aperture_accepts_float_string(): def test_processingpipeline_accepts_unknown_value_after_refactor(): params = DataCollectionParameters(processingpipeline="xia2") - assert params.processingpipeline == "xia2" \ No newline at end of file + assert params.processingpipeline == "xia2" + + +def test_datacollectionparameters_accepts_legacy_aliases(): + params = DataCollectionParameters( + totalrange=180, + cellparameters="10 20 30 90 90 120", + userresolution=1.4, + ) + assert params.totalangle == 180 + assert params.unitcell == "10 20 30 90 90 120" + assert params.processingresolution == 1.4 + + +def test_datacollectionparameters_accepts_new_fields(): + params = DataCollectionParameters( + totalangle=90, + unitcell="11,22,33,90,90,120", + processingresolution=1.2, + pdbmodel="model.pdb", + cloud=False, + ) + assert params.totalangle == 90 + assert params.unitcell == "11,22,33,90,90,120" + assert params.processingresolution == 1.2 + assert params.pdbmodel == "model.pdb" + assert params.cloud is False \ No newline at end of file diff --git a/tests/unit/common/test_models_extra.py b/tests/unit/common/test_models_extra.py index 2c7c5032..7c9557b8 100644 --- a/tests/unit/common/test_models_extra.py +++ b/tests/unit/common/test_models_extra.py @@ -1,5 +1,14 @@ import pytest -from aare.common.models import SampleShortInfo, DewarAddress, BeamMarkCoeffModel, MLOutputModel, MLBoxType, BeamlineStateEnum +from aare.common.models import ( + SampleShortInfo, + DewarAddress, + BeamMarkCoeffModel, + MLOutputModel, + MLBoxType, + BeamlineStateEnum, + DataCollectionParameters, +) + def test_sample_short_info_methods(): info = SampleShortInfo( @@ -8,34 +17,42 @@ def test_sample_short_info_methods(): dewar_name="dew1", sample_name="sample1", run_number=1, + aaredb_params=DataCollectionParameters( + totalangle=180, + processingresolution=1.5, + cloud=True, + ), user="group1", pin=3, location=DewarAddress(segment="A", pos=2), ) - - # Test tell_address (line 380) + addr = info.tell_address() assert addr.puck.segment == "A" assert addr.puck.pos == 2 assert addr.pin == 3 - - # Test loc_str (lines 383-386) + assert info.loc_str() == "A2-3" - - # Test loc_str_sort (lines 389-392) assert info.loc_str_sort() == "A2-03" - + assert info.aaredb_params is not None + assert info.aaredb_params.totalangle == 180 + assert info.aaredb_params.processingresolution == 1.5 + info_no_loc = info.model_copy(update={"location": None}) assert info_no_loc.loc_str() == "-" assert info_no_loc.loc_str_sort() == "" - # Test from_dict (line 396) data = { "db_id": 2, "puck_name": "puck2", "dewar_name": "dew2", "sample_name": "sample2", "run_number": 2, + "aaredb_params": { + "totalrange": 120, + "userresolution": 1.8, + "cloud": "", + }, "user": "group2", "pin": 4, "location": {"segment": "B", "pos": 5} @@ -43,47 +60,43 @@ def test_sample_short_info_methods(): info2 = SampleShortInfo.from_dict(data) assert info2.db_id == 2 assert info2.location.segment == "B" + assert info2.aaredb_params is not None + assert info2.aaredb_params.totalangle == 120 + assert info2.aaredb_params.processingresolution == 1.8 + assert info2.aaredb_params.cloud is True + def test_beam_mark_coeff_model_apply(): - # Test apply (line 414) model = BeamMarkCoeffModel( coeff_x=(1.0, 2.0, 5.0), coeff_y=(3.0, 4.0, 6.0) ) - # zoom = 10 - # x = 1.0 * 100 + 2.0 * 10 + 5.0 = 125.0 - # y = 3.0 * 100 + 4.0 * 10 + 6.0 = 346.0 res = model.apply(10.0) assert res.x == 125.0 assert res.y == 346.0 + def test_ml_output_model_extra_methods(): model = MLOutputModel() key = model.add_box(MLBoxType.CRYSTAL, (1, 2, 3, 4), 0.8) - - # Test get_box_model (line 499) + box_model = model.get_box_model(key) assert box_model.conf == 0.8 - - # Test get_box_tuple (lines 502-505) + assert model.get_box_tuple(key) == (1, 2, 3, 4) assert model.get_box_tuple("NonExistent") is None - - # Test get_box_tuple_with_conf (lines 508-512) + assert model.get_box_tuple_with_conf(key) == (1, 2, 3, 4, 0.8) assert model.get_box_tuple_with_conf("NonExistent") is None - - # Test get_tuples_for_class (lines 523-527) + tuples = model.get_tuples_for_class(MLBoxType.CRYSTAL) assert len(tuples) == 1 assert tuples[0] == (1, 2, 3, 4) - - # Test get_tuples_with_conf_for_class (lines 530-534) + tuples_conf = model.get_tuples_with_conf_for_class(MLBoxType.CRYSTAL) assert len(tuples_conf) == 1 assert tuples_conf[0] == (1, 2, 3, 4, 0.8) - - # Test get_class_str (lines 470, 475-481) + assert MLOutputModel.get_class_str(MLBoxType.LOOP_ALL) == "Loop_all" assert MLOutputModel.get_class_str(MLBoxType.PIN) == "Pin" assert MLOutputModel.get_class_str(MLBoxType.CRYSTAL) == "Crystal" @@ -92,9 +105,8 @@ def test_ml_output_model_extra_methods(): assert MLOutputModel.get_class_str(MLBoxType.NEEDLE) == "Needle" assert MLOutputModel.get_class_str(100) == "Unknown" + def test_beamline_state_enum_display_name(): - # Test display_name (line 556) assert BeamlineStateEnum.SampleExchange.display_name() == "Sample exchange" assert BeamlineStateEnum.Moving.display_name() == "Moving" - # Using value for potentially unknown - assert BeamlineStateEnum.display_name(None) == "-" + assert BeamlineStateEnum.display_name(None) == "-" \ No newline at end of file diff --git a/tests/unit/daq/test_auth.py b/tests/unit/daq/test_auth.py index ac895b69..bbfc2f2b 100644 --- a/tests/unit/daq/test_auth.py +++ b/tests/unit/daq/test_auth.py @@ -50,25 +50,24 @@ def test_authenticate_user(mock_cfg): patch('os.getgrouplist') as mock_groups, \ patch('grp.getgrgid') as mock_grp, \ patch('aare.daq.auth.SECRET_KEY', 'test_secret'): - + mock_pwd.return_value.pw_name = "testuser" mock_pwd.return_value.pw_gid = 1000 mock_groups.return_value = [1000, 1001] - + def get_group(gid): m = MagicMock() - if gid == 1000: m.gr_name = "p12345" - else: m.gr_name = "unx-MXgroup" + if gid == 1000: + m.gr_name = "p12345" + else: + m.gr_name = "unx-MXgroup" return m - + mock_grp.side_effect = get_group - - form_data = MagicMock() - form_data.username = "testuser" - - token = authenticate_user(mock_cfg, form_data) + + token = authenticate_user(mock_cfg, "testuser") assert isinstance(token, str) - + payload = jwt.decode(token, 'test_secret', algorithms=["HS256"]) assert payload["sub"] == "testuser" assert "p12345" in payload["pgroups"] diff --git a/tests/unit/daq/test_server.py b/tests/unit/daq/test_server.py index e41b9c53..54862bc4 100644 --- a/tests/unit/daq/test_server.py +++ b/tests/unit/daq/test_server.py @@ -49,9 +49,13 @@ def test_omega_put(client): def test_login_success(client): - with patch("aare.daq.auth.authenticate_user") as mock_auth: - mock_auth.return_value = "fake-access-token" - response = client.post("/token", data={"username": "user", "password": "pwd"}) + with patch("aare.daq.auth.authenticate_from_proxy_header", return_value="user"), \ + patch("aare.daq.auth.authenticate_user", return_value="fake-access-token"): + response = client.post( + "/token", + data={"username": "user", "password": "pwd"}, + headers={"X-Remote-User": "user"}, + ) assert response.status_code == 200 assert response.json() == {"access_token": "fake-access-token", "token_type": "bearer"} diff --git a/tests/unit/daq/test_spreadsheetupdater.py b/tests/unit/daq/test_spreadsheetupdater.py index c747db4e..c96e9529 100644 --- a/tests/unit/daq/test_spreadsheetupdater.py +++ b/tests/unit/daq/test_spreadsheetupdater.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch from aare.daq.spreadsheetupdater import on_message, get_ws_headers, set_spreadsheet_in_redis from aare.common.models import SampleShortInfoList + @pytest.fixture def mock_config(): with patch('aare.daq.spreadsheetupdater.config') as mock: @@ -14,30 +15,28 @@ def mock_config(): mock._BeamlineConfig__bl = mock._BeamlineConfig__bl yield mock + def test_get_ws_headers_success(): with patch('os.getenv', return_value="secret"): headers = get_ws_headers() assert headers == ["X-Shared-Password: secret"] + def test_get_ws_headers_fail(): with patch('os.getenv', return_value=None): with pytest.raises(ValueError): get_ws_headers() + def test_set_spreadsheet_in_redis(mock_config): data = {"test": "data"} with patch('aare.daq.spreadsheetupdater.config') as mock_cfg_internal: - # Mocking BOTH possible name-mangled names for the client mock_client = MagicMock() mock_cfg_internal._BeamlineConfig__client = mock_client - mock_cfg_internal.client = mock_client # In case it's not mangled or mangled differently - - # Also need to mock where it's actually used: config.__client.set - # If I can't guess the mangling, I'll just check what attributes mock_cfg_internal has - + mock_cfg_internal.client = mock_client + set_spreadsheet_in_redis(data) - - # Check all mock attributes for a 'set' call + found = False for attr in dir(mock_cfg_internal): val = getattr(mock_cfg_internal, attr) @@ -46,6 +45,7 @@ def test_set_spreadsheet_in_redis(mock_config): break assert found or mock_client.set.called + def test_on_message_success(mock_config): message = json.dumps({ "samples": [ @@ -67,6 +67,9 @@ def test_on_message_success(mock_config): "position": 1, "priority": 1, "mount_count": 0, + "rotation_count": 0, + "raster_count": 0, + "screening_count": 0, "data_collection_parameters": {} } ] @@ -89,24 +92,26 @@ def test_on_message_success(mock_config): "position": 1, "priority": 1, "mount_count": 0, + "rotation_count": 0, + "raster_count": 0, + "screening_count": 0, "data_collection_parameters": {} } ] } ] }) - + on_message(None, message) - - # Check if normal spreadsheet was written + normal_key = "X10SA:sample_spreadsheet" calls = mock_config._BeamlineConfig__client.set.call_args_list assert any(call.args[0] == normal_key for call in calls) - - # Check if reference tools were written + ref_key = "X10SA:reference-tools" assert any(call.args[0] == ref_key for call in calls) + def test_on_message_empty_ref(mock_config): message = json.dumps({ "samples": [ @@ -123,14 +128,13 @@ def test_on_message_empty_ref(mock_config): } ] }) - + on_message(None, message) - - # Check if reference tools were deleted + ref_key = "X10SA:reference-tools" mock_config._BeamlineConfig__client.delete.assert_called_with(ref_key) + def test_on_message_invalid_json(mock_config): - # Should not raise exception, just print error on_message(None, "invalid json") - mock_config._BeamlineConfig__client.set.assert_not_called() + mock_config._BeamlineConfig__client.set.assert_not_called() \ No newline at end of file diff --git a/tests/unit/gui/test_auth_mock.py b/tests/unit/gui/test_auth_mock.py index 29c400ad..91e908b2 100644 --- a/tests/unit/gui/test_auth_mock.py +++ b/tests/unit/gui/test_auth_mock.py @@ -1,72 +1,44 @@ +import json +import subprocess + import pytest -import requests + from aare.gui.auth import auth -# pytest-mock provides the 'mocker' fixture, which is a wrapper around the -# standard unittest.mock. It simplifies mocking by automatically handling -# cleanup (unpatching) after each test, and providing a more "pytest-native" -# feel compared to using @patch decorators or context managers. def test_auth_success(mocker): - """ - Test successful authentication using pytest-mock's mocker fixture. - - In standard pytest/unittest, you would typically use: - with mock.patch('requests.post') as mock_post: - ... - Or a decorator: - @patch('requests.post') - def test_auth(mock_post): - ... - - pytest-mock allows you to use the 'mocker' fixture directly in the function arguments. - This avoids deeply nested context managers and makes it easier to mock multiple things. - """ - - # We mock 'requests.post' to simulate a successful server response. - # mocker.patch returns a MagicMock object. - mock_post = mocker.patch("requests.post") - - # Configure the mock response - mock_response = mocker.Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"access_token": "fake_token_abc.123.xyz"} - mock_post.return_value = mock_response - - # Call the function under test + mock_run = mocker.patch("aare.gui.auth.subprocess.run") + mock_run.return_value = mocker.Mock( + returncode=0, + stdout=json.dumps({"access_token": "fake_token_abc.123.xyz"}), + stderr="", + ) + token = auth("http://test-server") - - # Verify the results + assert token == "fake_token_abc.123.xyz" - mock_post.assert_called_once() - - # Check that it was called with the expected URL - args, kwargs = mock_post.call_args - assert args[0] == "http://test-server/token" + mock_run.assert_called_once() + args, kwargs = mock_run.call_args + assert "curl" in args[0] + assert "http://test-server/token" in args[0] + def test_auth_network_failure(mocker): - """ - Test authentication failure due to network error using mocker. - """ - # Mock requests.post to raise an exception - mock_post = mocker.patch("requests.post") - mock_post.side_effect = requests.RequestException("Connection refused") - + mock_run = mocker.patch("aare.gui.auth.subprocess.run") + mock_run.side_effect = OSError("Connection refused") + with pytest.raises(RuntimeError) as excinfo: auth("http://test-server") - + assert "Cannot reach AareDAQ server" in str(excinfo.value) + def test_auth_no_url_returns_dummy_jwt(mocker): - """ - Test that when base_url is None, it returns a dummy JWT without network calls. - We can use mocker to verify that requests.post was NEVER called. - """ - mock_post = mocker.patch("requests.post") - mocker.patch("os.getlogin", return_value="testuser") - + mock_run = mocker.patch("aare.gui.auth.subprocess.run") + mocker.patch("aare.gui.auth.get_user", return_value="testuser") + token = auth(None) - + assert isinstance(token, str) - assert token.count('.') == 2 # Basic JWT structure check - mock_post.assert_not_called() + assert token.count('.') == 2 + mock_run.assert_not_called() \ No newline at end of file From 860cedfa1a49648f560e5d72f6725a5a7325882d Mon Sep 17 00:00:00 2001 From: appleb_m Date: Wed, 10 Jun 2026 13:17:59 +0200 Subject: [PATCH 24/28] GUI: added raster rotation and screening counts to GUI tell panel adn reference tools panel --- src/aare/gui/models/user_sample_model.py | 9 +++++++++ src/aare/gui/panels/reference_tools_panel.py | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/aare/gui/models/user_sample_model.py b/src/aare/gui/models/user_sample_model.py index 5f2e5dd6..e9163eb2 100644 --- a/src/aare/gui/models/user_sample_model.py +++ b/src/aare/gui/models/user_sample_model.py @@ -22,6 +22,12 @@ def get_entry(sample: SampleShortInfo, column: int): elif column == 6: return sample.mount_count elif column == 7: + return sample.raster_count + elif column == 8: + return sample.rotation_count + elif column == 9: + return sample.screening_count + elif column == 10: return sample.comment @@ -46,6 +52,9 @@ class UserSampleSpreadsheet(QAbstractTableModel): "Priority", "User", "Mount count", + "Raster count", + "Rotation count", + "Screening count", "Comment", ] self.current_sample = current_sample diff --git a/src/aare/gui/panels/reference_tools_panel.py b/src/aare/gui/panels/reference_tools_panel.py index b21807b4..03cbd1d8 100644 --- a/src/aare/gui/panels/reference_tools_panel.py +++ b/src/aare/gui/panels/reference_tools_panel.py @@ -27,6 +27,12 @@ def get_entry(sample: SampleShortInfo, column: int): return sample.sample_name elif column == 2: return sample.mount_count + elif column == 3: + return sample.rotation_count + elif column == 4: + return sample.raster_count + elif column == 5: + return sample.screening_count return "" class ReferenceToolsModel(QAbstractTableModel): @@ -35,7 +41,7 @@ class ReferenceToolsModel(QAbstractTableModel): self.samples: list[SampleShortInfo] = rows or [] self.current_reference = current_reference - self.header = ["Position", "Sample name", "Mount count"] + self.header = ["Position", "Sample name", "Mount count", "Raster count", "Rotation count", "Screening count",] self.__sort_col = 0 self.__sort_order = Qt.SortOrder.AscendingOrder self.__sorted_samples: list[SampleShortInfo] = [] From 01a656d956f6af2711e845e54a25822d61023f51 Mon Sep 17 00:00:00 2001 From: appleb_m Date: Wed, 10 Jun 2026 13:27:10 +0200 Subject: [PATCH 25/28] auth: added additioanl exception tracking, updated x10sa yaml --- src/aare/gui/auth.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 379433b0..3bbe26bb 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -47,6 +47,11 @@ def auth(base_url: str | None, cert_path: str | None) -> str: "Cannot reach AareDAQ server (OS error). " "Please check the server is running and your connection." ) from e + except Exception as e: + logger.error(f"Token request curl error: {e}") + raise RuntimeError( + "Cannot reach AareDAQ server (unknown error). " + ) if token_result.returncode != 0: logger.error(f"Token curl exited {token_result.returncode}. stderr: {token_result.stderr[:500]}") @@ -66,7 +71,7 @@ def auth(base_url: str | None, cert_path: str | None) -> str: token = response_json.get("access_token") if not token or not isinstance(token, str): - logger.error(f"Missing access_token. Keys: {list(response_json.keys())}") + logger.error(f"Missing access_token. Keys: {list(response_json.keys())}. Server response {response_json}") raise RuntimeError( "Authentication failed (missing token in server response). " "The server may be starting up." From a5683c9728b5e8b435ab5d486afb370926f19feb Mon Sep 17 00:00:00 2001 From: appleb_m Date: Wed, 10 Jun 2026 14:50:32 +0200 Subject: [PATCH 26/28] gui: made certPath command line option unique --- src/aare/gui/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aare/gui/gui.py b/src/aare/gui/gui.py index aceb5334..bd50867e 100644 --- a/src/aare/gui/gui.py +++ b/src/aare/gui/gui.py @@ -83,7 +83,7 @@ def main(): "url", default_url) parser.addOption(urlOption) - certPath = QCommandLineOption(["u", "aaredaq-cert-path"], + certPath = QCommandLineOption(["c", "aaredaq-cert-path"], "Server Certificate Path (for self-signed certificates)", "cert", default_cert_path) parser.addOption(certPath) From 22b8f15fd594fe72645544d7744496379c3bada1 Mon Sep 17 00:00:00 2001 From: appleb_m Date: Wed, 10 Jun 2026 15:06:45 +0200 Subject: [PATCH 27/28] gui: updated autoamtion view and added mode switrching buttons --- src/aare/gui/main_window.py | 90 ++++++-- .../gui/panels/compact_automation_panel.py | 202 ++++++++++-------- src/aare/gui/widgets/automation_progress.py | 46 ++-- 3 files changed, 210 insertions(+), 128 deletions(-) diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 1af8a7f0..648e820f 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -106,6 +106,13 @@ class MainWindow(QMainWindow): self._controls_help_dialog = None self._cleanup_done = False self._default_window_state = None + self._pre_automation_window_state = None + self._pre_automation_ref_tools_visible = False + self._pre_automation_left_column_visible = True + self._pre_automation_right_column_visible = True + self._in_compact_automation_view = False + self._enter_automation_view_action = None + self._return_main_view_action = None self._beamline_cam_addr = beamline_cam_addr self._gonio_cam_addr = gonio_cam_addr @@ -199,7 +206,7 @@ class MainWindow(QMainWindow): self.raster = RasterGridManager(geom=geom) self.rotation = RotationScanManager() - collection_controls_scroll = NoWheelScrollArea(top_widget) + self.collection_controls_scroll = NoWheelScrollArea(top_widget) self.left_column = QWidget(parent=top_widget) self.left_column_layout = QVBoxLayout(self.left_column) @@ -226,13 +233,13 @@ class MainWindow(QMainWindow): self.beamline_state_panel.hide() self.left_column_layout.addStretch() - top_widget_layout.addWidget(collection_controls_scroll) - collection_controls_scroll.setWidget(self.left_column) - collection_controls_scroll.setHorizontalScrollBarPolicy( + top_widget_layout.addWidget(self.collection_controls_scroll) + self.collection_controls_scroll.setWidget(self.left_column) + self.collection_controls_scroll.setHorizontalScrollBarPolicy( Qt.ScrollBarPolicy.ScrollBarAlwaysOff ) - collection_controls_scroll.setWidgetResizable(True) - collection_controls_scroll.setFixedWidth( + self.collection_controls_scroll.setWidgetResizable(True) + self.collection_controls_scroll.setFixedWidth( max( self.data_collection.set_width, self.loop_centering.sizeHint().width(), @@ -295,7 +302,7 @@ class MainWindow(QMainWindow): self.compact_automation_page.setStyleSheet( """ QWidget#compactAutomationPage { - background-color: #e8eefc; + background-color: rgb(216, 228, 253); } """ ) @@ -307,15 +314,15 @@ class MainWindow(QMainWindow): top_widget_layout.addWidget(self.video_tab) self._start_axis_camera_threads() - beamline_controls_scroll = NoWheelScrollArea(top_widget) + self.beamline_controls_scroll = NoWheelScrollArea(top_widget) - self.beamline = BeamlineControls(beamline_controls_scroll, staff=self.__decoded_token.staff) - top_widget_layout.addWidget(beamline_controls_scroll) - beamline_controls_scroll.setWidget(self.beamline) - beamline_controls_scroll.setHorizontalScrollBarPolicy( + self.beamline = BeamlineControls(self.beamline_controls_scroll, staff=self.__decoded_token.staff) + top_widget_layout.addWidget(self.beamline_controls_scroll) + self.beamline_controls_scroll.setWidget(self.beamline) + self.beamline_controls_scroll.setHorizontalScrollBarPolicy( Qt.ScrollBarPolicy.ScrollBarAlwaysOff ) - beamline_controls_scroll.setFixedWidth(self.beamline.set_width + 10) + self.beamline_controls_scroll.setFixedWidth(self.beamline.set_width + 10) self.tell_samples = TellSamplePanel(samples=SampleShortInfoList(s=[])) self.ref_tools_panel = ReferenceToolsPanel(samples=SampleShortInfoList(s=[])) @@ -478,6 +485,7 @@ class MainWindow(QMainWindow): self.setWindowTitle("AareGUI") self.create_menu_bar() + self._update_view_mode_actions() self._setup_global_shortcuts() self._capture_default_window_state() self._restore_window_state() @@ -916,6 +924,17 @@ class MainWindow(QMainWindow): key=lambda sample: sample.loc_str_sort(), ) + def _update_view_mode_actions(self) -> None: + in_automation_view = bool(self._in_compact_automation_view) + + if self._enter_automation_view_action is not None: + self._enter_automation_view_action.setVisible(not in_automation_view) + self._enter_automation_view_action.setEnabled(not in_automation_view) + + if self._return_main_view_action is not None: + self._return_main_view_action.setVisible(in_automation_view) + self._return_main_view_action.setEnabled(in_automation_view) + @Slot() def enter_compact_automation_view(self) -> None: self.job_list_panel.ensure_default_queue_from_samples( @@ -923,6 +942,12 @@ class MainWindow(QMainWindow): ) self._refresh_compact_queue_preview() + if not self._in_compact_automation_view: + self._pre_automation_window_state = self.saveState() + self._pre_automation_ref_tools_visible = self.ref_tools_dock.isVisible() + self._pre_automation_left_column_visible = self.collection_controls_scroll.isVisible() + self._pre_automation_right_column_visible = self.beamline_controls_scroll.isVisible() + self.tell_samples_dock.setVisible(False) self.job_list_dock.setVisible(False) self.manual_sample_dock.setVisible(False) @@ -937,12 +962,29 @@ class MainWindow(QMainWindow): if self.__decoded_token.staff: self.ref_tools_dock.setVisible(False) + self.collection_controls_scroll.setVisible(False) + self.beamline_controls_scroll.setVisible(False) + self.content_stack.setCurrentWidget(self.compact_automation_page) + self._in_compact_automation_view = True + self._update_view_mode_actions() @Slot() def _return_from_compact_automation_view(self) -> None: self.content_stack.setCurrentWidget(self._standard_main_page) - self.restore_default_view() + + if self._pre_automation_window_state is not None: + self.restoreState(self._pre_automation_window_state) + + self.collection_controls_scroll.setVisible(self._pre_automation_left_column_visible) + self.beamline_controls_scroll.setVisible(self._pre_automation_right_column_visible) + + if self.__decoded_token.staff: + self.ref_tools_dock.setVisible(self._pre_automation_ref_tools_visible) + + self._in_compact_automation_view = False + self._update_view_mode_actions() + self.job_list_dock.setVisible(True) self.tell_samples_dock.setVisible(True) self.job_list_dock.raise_() @@ -1025,6 +1067,16 @@ class MainWindow(QMainWindow): quit_action.triggered.connect(self.close) file_menu.addAction(quit_action) + self._enter_automation_view_action = QAction("Automation View", self) + self._enter_automation_view_action.setShortcut(QKeySequence("Ctrl+5")) + self._enter_automation_view_action.triggered.connect(self.enter_compact_automation_view) + menu_bar.addAction(self._enter_automation_view_action) + + self._return_main_view_action = QAction("Return to Main View", self) + self._return_main_view_action.setShortcut(QKeySequence("Ctrl+Shift+5")) + self._return_main_view_action.triggered.connect(self._return_from_compact_automation_view) + menu_bar.addAction(self._return_main_view_action) + view_menu = menu_bar.addMenu("View") if self._beamline_state_panel_enabled: @@ -1138,11 +1190,6 @@ class MainWindow(QMainWindow): beamline_combined_tab_action.triggered.connect(lambda: self.video_tab.setCurrentIndex(3)) view_menu.addAction(beamline_combined_tab_action) - compact_automation_action = QAction("Automation View", self) - compact_automation_action.setShortcut(QKeySequence("Ctrl+5")) - compact_automation_action.triggered.connect(self.enter_compact_automation_view) - view_menu.addAction(compact_automation_action) - view_menu.addSeparator() restore_default_view_action = QAction("Restore Default View", self) @@ -1194,6 +1241,11 @@ class MainWindow(QMainWindow): self.restoreState(self._default_window_state) self.content_stack.setCurrentWidget(self._standard_main_page) + self._in_compact_automation_view = False + self._update_view_mode_actions() + + self.collection_controls_scroll.setVisible(True) + self.beamline_controls_scroll.setVisible(True) self.tell_samples_dock.setVisible(True) self.job_list_dock.setVisible(True) diff --git a/src/aare/gui/panels/compact_automation_panel.py b/src/aare/gui/panels/compact_automation_panel.py index 3504ed77..3c64887f 100644 --- a/src/aare/gui/panels/compact_automation_panel.py +++ b/src/aare/gui/panels/compact_automation_panel.py @@ -37,48 +37,52 @@ class CompactAutomationPanel(QFrame): self.setStyleSheet( """ QFrame#compactAutomationPanel { - background: #edf3fb; + background: #d8e4fd; border: none; border-radius: 18px; } - QFrame#compactTopCard, + QFrame#compactCameraCard, + QFrame#compactControlsCard, QFrame#compactProgressCard, - QFrame#compactControlsCard { - background: #ffffff; - border: 1px solid #d9e4f2; + QFrame#compactQueueCard, + QFrame#compactQueueItem { + background: #e6eefc; + border: 1px solid #b9ccee; border-radius: 16px; } - QFrame#compactQueueItem { - background: #f7faff; - border: 1px solid #dbe6f5; - border-radius: 12px; - } - - QLabel#compactQueueTitle { - color: #4b5563; - font-size: 11px; - font-weight: 700; - text-transform: uppercase; - } - - QLabel#compactQueueValue { - color: #0f172a; + QLabel#compactSectionTitle { + background: transparent; + color: #17324d; font-size: 14px; font-weight: 700; } - QLabel#compactDashboardLabel { - color: #334155; + QLabel#compactSectionHint { + background: transparent; + color: #51657d; font-size: 12px; + } + + QLabel#compactQueueTitle { + background: transparent; + color: #51657d; + font-size: 11px; + font-weight: 700; + } + + QLabel#compactQueueValue { + background: transparent; + color: #10263a; + font-size: 14px; font-weight: 700; } QToolButton#compactMenuButton { - background: #eff6ff; - color: #1d4ed8; - border: 1px solid #bfdbfe; + background: #cbdcf8; + color: #17324d; + border: 1px solid #9fb9e5; border-radius: 14px; padding: 10px 14px; font-size: 18px; @@ -86,23 +90,19 @@ class CompactAutomationPanel(QFrame): } QToolButton#compactMenuButton:hover { - background: #dbeafe; - } - - QFrame#compactAutomationProgressStrip { - background: transparent; - border: none; + background: #bfd4f6; } QLabel#compactProgressSummary { - color: #1e293b; + background: transparent; + color: #17324d; font-size: 13px; padding: 2px 2px 6px 2px; } QLabel#compactProgressStep { - background: #f8fbff; - border: 1px solid #dbe6f5; + background: #dfe9fb; + border: 1px solid #bfd1ef; border-radius: 10px; padding: 8px 6px; } @@ -121,10 +121,11 @@ class CompactAutomationPanel(QFrame): background: #1d4ed8; } - QPushButton#compactSecondaryButton { - background: #ffffff; - color: #0f172a; - border: 1px solid #cbd5e1; + QPushButton#compactSecondaryButton, + QToolButton#compactSecondaryButton { + background: #dfe9fb; + color: #17324d; + border: 1px solid #b2c7eb; border-radius: 14px; padding: 14px 18px; font-size: 14px; @@ -133,17 +134,7 @@ class CompactAutomationPanel(QFrame): QPushButton#compactSecondaryButton:hover, QToolButton#compactSecondaryButton:hover { - background: #f8fafc; - } - - QToolButton#compactSecondaryButton { - background: #ffffff; - color: #0f172a; - border: 1px solid #cbd5e1; - border-radius: 14px; - padding: 14px 18px; - font-size: 14px; - font-weight: 700; + background: #d3e1f8; } """ ) @@ -152,44 +143,18 @@ class CompactAutomationPanel(QFrame): main_layout.setContentsMargins(18, 18, 18, 18) main_layout.setSpacing(12) - top_card = QFrame(self) - top_card.setObjectName("compactTopCard") - top_layout = QHBoxLayout(top_card) - top_layout.setContentsMargins(12, 12, 12, 12) - top_layout.setSpacing(10) + camera_card = QFrame(self) + camera_card.setObjectName("compactCameraCard") + camera_layout = QVBoxLayout(camera_card) + camera_layout.setContentsMargins(12, 12, 12, 12) + camera_layout.setSpacing(8) - self.current_card = self._build_queue_item("Current", "—") - self.next_card = self._build_queue_item("Next", "—") - self.next_next_card = self._build_queue_item("Then", "—") + camera_title = QLabel("Camera", self) + camera_title.setObjectName("compactSectionTitle") + camera_layout.addWidget(camera_title) + camera_layout.addWidget(camera_widget, 1) - top_layout.addWidget(self.current_card["frame"], 2) - top_layout.addWidget(self.next_card["frame"], 2) - top_layout.addWidget(self.next_next_card["frame"], 2) - - self.menu_button = QToolButton(self) - self.menu_button.setObjectName("compactMenuButton") - self.menu_button.setText("☰") - self.menu_button.setToolTip("Return to main view") - self.menu_button.clicked.connect(self.show_full_view_requested.emit) - top_layout.addWidget(self.menu_button, 0, Qt.AlignmentFlag.AlignTop) - - main_layout.addWidget(top_card) - main_layout.addWidget(camera_widget, 1) - - progress_card = QFrame(self) - progress_card.setObjectName("compactProgressCard") - progress_layout = QVBoxLayout(progress_card) - progress_layout.setContentsMargins(12, 10, 12, 10) - progress_layout.setSpacing(6) - - progress_title = QLabel("Automation progress", self) - progress_title.setObjectName("compactDashboardLabel") - progress_layout.addWidget(progress_title) - - self.progress_strip = CompactAutomationProgressStrip(self) - progress_layout.addWidget(self.progress_strip) - - main_layout.addWidget(progress_card) + main_layout.addWidget(camera_card, 1) controls_card = QFrame(self) controls_card.setObjectName("compactControlsCard") @@ -197,9 +162,31 @@ class CompactAutomationPanel(QFrame): controls_layout.setContentsMargins(12, 12, 12, 12) controls_layout.setSpacing(10) + controls_header = QHBoxLayout() + controls_header.setSpacing(10) + + controls_title_wrap = QVBoxLayout() + controls_title_wrap.setContentsMargins(0, 0, 0, 0) + controls_title_wrap.setSpacing(2) + controls_title = QLabel("Controls", self) - controls_title.setObjectName("compactDashboardLabel") - controls_layout.addWidget(controls_title) + controls_title.setObjectName("compactSectionTitle") + controls_hint = QLabel("Run and manage the automation queue.", self) + controls_hint.setObjectName("compactSectionHint") + + controls_title_wrap.addWidget(controls_title) + controls_title_wrap.addWidget(controls_hint) + + self.menu_button = QToolButton(self) + self.menu_button.setObjectName("compactMenuButton") + self.menu_button.setText("☰") + self.menu_button.setToolTip("Return to main view") + self.menu_button.clicked.connect(self.show_full_view_requested.emit) + + controls_header.addLayout(controls_title_wrap, 1) + controls_header.addWidget(self.menu_button, 0, Qt.AlignmentFlag.AlignTop) + + controls_layout.addLayout(controls_header) button_row = QHBoxLayout() button_row.setSpacing(10) @@ -212,7 +199,7 @@ class CompactAutomationPanel(QFrame): self.skip_button.setObjectName("compactSecondaryButton") self.skip_button.clicked.connect(self.skip_clicked.emit) - self.step_button = QPushButton("Step Through", self) + self.step_button = QPushButton("Step", self) self.step_button.setObjectName("compactSecondaryButton") self.step_button.setCheckable(True) self.step_button.toggled.connect(self._on_step_toggled) @@ -238,6 +225,45 @@ class CompactAutomationPanel(QFrame): controls_layout.addLayout(button_row) main_layout.addWidget(controls_card) + progress_card = QFrame(self) + progress_card.setObjectName("compactProgressCard") + progress_layout = QVBoxLayout(progress_card) + progress_layout.setContentsMargins(12, 12, 12, 12) + progress_layout.setSpacing(8) + + progress_title = QLabel("Progress", self) + progress_title.setObjectName("compactSectionTitle") + progress_layout.addWidget(progress_title) + + self.progress_strip = CompactAutomationProgressStrip(self) + progress_layout.addWidget(self.progress_strip) + + main_layout.addWidget(progress_card) + + queue_card = QFrame(self) + queue_card.setObjectName("compactQueueCard") + queue_layout = QVBoxLayout(queue_card) + queue_layout.setContentsMargins(12, 12, 12, 12) + queue_layout.setSpacing(10) + + queue_title = QLabel("Queue Preview", self) + queue_title.setObjectName("compactSectionTitle") + queue_layout.addWidget(queue_title) + + queue_row = QHBoxLayout() + queue_row.setSpacing(10) + + self.current_card = self._build_queue_item("Now", "—") + self.next_card = self._build_queue_item("Next", "—") + self.next_next_card = self._build_queue_item("Later", "—") + + queue_row.addWidget(self.current_card["frame"], 1) + queue_row.addWidget(self.next_card["frame"], 1) + queue_row.addWidget(self.next_next_card["frame"], 1) + + queue_layout.addLayout(queue_row) + main_layout.addWidget(queue_card) + def _build_queue_item(self, title: str, value: str) -> dict[str, QWidget | QLabel]: frame = QFrame(self) frame.setObjectName("compactQueueItem") diff --git a/src/aare/gui/widgets/automation_progress.py b/src/aare/gui/widgets/automation_progress.py index 2803fa0a..c700bf9c 100644 --- a/src/aare/gui/widgets/automation_progress.py +++ b/src/aare/gui/widgets/automation_progress.py @@ -18,16 +18,24 @@ class CompactAutomationProgressStrip(QFrame): self._queue_count = 0 self._running = False self._step_labels: dict[WorkflowStateKind, QLabel] = {} - self._summary_label: QLabel | None = None + self._timer = QTimer(self) self._timer.setInterval(1000) self._timer.timeout.connect(self._refresh_live_view) self.setObjectName("compactAutomationProgressStrip") + self.setStyleSheet( + """ + QFrame#compactAutomationProgressStrip { + background: transparent; + border: none; + } + """ + ) layout = QVBoxLayout(self) - layout.setContentsMargins(14, 12, 14, 12) - layout.setSpacing(10) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(8) self._summary_label = QLabel("Automation idle", self) self._summary_label.setObjectName("compactProgressSummary") @@ -46,7 +54,6 @@ class CompactAutomationProgressStrip(QFrame): ): label = QLabel(self) label.setObjectName("compactProgressStep") - label.setMinimumWidth(90) label.setWordWrap(True) steps_row.addWidget(label, 1) self._step_labels[step] = label @@ -55,8 +62,7 @@ class CompactAutomationProgressStrip(QFrame): self._apply_empty_state() def _apply_empty_state(self) -> None: - if self._summary_label is not None: - self._summary_label.setText("Automation idle") + self._summary_label.setText("Automation idle") for step, label in self._step_labels.items(): label.setText(self._format_step_html(step, StepStatus.PENDING)) @@ -97,9 +103,9 @@ class CompactAutomationProgressStrip(QFrame): icon = self._step_icon(status) title = self._step_title(step) return ( - f"
" - f"
{icon}
" - f"
{title}
" + f"
" + f"
{icon}
" + f"
{title}
" f"
" ) @@ -120,6 +126,7 @@ class CompactAutomationProgressStrip(QFrame): return "N/A" return datetime.fromtimestamp(epoch_seconds).strftime("%H:%M:%S") + @Slot() def _refresh_live_view(self) -> None: if self._progress is not None: self.set_progress(self._progress) @@ -163,19 +170,16 @@ class CompactAutomationProgressStrip(QFrame): elif self._running: state_text = "Running" state_color = "#2563eb" - elif not self._running: - state_text = "Paused" - state_color = "#c2410c" - if self._summary_label is not None: - self._summary_label.setText( - f"Status: {state_text}" - f"    Current: {current_sample}" - f"    Queue: {self._queue_count}" - f"    ETA: {self._format_duration(queue_remaining)}" - f"    Done by: {self._format_eta(eta)}" - ) + self._summary_label.setText( + f"Status: " + f"{state_text}" + f"    Now: {current_sample}" + f"    Queue: {self._queue_count}" + f"    ETA: {self._format_duration(queue_remaining)}" + f"    Done: {self._format_eta(eta)}" + ) states = {step.step: step.status for step in progress.steps} for step, label in self._step_labels.items(): - label.setText(self._format_step_html(step, states.get(step, StepStatus.PENDING))) + label.setText(self._format_step_html(step, states.get(step, StepStatus.PENDING))) \ No newline at end of file From 2a3b09e7196323b835125d1070ec8e67b002eb1a Mon Sep 17 00:00:00 2001 From: appleb_m Date: Wed, 10 Jun 2026 15:06:56 +0200 Subject: [PATCH 28/28] banner: added banner for spalsh screen --- src/aare/gui/graphics/aare_banner.png | Bin 0 -> 120708 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/aare/gui/graphics/aare_banner.png diff --git a/src/aare/gui/graphics/aare_banner.png b/src/aare/gui/graphics/aare_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..831224d281024929577273db2469291809e923ca GIT binary patch literal 120708 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 Xiu~;?NyH6Y00000NkvXXu0mjfRXWap literal 0 HcmV?d00001