Add merchant-onboard scoped-key role for self-serve onboarding
New scoped API-key role granting read + products:write + policies:write + licenses:write — the least-privilege credential for end-to-end catalog setup and license issuance (create product, define policies/tiers, issue licenses against them) without holding the master key. The catalog write scopes already existed and were enforced on the endpoints; only the role->scope expansion was missing. So this is a new Role variant, not a scope-model change. grants() matches scope strings explicitly (never by :write suffix) so the role can't widen into settings / payment / merchant-profile / webhook writes, and every master-only operation stays behind require_admin and so is structurally unreachable. Existing tier caps still bound it (Creator: 5 products / 5 policies per product). Migration 0023 rebuilds scoped_api_keys to widen the role CHECK (SQLite can't alter a CHECK in place); the table has no FKs, so it's a plain copy/drop/rename. Test covers the full onboard chain under the key's own credential plus denial of master-only gates and support-only writes.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
-- Migration 0023: add the 'merchant-onboard' scoped-API-key role.
|
||||
--
|
||||
-- 0016 created scoped_api_keys with a CHECK that pins `role` to the four
|
||||
-- roles known then (read-only | license-issuer | support | full-admin).
|
||||
-- SQLite can't ALTER or DROP a CHECK constraint in place, so adding a
|
||||
-- fifth role means rebuilding the table with a widened CHECK.
|
||||
--
|
||||
-- scoped_api_keys has no foreign keys (inbound or outbound), so this is
|
||||
-- the simple copy -> drop -> rename rebuild, without any of the FK
|
||||
-- juggling that 0009 needed. sqlx-migrate wraps each file in a
|
||||
-- transaction; we don't BEGIN here.
|
||||
--
|
||||
-- Idempotent: re-running produces the same end state. Existing rows (any
|
||||
-- role, active or revoked) are preserved verbatim. The leading DROP IF
|
||||
-- EXISTS clears a stray _new table from any partially-applied prior run
|
||||
-- before we rebuild.
|
||||
|
||||
DROP TABLE IF EXISTS scoped_api_keys_new;
|
||||
|
||||
CREATE TABLE scoped_api_keys_new (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
role TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
last_used_at TEXT,
|
||||
revoked_at TEXT,
|
||||
CHECK (role IN ('read-only', 'license-issuer', 'support', 'merchant-onboard', 'full-admin'))
|
||||
);
|
||||
|
||||
INSERT INTO scoped_api_keys_new
|
||||
SELECT id, label, token_hash, role, created_at, last_used_at, revoked_at
|
||||
FROM scoped_api_keys;
|
||||
|
||||
DROP TABLE scoped_api_keys;
|
||||
ALTER TABLE scoped_api_keys_new RENAME TO scoped_api_keys;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_scoped_api_keys_token ON scoped_api_keys(token_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_scoped_api_keys_active ON scoped_api_keys(revoked_at);
|
||||
Reference in New Issue
Block a user