Spaces:
Sleeping
Sleeping
File size: 4,272 Bytes
6952bcb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | -- 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;
|