|
| 1 | +-- Copyright (c) 2026, SoftlaneIT (https://softlaneit.com/) All Rights Reserved. |
| 2 | +-- |
| 3 | +-- SoftlaneIT licenses this file to you under the Apache License, |
| 4 | +-- Version 2.0 (the "LICENSE"); you may not use this file except |
| 5 | +-- in compliance with the LICENSE. |
| 6 | +-- You may obtain a copy of the LICENSE at |
| 7 | +-- |
| 8 | +-- https://softlaneit.com/LICENSE.txt |
| 9 | +-- |
| 10 | +-- Unless required by applicable law or agreed to in writing, |
| 11 | +-- software distributed under the LICENSE is distributed on an |
| 12 | +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 13 | +-- KIND, either express or implied. See the LICENSE for the |
| 14 | +-- specific language governing permissions and limitations |
| 15 | +-- under the LICENSE. |
| 16 | + |
| 17 | +-- ── |
| 18 | +-- 000003_create_api_keys.up |
| 19 | +-- |
| 20 | +-- API keys are the primary authentication credential for tenant applications. |
| 21 | +-- A tenant may hold multiple keys (e.g. one per environment, one per service |
| 22 | +-- integration). |
| 23 | +-- |
| 24 | +-- Security model: |
| 25 | +-- • The raw key is generated once by auth-service and shown to the developer |
| 26 | +-- exactly once; it is never stored. |
| 27 | +-- • `key_hash` stores a SHA-256 hex digest of the raw key. Fast enough for |
| 28 | +-- O(1) lookup by hash; no need for bcrypt-level cost here because keys are |
| 29 | +-- high-entropy random strings (not passwords). |
| 30 | +-- • `key_prefix` stores the first 8 characters of the raw key (e.g. "sf_live_") |
| 31 | +-- for display in the management UI so developers can identify which key |
| 32 | +-- they are looking at without exposing the full secret. |
| 33 | +-- |
| 34 | +-- Rotation: |
| 35 | +-- Keys are never mutated after creation. To rotate, the developer creates a |
| 36 | +-- new key, updates their application, then revokes the old key by setting |
| 37 | +-- revoked_at. This gives a zero-downtime rotation window. |
| 38 | +-- |
| 39 | +-- Row-Level Security: |
| 40 | +-- api_keys are scoped to a tenant. The RLS policy enforces that a service |
| 41 | +-- connected as sf_app can only see rows belonging to the tenant whose ID was |
| 42 | +-- set via set_tenant_context(). The gateway's auth middleware bypasses RLS |
| 43 | +-- during the key-lookup step by calling the function as a superuser; once the |
| 44 | +-- tenant is identified it sets the context and all subsequent queries are |
| 45 | +-- automatically scoped. |
| 46 | +-- ── |
| 47 | + |
| 48 | +CREATE TABLE api_keys ( |
| 49 | + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| 50 | + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, |
| 51 | + -- Human-readable label set by the developer (e.g. "mobile-app-prod"). |
| 52 | + name VARCHAR(255) NOT NULL, |
| 53 | + -- SHA-256 hex digest of the raw key. Used for fast O(1) lookup. |
| 54 | + key_hash CHAR(64) NOT NULL, |
| 55 | + -- First 8 characters of the raw key for UI display (e.g. "sf_live_a"). |
| 56 | + key_prefix VARCHAR(12) NOT NULL, |
| 57 | + -- Environment this key is valid in. |
| 58 | + environment VARCHAR(20) NOT NULL DEFAULT 'sandbox' |
| 59 | + CHECK (environment IN ('sandbox', 'production')), |
| 60 | + -- Optional scope restriction: empty array = access to all subscribed modules. |
| 61 | + -- Non-empty = only the listed module names (e.g. '{"booking","payment"}'). |
| 62 | + module_scope TEXT[] NOT NULL DEFAULT '{}', |
| 63 | + -- Lifecycle state. Revoked keys must be retained for audit purposes. |
| 64 | + status VARCHAR(20) NOT NULL DEFAULT 'active' |
| 65 | + CHECK (status IN ('active', 'revoked')), |
| 66 | + -- Populated on successful authentication for anomaly detection. |
| 67 | + last_used_at TIMESTAMPTZ, |
| 68 | + -- Optional expiry. NULL = no expiry. |
| 69 | + expires_at TIMESTAMPTZ, |
| 70 | + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), |
| 71 | + -- Soft-revoke timestamp; NULL = key is not revoked. |
| 72 | + revoked_at TIMESTAMPTZ, |
| 73 | + -- Ensure key_hash is unique across all tenants (prevents hash collisions |
| 74 | + -- from different tenants accidentally sharing a credential). |
| 75 | + CONSTRAINT uq_api_keys_hash UNIQUE (key_hash) |
| 76 | +); |
| 77 | + |
| 78 | +-- ── Indices ── |
| 79 | +-- The gateway performs key lookup by hash on every authenticated request; |
| 80 | +-- this must be a fast index scan. |
| 81 | +CREATE UNIQUE INDEX idx_api_keys_hash ON api_keys (key_hash); |
| 82 | + |
| 83 | +-- Lists all keys for a tenant in the management UI. |
| 84 | +CREATE INDEX idx_api_keys_tenant ON api_keys (tenant_id); |
| 85 | + |
| 86 | +-- Prefix lookup for display deduplication. |
| 87 | +CREATE UNIQUE INDEX idx_api_keys_prefix ON api_keys (key_prefix); |
| 88 | + |
| 89 | +-- ── Row-Level Security ─ |
| 90 | +ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY; |
| 91 | + |
| 92 | +-- The SELECT / INSERT / UPDATE / DELETE policy: |
| 93 | +-- • SELECT: a connection can only read keys that belong to its current tenant. |
| 94 | +-- • INSERT: tenant_id must equal the current tenant (the CHECK option enforces |
| 95 | +-- this on write so a service cannot accidentally insert a key for another |
| 96 | +-- tenant). |
| 97 | +-- • The gateway's superuser connection that performs the initial key-lookup |
| 98 | +-- (before tenant context is known) must use SET LOCAL to temporarily bypass |
| 99 | +-- RLS — it should do so only for the hash lookup, then immediately set the |
| 100 | +-- tenant context for the rest of the transaction. |
| 101 | +CREATE POLICY api_keys_tenant_isolation ON api_keys |
| 102 | + USING (tenant_id = current_setting('app.current_tenant_id', TRUE)::UUID) |
| 103 | + WITH CHECK (tenant_id = current_setting('app.current_tenant_id', TRUE)::UUID); |
| 104 | + |
| 105 | +-- ── Trigger ── |
| 106 | +-- api_keys are immutable after creation; there is no updated_at column and |
| 107 | +-- therefore no trigger needed here. |
0 commit comments