feat: operation-level privacy gating instead of broad service-level
All checks were successful
CI / lint-and-test (push) Successful in 21s
All checks were successful
CI / lint-and-test (push) Successful in 21s
- Replace broad service-level hiding with fine-grained operation-level gating
- PRIVACY_SENSITIVE_OPERATIONS config: hide specific operations across ALL services
- PRIVACY_SERVICES still works for broad service-level blocking (optional)
- Users without PRIVACY_SERVICE_ROLES:
* Don't see sensitive operations in /api/filter-options
* Can't query sensitive operations via /api/events or /api/ask
* Get 403 on /api/events/{id}/explain for sensitive events
- Exchange/Teams services remain visible; only privacy ops are hidden
- Update .env.example with new operation-level config docs
This commit is contained in:
@@ -13,6 +13,7 @@ from config import (
|
||||
LLM_MAX_EVENTS,
|
||||
LLM_MODEL,
|
||||
LLM_TIMEOUT_SECONDS,
|
||||
PRIVACY_SENSITIVE_OPERATIONS,
|
||||
PRIVACY_SERVICES,
|
||||
)
|
||||
from database import events_collection
|
||||
@@ -596,7 +597,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):
|
||||
if (
|
||||
event.get("service") in PRIVACY_SERVICES or event.get("operation") in PRIVACY_SENSITIVE_OPERATIONS
|
||||
) and not user_can_access_privacy_services(user):
|
||||
raise HTTPException(status_code=403, detail="Access to this event is restricted")
|
||||
|
||||
event.pop("_id", None)
|
||||
@@ -689,7 +692,8 @@ 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)
|
||||
privacy_excluded_services = [] if user_can_access_privacy_services(user) else list(PRIVACY_SERVICES)
|
||||
privacy_excluded_ops = [] if user_can_access_privacy_services(user) else list(PRIVACY_SENSITIVE_OPERATIONS)
|
||||
query = _build_event_query(
|
||||
entity,
|
||||
start,
|
||||
@@ -701,8 +705,13 @@ 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}}]
|
||||
extra_filters = []
|
||||
if privacy_excluded_services:
|
||||
extra_filters.append({"service": {"$nin": privacy_excluded_services}})
|
||||
if privacy_excluded_ops:
|
||||
extra_filters.append({"operation": {"$nin": privacy_excluded_ops}})
|
||||
if extra_filters:
|
||||
query["$and"] = query.get("$and", []) + extra_filters
|
||||
|
||||
try:
|
||||
total = events_collection.count_documents(query)
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import UTC, datetime
|
||||
from audit_trail import log_action
|
||||
from auth import require_auth, user_can_access_privacy_services
|
||||
from bson import ObjectId
|
||||
from config import PRIVACY_SERVICES
|
||||
from config import PRIVACY_SENSITIVE_OPERATIONS, PRIVACY_SERVICES
|
||||
from database import events_collection
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from models.api import (
|
||||
@@ -45,7 +45,7 @@ def _build_query(
|
||||
cursor: str | None = None,
|
||||
include_tags: list[str] | None = None,
|
||||
exclude_tags: list[str] | None = None,
|
||||
exclude_services: list[str] | None = None,
|
||||
exclude_operations: list[str] | None = None,
|
||||
) -> dict:
|
||||
filters = []
|
||||
|
||||
@@ -53,8 +53,8 @@ def _build_query(
|
||||
filters.append({"service": service})
|
||||
if services:
|
||||
filters.append({"service": {"$in": services}})
|
||||
if exclude_services:
|
||||
filters.append({"service": {"$nin": exclude_services}})
|
||||
if exclude_operations:
|
||||
filters.append({"operation": {"$nin": exclude_operations}})
|
||||
if actor:
|
||||
actor_safe = re.escape(actor)
|
||||
filters.append(
|
||||
@@ -129,7 +129,8 @@ def list_events(
|
||||
exclude_tags: list[str] | None = Query(default=None),
|
||||
user: dict = Depends(require_auth),
|
||||
):
|
||||
privacy_excluded = [] if user_can_access_privacy_services(user) else list(PRIVACY_SERVICES)
|
||||
privacy_excluded_services = [] if user_can_access_privacy_services(user) else list(PRIVACY_SERVICES)
|
||||
privacy_excluded_ops = [] if user_can_access_privacy_services(user) else list(PRIVACY_SENSITIVE_OPERATIONS)
|
||||
query = _build_query(
|
||||
service=service,
|
||||
services=services,
|
||||
@@ -142,8 +143,13 @@ def list_events(
|
||||
cursor=cursor,
|
||||
include_tags=include_tags,
|
||||
exclude_tags=exclude_tags,
|
||||
exclude_services=privacy_excluded,
|
||||
exclude_operations=privacy_excluded_ops,
|
||||
)
|
||||
if privacy_excluded_services:
|
||||
query = query if query else {}
|
||||
if "$and" not in query:
|
||||
query = {"$and": [query]} if query else {"$and": []}
|
||||
query["$and"].append({"service": {"$nin": privacy_excluded_services}})
|
||||
|
||||
safe_page_size = max(1, min(page_size, 500))
|
||||
|
||||
@@ -208,7 +214,8 @@ def bulk_tags(
|
||||
exclude_tags: list[str] | None = Query(default=None),
|
||||
user: dict = Depends(require_auth),
|
||||
):
|
||||
privacy_excluded = [] if user_can_access_privacy_services(user) else list(PRIVACY_SERVICES)
|
||||
privacy_excluded_services = [] if user_can_access_privacy_services(user) else list(PRIVACY_SERVICES)
|
||||
privacy_excluded_ops = [] if user_can_access_privacy_services(user) else list(PRIVACY_SENSITIVE_OPERATIONS)
|
||||
query = _build_query(
|
||||
service=service,
|
||||
services=services,
|
||||
@@ -220,8 +227,13 @@ def bulk_tags(
|
||||
search=search,
|
||||
include_tags=include_tags,
|
||||
exclude_tags=exclude_tags,
|
||||
exclude_services=privacy_excluded,
|
||||
exclude_operations=privacy_excluded_ops,
|
||||
)
|
||||
if privacy_excluded_services:
|
||||
query = query if query else {}
|
||||
if "$and" not in query:
|
||||
query = {"$and": [query]} if query else {"$and": []}
|
||||
query["$and"].append({"service": {"$nin": privacy_excluded_services}})
|
||||
tags = [t.strip() for t in body.tags if t.strip()]
|
||||
if not tags:
|
||||
raise HTTPException(status_code=400, detail="No tags provided")
|
||||
@@ -260,6 +272,7 @@ def filter_options(
|
||||
|
||||
if not user_can_access_privacy_services(user):
|
||||
services = [s for s in services if s not in PRIVACY_SERVICES]
|
||||
operations = [o for o in operations if o not in PRIVACY_SENSITIVE_OPERATIONS]
|
||||
|
||||
return {
|
||||
"services": services,
|
||||
|
||||
Reference in New Issue
Block a user