CLAUDE.md rewritten (708 -> ~320 lines): four overlapping release sections collapsed to one, stale v1->v35 schema history dropped (it lives in CHANGELOG), marketplace endpoint internals and verbose process sections moved out or tightened. New focused docs: - docs/RELEASING.md - release process, deploy workflows, CI quirks (RELEASE_TEMPLATE.md folded in as an appendix) - docs/marketplace.md - marketplace ingestion + re-serving internals - docs/README.md - documentation index by audience, linked from README.md and CLAUDE.md Archived under docs/archive/: docs/superpowers/ (52 historical planning artifacts), HACKATHON.md, pd-ps-comments.md, security-audit-2026-04.md, future/NOTIFICATIONS.md. Removed the docs/auto-install.md stub. Fixed dangling links in connectors/jira/README.md and dev_docs/README.md, repointed code/doc references to archived paths.
202 lines
6.2 KiB
Markdown
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
|