agnes-the-ai-analyst/docs/archive/superpowers/specs/2026-03-31-data-access-control.md
ZdenekSrotyr a48524509a
docs: consolidate and de-clutter the documentation tree (#306)
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.
2026-05-14 18:54:22 +00:00

6.2 KiB

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:

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

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)

# 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)

# 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)

# 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)

# 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

# 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