feat: service-level role gating for privacy-sensitive services (Option A)
All checks were successful
CI / lint-and-test (push) Successful in 25s

- Add PRIVACY_SERVICES and PRIVACY_SERVICE_ROLES config variables
- Add user_can_access_privacy_services(claims) helper in auth.py
- /api/events filters out privacy services for users without required roles
- /api/filter-options excludes privacy services from dropdown options
- /api/ask excludes privacy services from NLQ queries
- /api/events/{id}/explain returns 403 for privacy events if unauthorized
- Teams added to default noisy service exclusion (frontend + backend)
- Update .env.example with privacy config documentation
- Add tests for event filtering, filter-options exclusion, and explain 403
This commit is contained in:
2026-04-22 07:26:21 +02:00
parent e069869a94
commit b2f4cabef4
7 changed files with 132 additions and 4 deletions

View File

@@ -5,8 +5,16 @@ from datetime import UTC, datetime, timedelta
import httpx
import structlog
from auth import require_auth
from config import LLM_API_KEY, LLM_API_VERSION, LLM_BASE_URL, LLM_MAX_EVENTS, LLM_MODEL, LLM_TIMEOUT_SECONDS
from auth import require_auth, user_can_access_privacy_services
from config import (
LLM_API_KEY,
LLM_API_VERSION,
LLM_BASE_URL,
LLM_MAX_EVENTS,
LLM_MODEL,
LLM_TIMEOUT_SECONDS,
PRIVACY_SERVICES,
)
from database import events_collection
from fastapi import APIRouter, Depends, HTTPException
from models.api import AskRequest, AskResponse
@@ -588,6 +596,9 @@ async def explain_event(event_id: str, user: dict = Depends(require_auth)):
if not event:
raise HTTPException(status_code=404, detail="Event not found")
if event.get("service") in PRIVACY_SERVICES and not user_can_access_privacy_services(user):
raise HTTPException(status_code=403, detail="Access to this event is restricted")
event.pop("_id", None)
# Fetch related events for context (same actor or target in last 24h)
@@ -678,6 +689,7 @@ async def ask_question(body: AskRequest, user: dict = Depends(require_auth)):
# -----------------------------------------------------------------------
# Build and run query
# -----------------------------------------------------------------------
privacy_excluded = [] if user_can_access_privacy_services(user) else list(PRIVACY_SERVICES)
query = _build_event_query(
entity,
start,
@@ -689,6 +701,8 @@ async def ask_question(body: AskRequest, user: dict = Depends(require_auth)):
include_tags=body.include_tags,
exclude_tags=body.exclude_tags,
)
if privacy_excluded:
query["$and"] = query.get("$and", []) + [{"service": {"$nin": privacy_excluded}}]
try:
total = events_collection.count_documents(query)