add Apache authentication and token handling using curl in GUI and access log parsing in DAQ

This commit is contained in:
2026-06-09 16:16:11 +02:00
parent 8dc1ecacb2
commit 6351265698
3 changed files with 96 additions and 21 deletions
+34
View File
@@ -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.
+1 -1
View File
@@ -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"}
+61 -20
View File
@@ -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."