4 Commits

Author SHA1 Message Date
c086fa4260 hotfix(v1.7.11): add unsafe-eval to CSP for Alpine.js
All checks were successful
CI / lint-and-test (push) Successful in 1m26s
Release / build-and-push (push) Successful in 3m1s
2026-04-27 10:39:33 +02:00
be700fefc3 hotfix(v1.7.10): add font-src to CSP for data URI fonts
All checks were successful
CI / lint-and-test (push) Successful in 1m29s
Release / build-and-push (push) Successful in 2m53s
2026-04-27 10:32:35 +02:00
e2cea50d87 hotfix(v1.7.9): auth diagnostics and rate-limit exemptions
All checks were successful
CI / lint-and-test (push) Successful in 2m30s
Release / build-and-push (push) Successful in 4m46s
- Exempt /api/config/auth, /api/config/features, /health, /metrics from rate limiting
- Fix generic exception handler to return proper JSON for HTTPException instead of re-raising
- Add startup log with auth_enabled and version
- Add frontend console logging for auth config fetch errors
- Show 'Auth: OFF' or 'Auth: misconfigured' on auth button instead of empty text
- Add backend debug logging to /api/config/auth endpoint
2026-04-27 10:09:44 +02:00
7fe53f882a hotfix(v1.7.8): restore CORS wildcard and fix CSP for MSAL auth
All checks were successful
CI / lint-and-test (push) Successful in 51s
Release / build-and-push (push) Successful in 2m4s
- Revert automatic CORS wildcard stripping that broke production deployments
  with CORS_ORIGINS=* (now logs a warning but preserves the config)
- Expand CSP headers to allow MSAL auth flows:
  - connect-src: login.microsoftonline.com
  - frame-src: login.microsoftonline.com
  - form-action: login.microsoftonline.com
2026-04-27 09:41:28 +02:00
4 changed files with 50 additions and 19 deletions

View File

@@ -1 +1 @@
1.7.7
1.7.11

View File

@@ -591,9 +591,15 @@
async initAuth() {
try {
const res = await fetch('/api/config/auth');
this.authConfig = await res.json();
} catch {
this.authConfig = { auth_enabled: false };
if (!res.ok) {
console.error('Auth config fetch failed:', res.status, res.statusText);
this.authConfig = { auth_enabled: false, _error: res.status };
} else {
this.authConfig = await res.json();
}
} catch (err) {
console.error('Auth config fetch error:', err);
this.authConfig = { auth_enabled: false, _error: 'network' };
}
try {
@@ -614,7 +620,17 @@
}
if (!this.authConfig?.auth_enabled) {
this.authBtnText = '';
this.authBtnText = 'Auth: OFF';
console.warn('AOC auth is disabled. Set AUTH_ENABLED=true in .env to enable login.');
return;
}
const tenantId = this.authConfig.tenant_id;
const clientId = this.authConfig.client_id;
if (!clientId || !tenantId) {
this.authBtnText = 'Auth: misconfigured';
this.statusText = 'Auth is enabled but client_id or tenant_id is missing. Check .env configuration.';
console.error('AOC auth misconfigured: missing client_id or tenant_id in /api/config/auth');
return;
}
@@ -623,8 +639,6 @@
return;
}
const tenantId = this.authConfig.tenant_id;
const clientId = this.authConfig.client_id;
const baseScope = this.authConfig.scope || "";
this.authScopes = Array.from(new Set(['openid', 'profile', 'email', ...baseScope.split(/[ ,]+/).filter(Boolean)]));
const authority = `https://login.microsoftonline.com/${tenantId}`;

View File

@@ -1,5 +1,6 @@
import asyncio
import logging
import os
import time
from contextlib import suppress
from pathlib import Path
@@ -52,14 +53,12 @@ logger = structlog.get_logger("aoc.fetcher")
app = FastAPI()
# CORS: reject wildcard in production when auth is enabled
# CORS: warn if wildcard is used with auth enabled, but do not break deployments
_effective_cors = CORS_ORIGINS
if AUTH_ENABLED and "*" in _effective_cors:
logger.warning(
"CORS wildcard (*) is insecure when AUTH_ENABLED=true. "
"Removing wildcard. Set CORS_ORIGINS explicitly in production."
"CORS wildcard (*) is insecure when AUTH_ENABLED=true. Set CORS_ORIGINS to your actual origin(s) in production."
)
_effective_cors = [o for o in _effective_cors if o != "*"] or ["http://localhost:8000"]
app.add_middleware(CorrelationIdMiddleware)
app.add_middleware(
@@ -89,14 +88,17 @@ async def cache_control_middleware(request: Request, call_next):
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
# Basic CSP for the UI and API
# Basic CSP for the UI and API (allows MSAL auth flows)
if request.url.path.startswith("/api/") or request.url.path in ("/", "/index.html"):
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' cdn.jsdelivr.net alcdn.msauth.net; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval' cdn.jsdelivr.net alcdn.msauth.net; "
"style-src 'self' 'unsafe-inline'; "
"connect-src 'self'; "
"img-src 'self' data:;"
"connect-src 'self' https://login.microsoftonline.com; "
"frame-src 'self' https://login.microsoftonline.com; "
"form-action 'self' https://login.microsoftonline.com; "
"img-src 'self' data:; "
"font-src 'self' data:;"
)
return response
@@ -104,7 +106,9 @@ async def cache_control_middleware(request: Request, call_next):
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
"""Apply Redis-backed rate limiting before processing the request."""
if request.url.path.startswith("/api/"):
# Exempt config and health endpoints from rate limiting
exempt_paths = {"/api/config/auth", "/api/config/features", "/health", "/metrics"}
if request.url.path.startswith("/api/") and request.url.path not in exempt_paths:
from rate_limiter import check_rate_limit
await check_rate_limit(request)
@@ -168,8 +172,6 @@ async def metrics():
@app.get("/api/version")
async def version():
import os
return {"version": os.environ.get("VERSION", "unknown")}
@@ -177,7 +179,13 @@ async def version():
async def generic_exception_handler(request: Request, exc: Exception):
"""Return generic error messages for unhandled exceptions to avoid info leakage."""
if isinstance(exc, HTTPException):
raise exc
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
headers=getattr(exc, "headers", None) or {},
)
logger.error("Unhandled exception", path=request.url.path, error=str(exc))
return Response(
content='{"detail":"Internal server error"}',
@@ -206,6 +214,12 @@ async def start_periodic_fetch():
from rules import seed_default_rules
seed_default_rules()
logger.info(
"AOC startup",
version=os.environ.get("VERSION", "unknown"),
auth_enabled=AUTH_ENABLED,
ai_enabled=AI_FEATURES_ENABLED,
)
if ENABLE_PERIODIC_FETCH:
app.state.fetch_task = asyncio.create_task(_periodic_fetch())

View File

@@ -1,3 +1,4 @@
import structlog
from config import (
AI_FEATURES_ENABLED,
AUTH_CLIENT_ID,
@@ -9,10 +10,12 @@ from config import (
from fastapi import APIRouter
router = APIRouter()
logger = structlog.get_logger("aoc.config")
@router.get("/config/auth")
def auth_config():
logger.debug("Auth config requested", auth_enabled=AUTH_ENABLED)
return {
"auth_enabled": AUTH_ENABLED,
"tenant_id": AUTH_TENANT_ID,