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

@@ -149,6 +149,79 @@ def test_saved_searches_create_validation(client, monkeypatch):
assert response.status_code == 400
def test_privacy_filtering_events(client, mock_events_collection, monkeypatch):
monkeypatch.setattr("config.PRIVACY_SERVICES", {"Exchange", "Teams"})
monkeypatch.setattr("routes.events.PRIVACY_SERVICES", {"Exchange", "Teams"})
monkeypatch.setattr("auth.PRIVACY_SERVICE_ROLES", {"SecurityAdmin"})
monkeypatch.setattr("auth.user_can_access_privacy_services", lambda claims: False)
monkeypatch.setattr("routes.events.user_can_access_privacy_services", lambda claims: False)
mock_events_collection.insert_one(
{
"id": "evt-dir",
"timestamp": datetime.now(UTC).isoformat(),
"service": "Directory",
"operation": "Add user",
"result": "success",
"actor_display": "Alice",
"raw_text": "",
}
)
mock_events_collection.insert_one(
{
"id": "evt-exc",
"timestamp": datetime.now(UTC).isoformat(),
"service": "Exchange",
"operation": "Send",
"result": "success",
"actor_display": "Bob",
"raw_text": "",
}
)
response = client.get("/api/events")
assert response.status_code == 200
data = response.json()
ids = [e["id"] for e in data["items"]]
assert "evt-dir" in ids
assert "evt-exc" not in ids
def test_privacy_filter_options(client, mock_events_collection, monkeypatch):
monkeypatch.setattr("config.PRIVACY_SERVICES", {"Exchange"})
monkeypatch.setattr("routes.events.PRIVACY_SERVICES", {"Exchange"})
monkeypatch.setattr("auth.PRIVACY_SERVICE_ROLES", {"SecurityAdmin"})
monkeypatch.setattr("auth.user_can_access_privacy_services", lambda claims: False)
monkeypatch.setattr("routes.events.user_can_access_privacy_services", lambda claims: False)
response = client.get("/api/filter-options")
assert response.status_code == 200
data = response.json()
assert "Exchange" not in data["services"]
def test_privacy_explain_forbidden(client, mock_events_collection, monkeypatch):
monkeypatch.setattr("config.PRIVACY_SERVICES", {"Exchange"})
monkeypatch.setattr("routes.ask.PRIVACY_SERVICES", {"Exchange"})
monkeypatch.setattr("auth.PRIVACY_SERVICE_ROLES", {"SecurityAdmin"})
monkeypatch.setattr("auth.user_can_access_privacy_services", lambda claims: False)
monkeypatch.setattr("routes.ask.user_can_access_privacy_services", lambda claims: False)
mock_events_collection.insert_one(
{
"id": "evt-exc2",
"timestamp": datetime.now(UTC).isoformat(),
"service": "Exchange",
"operation": "Send",
"result": "success",
"actor_display": "Bob",
"raw_text": "",
}
)
response = client.post("/api/events/evt-exc2/explain")
assert response.status_code == 403
def test_health(client):
response = client.get("/health")
assert response.status_code == 200