File size: 5,831 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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
-- 006_licensing (postgres) β€” Phase 4 billing (docs/HARDENING.md): org-scoped
-- agent licenses + append-only usage metering, PLUS layer 3 (RLS) on both
-- tables (docs/TENANCY.md: both are T1 org-internal data).
--
-- Mutability model (deliberate asymmetry):
--   * `licenses` is MUTABLE β€” revocation and expiry are DB state transitions
--     (status active -> revoked | expired). A license key alone NEVER
--     suffices: atp/licensing.py check_license() re-reads the row on every
--     call, so a revoked/expired row fails instantly regardless of a valid
--     key signature.
--   * `usage_events` is APPEND-ONLY at the DB level (BEFORE UPDATE/DELETE
--     trigger aborts) β€” usage rows are billing evidence and must be as
--     tamper-resistant as the atp_evidence chain.
--
-- Key material: only `key_id` (the public identifier half of the license
-- key) is stored. The secret half is an HMAC over key_id with
-- LICENSE_SIGNING_KEY, shown once at issue time and never persisted.
--
-- RLS model β€” DENY BY DEFAULT, 005 policy pattern:
--   * org_isolation on both tables:
--       USING / WITH CHECK (org_id = current_setting('app.org_id', true))
--     current_setting(..., true) is NULL when unset β†’ predicate NULL β†’ row
--     invisible / write rejected. Do NOT add a NULL fallback (see 005).
--   * FORCE ROW LEVEL SECURITY so the table-owner app role has no bypass.
--   * ONE narrow addition on `licenses` only: policy `key_lookup` (SELECT
--     only). check_license() authenticates by license KEY, not by org β€” the
--     caller of the gated proxy endpoint may be a machine holding only the
--     key, and the org is learned FROM the row. After atp/licensing.py has
--     verified the key's HMAC signature (constant time), it runs, inside one
--     transaction:
--         SELECT set_config('app.license_key_id', :key_id, true);  -- SET LOCAL
--         SELECT ... FROM licenses WHERE key_id = :key_id;
--     which makes exactly the ONE row whose bearer credential was presented
--     visible β€” nothing else, no writes (writes still require the org GUC
--     via org_isolation's WITH CHECK). Absent both GUCs the predicate is
--     NULL β†’ zero rows, so deny-by-default is preserved.
--   * All other application access goes through atp/tenant_db.py
--     (SET LOCAL app.org_id per transaction), exactly as for the 005 tables.
--
-- Statement separator convention: a line containing only `--;;` splits this
-- file into statements (see atp/db.py).

CREATE TABLE IF NOT EXISTS licenses (
  id                  TEXT PRIMARY KEY,
  org_id              TEXT NOT NULL DEFAULT 'org-demo',
  agent_id            TEXT NOT NULL,
  kind                TEXT NOT NULL CHECK (kind IN ('evaluation', 'production')),
  status              TEXT NOT NULL DEFAULT 'active'
                      CHECK (status IN ('active', 'revoked', 'expired')),
  key_id              TEXT NOT NULL UNIQUE,
  created_at          TEXT NOT NULL,
  expires_at          TEXT,            -- NULL = never expires
  revoked_at          TEXT,
  revoked_reason      TEXT,
  stripe_customer     TEXT,            -- NULL for 'manual' (invoice) licenses
  stripe_subscription TEXT,            -- NULL for 'manual' (invoice) licenses
  seats               INTEGER NOT NULL DEFAULT 1
);
--;;
-- (org_id, <pk>) per docs/TENANCY.md invariants; key_id lookup is covered by
-- the UNIQUE constraint's implicit index.
CREATE INDEX IF NOT EXISTS idx_licenses_org ON licenses(org_id, id);
--;;
CREATE TABLE IF NOT EXISTS usage_events (
  id         BIGSERIAL PRIMARY KEY,
  ts         TEXT NOT NULL,
  org_id     TEXT NOT NULL DEFAULT 'org-demo',
  license_id TEXT,
  agent_id   TEXT,
  endpoint   TEXT,
  tokens_in  INTEGER,
  tokens_out INTEGER,
  status     TEXT
);
--;;
CREATE INDEX IF NOT EXISTS idx_usage_events_org ON usage_events(org_id, id);
--;;
-- ── Append-only enforcement (002/004 pattern) ───────────────────────────────
-- Same trigger function as 002/004; re-declared (CREATE OR REPLACE, identical
-- body) so this migration stands alone.
CREATE OR REPLACE FUNCTION atp_append_only() RETURNS trigger AS $$
BEGIN
  RAISE EXCEPTION 'append-only: % on % is forbidden', TG_OP, TG_TABLE_NAME;
END;
$$ LANGUAGE plpgsql;
--;;
DROP TRIGGER IF EXISTS trg_usage_events_append_only ON usage_events;
--;;
CREATE TRIGGER trg_usage_events_append_only
BEFORE UPDATE OR DELETE ON usage_events
FOR EACH ROW EXECUTE FUNCTION atp_append_only();
--;;
-- ── Row-Level Security (layer 3, 005 pattern) ───────────────────────────────
-- CREATE POLICY has no IF NOT EXISTS β†’ DROP IF EXISTS first so a
-- partially-applied database can be re-run safely.
ALTER TABLE licenses ENABLE ROW LEVEL SECURITY;
--;;
ALTER TABLE licenses FORCE ROW LEVEL SECURITY;
--;;
DROP POLICY IF EXISTS org_isolation ON licenses;
--;;
CREATE POLICY org_isolation ON licenses FOR ALL
  USING      (org_id = current_setting('app.org_id', true))
  WITH CHECK (org_id = current_setting('app.org_id', true));
--;;
-- Narrow key-based lookup path for check_license() β€” SELECT only, exactly
-- the row whose (already signature-verified) key_id was pinned via
-- set_config('app.license_key_id', :key_id, true). See header comment.
DROP POLICY IF EXISTS key_lookup ON licenses;
--;;
CREATE POLICY key_lookup ON licenses FOR SELECT
  USING (key_id = current_setting('app.license_key_id', true));
--;;
ALTER TABLE usage_events ENABLE ROW LEVEL SECURITY;
--;;
ALTER TABLE usage_events FORCE ROW LEVEL SECURITY;
--;;
DROP POLICY IF EXISTS org_isolation ON usage_events;
--;;
CREATE POLICY org_isolation ON usage_events FOR ALL
  USING      (org_id = current_setting('app.org_id', true))
  WITH CHECK (org_id = current_setting('app.org_id', true));