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;