Spaces:
Sleeping
Sleeping
| -- 004_identity (sqlite) β Phase 2 identity & tenancy principals: | |
| -- orgs, org-aware users, append-only access_log (docs/TENANCY.md). | |
| -- | |
| -- NOTE on `users`: migration 001 already created `users` as the learner | |
| -- profile table (user_id TEXT PK, display_name, created_at REAL β written by | |
| -- agents/persistence.py). Rather than a second colliding table, this | |
| -- migration EXTENDS it with the identity columns from docs/TENANCY.md: | |
| -- `user_id` serves as the spec's `id`; legacy learner rows keep NULL | |
| -- org_id/email/role and are invisible to identity lookups (which filter on | |
| -- email / (oidc_iss, oidc_sub)). | |
| -- | |
| -- Conventions: | |
| -- * ts / created_at columns added here are ISO-8601 UTC strings | |
| -- ("%Y-%m-%dT%H:%M:%SZ"), matching atp/store.py `_now()`. | |
| -- (users.created_at stays REAL β pre-existing column from 001.) | |
| -- * access_log is APPEND-ONLY at the DB level: BEFORE UPDATE / BEFORE | |
| -- DELETE triggers abort with 'append-only' (same pattern as 002). | |
| -- * role CHECK (admin|sme|viewer) is enforced by triggers because SQLite | |
| -- cannot ALTER TABLE ... ADD CHECK; NULL stays allowed for legacy | |
| -- learner rows. | |
| -- | |
| -- Statement separator convention: a line containing only `--;;` splits this | |
| -- file into statements (see atp/db.py). Plain ';' inside a statement (e.g. | |
| -- trigger bodies below) is NOT a separator once the marker is used anywhere | |
| -- in the file. | |
| CREATE TABLE IF NOT EXISTS orgs ( | |
| id TEXT PRIMARY KEY, | |
| name TEXT, | |
| domain TEXT, | |
| created_at TEXT NOT NULL | |
| ); | |
| --;; | |
| CREATE UNIQUE INDEX IF NOT EXISTS idx_orgs_domain | |
| ON orgs(domain) WHERE domain IS NOT NULL; | |
| --;; | |
| -- ββ users: identity columns (see NOTE above) βββββββββββββββββββββββββββββββ | |
| ALTER TABLE users ADD COLUMN org_id TEXT REFERENCES orgs(id); | |
| --;; | |
| ALTER TABLE users ADD COLUMN email TEXT; | |
| --;; | |
| ALTER TABLE users ADD COLUMN role TEXT; | |
| --;; | |
| ALTER TABLE users ADD COLUMN oidc_iss TEXT; | |
| --;; | |
| ALTER TABLE users ADD COLUMN oidc_sub TEXT; | |
| --;; | |
| ALTER TABLE users ADD COLUMN pw_hash TEXT; | |
| --;; | |
| CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email | |
| ON users(email) WHERE email IS NOT NULL; | |
| --;; | |
| CREATE UNIQUE INDEX IF NOT EXISTS idx_users_oidc | |
| ON users(oidc_iss, oidc_sub) | |
| WHERE oidc_iss IS NOT NULL AND oidc_sub IS NOT NULL; | |
| --;; | |
| CREATE INDEX IF NOT EXISTS idx_users_org ON users(org_id); | |
| --;; | |
| -- role CHECK via triggers (NULL allowed β legacy learner rows have no role) | |
| CREATE TRIGGER IF NOT EXISTS trg_users_role_check_insert | |
| BEFORE INSERT ON users | |
| WHEN NEW.role IS NOT NULL AND NEW.role NOT IN ('admin', 'sme', 'viewer') | |
| BEGIN | |
| SELECT RAISE(ABORT, 'users.role must be admin|sme|viewer'); | |
| END; | |
| --;; | |
| CREATE TRIGGER IF NOT EXISTS trg_users_role_check_update | |
| BEFORE UPDATE ON users | |
| WHEN NEW.role IS NOT NULL AND NEW.role NOT IN ('admin', 'sme', 'viewer') | |
| BEGIN | |
| SELECT RAISE(ABORT, 'users.role must be admin|sme|viewer'); | |
| END; | |
| --;; | |
| -- ββ access_log β tenant-data audit trail (TENANCY.md layer 4) ββββββββββββββ | |
| CREATE TABLE IF NOT EXISTS access_log ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| ts TEXT NOT NULL, | |
| org_id TEXT, | |
| user_id TEXT, | |
| role TEXT, | |
| method TEXT, | |
| path TEXT, | |
| target_table TEXT, | |
| row_count INTEGER, | |
| status INTEGER | |
| ); | |
| --;; | |
| CREATE INDEX IF NOT EXISTS idx_access_log_org ON access_log(org_id, id); | |
| --;; | |
| -- ββ Append-only enforcement (same pattern as 002 atp_hitl) βββββββββββββββββ | |
| CREATE TRIGGER IF NOT EXISTS trg_access_log_no_update | |
| BEFORE UPDATE ON access_log | |
| BEGIN | |
| SELECT RAISE(ABORT, 'append-only'); | |
| END; | |
| --;; | |
| CREATE TRIGGER IF NOT EXISTS trg_access_log_no_delete | |
| BEFORE DELETE ON access_log | |
| BEGIN | |
| SELECT RAISE(ABORT, 'append-only'); | |
| END; | |
| --;; | |
| -- ββ Seed: bootstrap org (and nothing else) βββββββββββββββββββββββββββββββββ | |
| -- Legacy single-admin sessions and BU_BOOTSTRAP_ADMIN_* provisioning map to | |
| -- this org (api/identity.py). domain stays NULL: no OIDC domain auto-join. | |
| INSERT INTO orgs (id, name, domain, created_at) | |
| VALUES ('org-demo', 'Demo Org', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) | |
| ON CONFLICT (id) DO NOTHING; | |