sqlc fix
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
-- api: shall-issue tenancy. Owners, keys, usage.
|
||||
--
|
||||
-- Most sensitive schema in the database. hammy_readonly is deliberately NOT
|
||||
-- granted USAGE on it - a reporting query has no business reading key material.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- owners
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE api.owners (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
|
||||
-- The abuse contact, and the only field that is genuinely required. It is
|
||||
-- what makes shall-issue workable: a channel to say "your key is doing
|
||||
-- something odd, fix it or I revoke".
|
||||
email core.email NOT NULL UNIQUE,
|
||||
email_verified_at timestamptz,
|
||||
|
||||
-- Optional, and a scope multiplier rather than a gate. An unlicensed
|
||||
-- developer building a study tool against the exam endpoints has no
|
||||
-- callsign and no reason to get one yet.
|
||||
callsign core.callsign,
|
||||
callsign_verified_at timestamptz,
|
||||
|
||||
status text NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'suspended', 'closed')),
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE api.owners IS 'Registrants for API keys. Holds PII from an international user base, so the deletion path in 005_privacy.sql applies here too, not just to Discord users.';
|
||||
|
||||
CREATE UNIQUE INDEX owners_verified_callsign_uniq
|
||||
ON api.owners (callsign)
|
||||
WHERE callsign_verified_at IS NOT NULL;
|
||||
|
||||
CREATE TRIGGER owners_touch
|
||||
BEFORE UPDATE ON api.owners
|
||||
FOR EACH ROW EXECUTE FUNCTION core.touch_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- keys
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE api.keys (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
owner_id bigint NOT NULL REFERENCES api.owners(id) ON DELETE CASCADE,
|
||||
|
||||
-- SHA-256 of the key, computed in the application. The key itself is shown
|
||||
-- once at issue and never stored: a database dump must not be a set of
|
||||
-- working credentials. Hashing in the app rather than via pgcrypto keeps
|
||||
-- the plaintext out of the query log.
|
||||
key_hash bytea NOT NULL UNIQUE,
|
||||
CONSTRAINT keys_hash_is_sha256 CHECK (octet_length(key_hash) = 32),
|
||||
|
||||
-- Enough of the key to identify it in a UI and in a leak report, e.g.
|
||||
-- 'hmy_live_a1b2c3d4'. Not secret and not sufficient to authenticate.
|
||||
key_prefix text NOT NULL,
|
||||
label text,
|
||||
|
||||
-- Tier 0 reference data can be near-unlimited; callsign lookup and 44net
|
||||
-- probing need much tighter ceilings. Scoping from day one means a scope
|
||||
-- can be tightened later without breaking every key.
|
||||
scopes text[] NOT NULL DEFAULT '{}',
|
||||
CONSTRAINT keys_scopes_known CHECK (
|
||||
scopes <@ ARRAY[
|
||||
'reference', -- band plans, prefixes, calculators
|
||||
'callsign', -- callsign lookup, rate limited
|
||||
'spots', -- POTA/SOTA/cluster/RBN feeds
|
||||
'propagation',
|
||||
'logbook', -- read/write a user's own log
|
||||
'net44', -- reachability probes from the 44net allocation
|
||||
'admin'
|
||||
]::text[]
|
||||
),
|
||||
|
||||
quota_tier text NOT NULL DEFAULT 'default'
|
||||
CHECK (quota_tier IN ('default', 'verified', 'trusted', 'internal')),
|
||||
|
||||
status text NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'revoked', 'suspended')),
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz,
|
||||
last_used_at timestamptz,
|
||||
revoked_at timestamptz,
|
||||
revoked_reason text,
|
||||
|
||||
CONSTRAINT keys_revoked_consistent CHECK (
|
||||
(status = 'revoked') = (revoked_at IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE api.keys IS 'Several active keys per owner is intentional: people leak keys into public repositories, and rotating needs an overlap window where both the old and new key work.';
|
||||
|
||||
CREATE INDEX keys_owner_idx ON api.keys (owner_id);
|
||||
|
||||
-- No separate index on key_hash: the UNIQUE constraint above already builds a
|
||||
-- btree over exactly that column, and the planner uses it for the lookup.
|
||||
|
||||
CREATE INDEX keys_expiring_idx
|
||||
ON api.keys (expires_at)
|
||||
WHERE status = 'active' AND expires_at IS NOT NULL;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- usage
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Rolled up from Redis, which is authoritative for the live rate-limit window.
|
||||
-- Redis holds the current counters; this holds completed days. A Redis flush
|
||||
-- loses at most the window in progress, which for abuse detection is fine.
|
||||
CREATE TABLE api.usage_daily (
|
||||
key_id bigint NOT NULL REFERENCES api.keys(id) ON DELETE CASCADE,
|
||||
day date NOT NULL,
|
||||
endpoint text NOT NULL,
|
||||
calls bigint NOT NULL DEFAULT 0,
|
||||
errors bigint NOT NULL DEFAULT 0,
|
||||
|
||||
-- Distinct end users seen, not who they were. Counting them is the
|
||||
-- enumeration signal - one Discord user driving 90% of a key's traffic
|
||||
-- looks very different from fifty users sharing it - but the identities
|
||||
-- themselves are not needed and so are not kept.
|
||||
distinct_users integer NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (key_id, day, endpoint)
|
||||
);
|
||||
|
||||
-- The flush job is an upsert:
|
||||
-- INSERT INTO api.usage_daily (key_id, day, endpoint, calls)
|
||||
-- VALUES ($1, $2, $3, $4)
|
||||
-- ON CONFLICT (key_id, day, endpoint)
|
||||
-- DO UPDATE SET calls = api.usage_daily.calls + EXCLUDED.calls;
|
||||
CREATE INDEX usage_daily_day_idx ON api.usage_daily (day);
|
||||
Reference in New Issue
Block a user