From 4b94416ef33f6dbd0d6cea42f2b5f639fc85c2da Mon Sep 17 00:00:00 2001 From: pollutri Date: Fri, 27 Feb 2026 11:25:27 +0100 Subject: [PATCH] API fails to authenticate --- app.py | 21 ++++++++++++ config_env.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++----- security.py | 8 +++++ 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/app.py b/app.py index 9e0e557..5ff3837 100644 --- a/app.py +++ b/app.py @@ -329,6 +329,27 @@ async def get_ble_ai_maps(): return {"items": items, "count": len(items)} +@app.get("/ble-ai/maps/{filename}", tags=["BLE-AI"], dependencies=[Depends(get_current_user)]) +async def download_ble_ai_map(filename: str): + maps_dir = config_env.BLE_AI_MAPS_DIR + if not os.path.isdir(maps_dir): + raise HTTPException(status_code=404, detail="Maps directory not found") + + safe_name = os.path.basename(filename) + if safe_name != filename or not safe_name.lower().endswith(".png"): + raise HTTPException(status_code=400, detail="Invalid map filename") + + file_path = os.path.join(maps_dir, safe_name) + if not os.path.isfile(file_path): + raise HTTPException(status_code=404, detail="Map file not found") + + return FileResponse( + path=file_path, + filename=safe_name, + media_type="image/png", + ) + + @app.get("/openapi.json/", tags=["Documentation"]) async def get_open_api_endpoint(): #async def get_open_api_endpoint(current_user: User = Depends(get_current_active_user)): diff --git a/config_env.py b/config_env.py index e863c69..7988ac4 100644 --- a/config_env.py +++ b/config_env.py @@ -1,18 +1,90 @@ -#This file reads the .env where the variables should be stored import os from dotenv import load_dotenv load_dotenv() -#Keycloak configuration (look in the .env) + +def _clean(value: str) -> str: + if value is None: + return "" + return str(value).strip().strip('"').strip("'") + + +def _has_http_scheme(value: str) -> bool: + return value.startswith("http://") or value.startswith("https://") + + +def _ensure_http_scheme(value: str, default_scheme: str = "https") -> str: + value = _clean(value) + if not value: + return "" + if _has_http_scheme(value): + return value + if value.startswith("/"): + return value + return f"{default_scheme}://{value}" + + +def _absolute_or_join(value: str, base_url: str) -> str: + value = _clean(value) + if not value: + return "" + if _has_http_scheme(value): + return value + if value.startswith("/") and base_url: + return f"{base_url.rstrip('/')}{value}" + return _ensure_http_scheme(value) + + +# Keycloak configuration (look in the .env) SECRET = os.getenv("SECRET") KEYCLOAK_AUDIENCE = os.getenv("KEYCLOAK_AUDIENCE") -KEYCLOAK_SERVER = os.getenv("KEYCLOAK_SERVER") -KEYCLOAK_ISSUER = os.getenv("KEYCLOAK_ISSUER") -KEYCLOAK_PROTOCOL_ENDPOINT = os.getenv("KEYCLOAK_PROTOCOL_ENDPOINT") -KEYCLOAK_JWKS_URL = os.getenv("KEYCLOAK_JWKS_URL") -KEYCLOAK_AUTH_URL = os.getenv("KEYCLOAK_AUTH_URL") -KEYCLOAK_TOKEN_URL = os.getenv("KEYCLOAK_TOKEN_URL") + +_raw_keycloak_server = _clean(os.getenv("KEYCLOAK_SERVER")) +KEYCLOAK_SERVER = _ensure_http_scheme(_raw_keycloak_server) + +_default_realm = _clean(os.getenv("KEYCLOAK_REALM")) or "API.Server.local" + +_raw_keycloak_issuer = _clean(os.getenv("KEYCLOAK_ISSUER")) +if _raw_keycloak_issuer and "${" not in _raw_keycloak_issuer: + KEYCLOAK_ISSUER = _absolute_or_join(_raw_keycloak_issuer, KEYCLOAK_SERVER) +elif KEYCLOAK_SERVER: + KEYCLOAK_ISSUER = f"{KEYCLOAK_SERVER.rstrip('/')}/realms/{_default_realm}" +else: + KEYCLOAK_ISSUER = "" + +_raw_keycloak_protocol = _clean(os.getenv("KEYCLOAK_PROTOCOL_ENDPOINT")) +if _raw_keycloak_protocol and "${" not in _raw_keycloak_protocol: + KEYCLOAK_PROTOCOL_ENDPOINT = _absolute_or_join(_raw_keycloak_protocol, KEYCLOAK_SERVER) +elif KEYCLOAK_ISSUER: + KEYCLOAK_PROTOCOL_ENDPOINT = f"{KEYCLOAK_ISSUER.rstrip('/')}/protocol/openid-connect" +else: + KEYCLOAK_PROTOCOL_ENDPOINT = "" + +_raw_jwks = _clean(os.getenv("KEYCLOAK_JWKS_URL")) +if _raw_jwks and "${" not in _raw_jwks: + KEYCLOAK_JWKS_URL = _absolute_or_join(_raw_jwks, KEYCLOAK_SERVER) +elif KEYCLOAK_PROTOCOL_ENDPOINT: + KEYCLOAK_JWKS_URL = f"{KEYCLOAK_PROTOCOL_ENDPOINT.rstrip('/')}/certs" +else: + KEYCLOAK_JWKS_URL = "" + +_raw_auth = _clean(os.getenv("KEYCLOAK_AUTH_URL")) +if _raw_auth and "${" not in _raw_auth: + KEYCLOAK_AUTH_URL = _absolute_or_join(_raw_auth, KEYCLOAK_SERVER) +elif KEYCLOAK_PROTOCOL_ENDPOINT: + KEYCLOAK_AUTH_URL = f"{KEYCLOAK_PROTOCOL_ENDPOINT.rstrip('/')}/auth" +else: + KEYCLOAK_AUTH_URL = "" + +_raw_token = _clean(os.getenv("KEYCLOAK_TOKEN_URL")) +if _raw_token and "${" not in _raw_token: + KEYCLOAK_TOKEN_URL = _absolute_or_join(_raw_token, KEYCLOAK_SERVER) +elif KEYCLOAK_PROTOCOL_ENDPOINT: + KEYCLOAK_TOKEN_URL = f"{KEYCLOAK_PROTOCOL_ENDPOINT.rstrip('/')}/token" +else: + KEYCLOAK_TOKEN_URL = "" + CORE_API_URL = os.getenv("CORE_API_URL", "http://localhost:1902") MQTT_HOST = os.getenv("MQTT_HOST", "192.168.1.101") diff --git a/security.py b/security.py index 790fcd6..1f0d26c 100644 --- a/security.py +++ b/security.py @@ -39,6 +39,14 @@ http_bearer = HTTPBearer(auto_error=True) async def _get_jwks() -> Dict[str, Any]: global _cached_jwks + if not KEYCLOAK_JWKS_URL or not ( + KEYCLOAK_JWKS_URL.startswith("http://") or KEYCLOAK_JWKS_URL.startswith("https://") + ): + logger.error("_get_jwks: invalid KEYCLOAK_JWKS_URL=%r", KEYCLOAK_JWKS_URL) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Invalid Keycloak JWKS URL configuration", + ) if _cached_jwks is None: logger.info(f"_get_jwks: cache miss, fetching from {KEYCLOAK_JWKS_URL}") resp = await _http.get(KEYCLOAK_JWKS_URL)