add Apache authentication and token handling using curl in GUI and access log parsing in DAQ
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user