agnes-the-ai-analyst/docs/superpowers/specs/2026-03-31-data-access-control.md
ZdenekSrotyr 1074d5ec49 feat: implement data access control — table-level permissions
Schema v3: add is_public column to table_registry (default true).

src/rbac.py: can_access_table() checks admin bypass, public flag,
explicit permissions, wildcard bucket permissions.

API enforcement:
- manifest: filters tables by user access
- download: 403 if no access
- catalog: filters table list
- query: validates referenced tables against allowed list

New admin permissions API (/api/admin/permissions) for grant/revoke.

28 access control tests + 733 total tests passing.
2026-03-31 12:33:31 +02:00

202 lines
6.2 KiB
Markdown

# Data Access Control — Spec
**Date:** 2026-03-31
**Status:** Draft
## 1. Problem
V novém systému (API místo rsync) nemáme ekvivalent rsync filtru. Každý přihlášený uživatel vidí a stáhne všechny tabulky. V produkci to řeší filesystem permissions + per-user rsync filter.
## 2. Současný model (produkce, rsync)
```
Server: /data/src_data/parquet/
├── crm/orders.parquet ← dataread group
├── crm/customers.parquet ← dataread group
├── private/salaries.parquet ← data-private group only
└── jira/issues/2026-03.parquet ← dataread group
Analytik (sync_data.sh):
1. Webapp generuje ~/.sync_rsync_filter (include/exclude per tabulka)
2. rsync --filter="merge ~/.sync_rsync_filter" stáhne jen povolené
3. AI agent pracuje s lokálními soubory → vidí jen to co se stáhlo
```
Tři vrstvy:
- **Linux skupiny** (dataread, data-private) → hrubé řízení
- **Datasety** (opt-in v instance.yaml) → celé skupiny tabulek
- **Per-table subscription** (explicit mode) → jednotlivé tabulky
## 3. Nový model (API)
Princip zůstává: **uživatel vidí jen to, k čemu má explicitní přístup**.
### 3.1 Datový model
Stávající tabulka `dataset_permissions` v DuckDB:
```sql
CREATE TABLE dataset_permissions (
user_id VARCHAR NOT NULL,
dataset VARCHAR NOT NULL, -- table_id nebo dataset group name
access VARCHAR DEFAULT 'read', -- 'read', 'none'
PRIMARY KEY (user_id, dataset)
);
```
`dataset` může být:
- **Table ID** (`circle`, `chart_of_accounts`) — přístup k jedné tabulce
- **Wildcard/group** (`in.c-finance.*`) — přístup ke všem tabulkám v bucketu
- **Dataset name** (`jira`, `finance`) — pojmenovaná skupina z instance.yaml
### 3.2 Pravidla přístupu
```
Admin → vidí vše (bypass permissions)
Ostatní → vidí jen tabulky kde:
1. Existuje explicitní permission (dataset_permissions.access = 'read')
2. NEBO tabulka patří do povoleného datasetu/bucketu
3. NEBO je tabulka public (nový flag v table_registry)
```
### 3.3 Nový sloupec v table_registry
```sql
ALTER TABLE table_registry ADD COLUMN is_public BOOLEAN DEFAULT true;
```
- `is_public = true` → každý přihlášený uživatel vidí (default, zpětně kompatibilní)
- `is_public = false` → vyžaduje explicitní permission
## 4. Kde se kontroluje
### 4.1 Manifest (`GET /api/sync/manifest`)
```python
# Současný kód (NEFUNGUJE):
accessible = set(perm_repo.get_accessible_datasets(user["id"]))
# ... ale nikdy nefiltruje
# Nový kód:
all_states = repo.get_all_states()
if user["role"] != "admin":
all_states = [s for s in all_states if _user_can_access(user, s["table_id"])]
```
### 4.2 Download (`GET /api/data/{table}/download`)
```python
# Současný kód (ŽÁDNÁ KONTROLA):
return FileResponse(path=file_path)
# Nový kód:
if not _user_can_access(user, table_id):
raise HTTPException(403, "Access denied")
return FileResponse(path=file_path)
```
### 4.3 Query (`POST /api/query`)
```python
# Současný kód: otevře analytics.duckdb s VŠEMI views
# Nový kód: vytvořit per-user filtered connection
# Varianta A: CREATE TEMP VIEW pro povolené tabulky
# Varianta B: Dynamicky generovat allowed list, validovat SQL against it
```
Query je nejtěžší — uživatel může napsat `SELECT * FROM salaries` a pokud view existuje v analytics.duckdb, data se vrátí. Řešení:
**Varianta A — Filtered views (doporučeno):**
Per-request vytvoření in-memory DuckDB, ATTACH analytics.duckdb, vytvořit views jen pro povolené tabulky. Overhead ~10ms.
**Varianta B — SQL validation:**
Parsovat SQL, extrahovat referenced tables, ověřit proti allowed list. Křehké (sub-queries, CTEs, aliasy).
### 4.4 Catalog (`GET /api/catalog/tables`)
```python
# Filtrovat jako manifest — uživatel vidí metadata jen povolených tabulek
if user["role"] != "admin":
tables = [t for t in tables if _user_can_access(user, t["id"])]
```
## 5. Shared helper
```python
# src/rbac.py — rozšíření
def can_access_table(user: dict, table_id: str) -> bool:
"""Check if user can access a specific table."""
# Admin bypass
if user.get("role") == "admin":
return True
# Check if table is public
table = TableRegistryRepository(conn).get(table_id)
if table and table.get("is_public", True):
return True
# Check explicit permission
user_id = user["id"]
if DatasetPermissionRepository(conn).has_access(user_id, table_id):
return True
# Check wildcard/bucket permission (e.g., "in.c-finance.*")
bucket = table.get("bucket", "") if table else ""
if bucket and DatasetPermissionRepository(conn).has_access(user_id, f"{bucket}.*"):
return True
return False
```
## 6. Admin API pro permissions
```
POST /api/admin/permissions — grant access
DELETE /api/admin/permissions — revoke access
GET /api/admin/permissions/{user_id} — list user's permissions
GET /api/admin/permissions — list all (admin only)
POST body: {"user_id": "...", "dataset": "circle", "access": "read"}
```
## 7. Migrace
### Pro existující instance:
1. Všechny stávající tabulky: `is_public = true` (zachová současné chování)
2. Admin nastaví `is_public = false` pro citlivé tabulky
3. Přidá explicitní permissions pro uživatele
### Pro nové instance:
- Default `is_public = true` → otevřený model (jako teď)
- Admin může přepnout na uzavřený: `is_public = false` per tabulka
## 8. CLI (`da sync`)
```
da sync
→ GET /api/sync/manifest (vrátí jen povolené tabulky)
→ pro každou tabulku: GET /api/data/{table}/download
→ rebuild lokální DuckDB jen z povolených parquetů
→ AI agent vidí jen to co se stáhlo
```
Identický princip jako rsync filter — ale filtr je server-side v API, ne v souboru.
## 9. Co se NEMĚNÍ
- Role hierarchy (viewer < analyst < km_admin < admin)
- Admin vidí vše
- JWT auth flow
- Orchestrator + extractory (server-side, vidí vše)
- Sync trigger (admin-only, stahuje vše na server)
## 10. Implementační pořadí
1. `is_public` sloupec v table_registry (schema v3)
2. `can_access_table()` helper v src/rbac.py
3. Filtrování v manifest + download + catalog
4. Admin permissions API
5. Query endpoint filtered views
6. Testy