feat(authority): wire auto-migration + idempotent schema (AGENTS.md §2.7)
Brings Authority into §2.7 compliance. Previously AutoMigrate=true was set
in Program.cs but no runner was wired; 001_initial_schema.sql was
non-idempotent so wiring AddStartupMigrations against a pre-bootstrapped
DB crash-looped. Discovered during DEPRECATE-003 when the new drop
migration couldn't apply via Authority's own startup path.
Idempotency fixes in 001_initial_schema.sql:
- CREATE INDEX → CREATE INDEX IF NOT EXISTS (27 indexes)
- CREATE TRIGGER → DROP TRIGGER IF EXISTS + CREATE TRIGGER (3 triggers)
- CREATE POLICY → DROP POLICY IF EXISTS + CREATE POLICY (12 policies)
- CREATE TABLE / FUNCTION (OR REPLACE) / RLS ENABLE / role DO blocks were
already idempotent — left unchanged
Wiring:
- AddStartupMigrations("authority", "Authority", typeof(AuthorityDataSource)
.Assembly) called inside RegisterAuthorityServices (canonical
Signals/Scanner pattern).
- Stale options.AutoMigrate = true + options.MigrationsPath removed from
Program.cs.
- Migrations\_archived\** excluded from the EmbeddedResource glob.
Init script cleanup (migrations own schema authority now):
- 04-authority-schema.sql: 569 lines → 60 lines (schema shells + guarded
default-tenant seed fallback only; all DDL removed)
- 04b-authority-dedicated-schema.sql: same reduction for dedicated DB
Verification sequence — all PASS:
1. Green-field replay: 001 runs twice with zero semantic drift (pg_dump
diff shows only session restrict nonce).
2. Wire against pre-migrated volume: runner applies 001+002 in 209ms, no
crash-loop.
3. Wire + fresh schema: migrates 20 tables from empty in 395ms.
4. Idempotent restart: "Database is up to date", pure no-op.
Sprint SPRINT_20260422_003_Authority_auto_migration_compliance created
and archived in the same pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,625 +1,63 @@
|
||||
-- Authority Schema: Consolidated Initial Schema
|
||||
-- Consolidated from migrations 001-005 (pre_1.0 archived)
|
||||
-- Creates the complete authority schema for IAM, tenants, users, tokens, RLS, and audit
|
||||
-- Authority Schema: Bootstrap Fallback (NOT the migration authority).
|
||||
--
|
||||
-- AGENTS.md §2.7 / SPRINT_20260422_003 (AUTH-MIGRATE-003):
|
||||
-- The Authority service now owns its schema via embedded SQL migrations in
|
||||
-- `src/Authority/__Libraries/StellaOps.Authority.Persistence/Migrations/*.sql`
|
||||
-- applied by `StellaOps.Infrastructure.Postgres.Migrations.AddStartupMigrations`
|
||||
-- on service startup. This init script is retained only as a first-run fallback
|
||||
-- for the shared `stellaops_platform` database; it creates the `authority`
|
||||
-- namespace shells so compose health checks can start, but DDL authority lives
|
||||
-- with the service migration runner.
|
||||
--
|
||||
-- Table/index/trigger/policy creation was removed in AUTH-MIGRATE-003. The
|
||||
-- Authority service is responsible for applying all schema DDL. This file
|
||||
-- only:
|
||||
-- 1. Creates the `authority` and `authority_app` schemas so the service can
|
||||
-- connect before its first migration run (migration 001 also does this
|
||||
-- via CREATE SCHEMA IF NOT EXISTS, but having the schema pre-exist keeps
|
||||
-- legacy bootstrap scripts that reference `authority.*` qualifiers safe).
|
||||
-- 2. Seeds the `default` tenant row only if the `authority.tenants` table
|
||||
-- already exists (guarded so the init script is a pure no-op when the
|
||||
-- migration runner has not yet populated the schema).
|
||||
--
|
||||
-- First-party clients (stella-ops-ui, stellaops-cli) and demo seed data come
|
||||
-- from migration `S001_demo_seed.sql` in the persistence assembly, not here.
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 1: Schema Creation
|
||||
-- SECTION 1: Schema shells (idempotent)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS authority;
|
||||
CREATE SCHEMA IF NOT EXISTS authority_app;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 2: Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION authority.update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Tenant context helper function for RLS
|
||||
CREATE OR REPLACE FUNCTION authority_app.require_current_tenant()
|
||||
RETURNS TEXT
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
v_tenant TEXT;
|
||||
BEGIN
|
||||
v_tenant := current_setting('app.tenant_id', true);
|
||||
IF v_tenant IS NULL OR v_tenant = '' THEN
|
||||
RAISE EXCEPTION 'app.tenant_id session variable not set'
|
||||
USING HINT = 'Set via: SELECT set_config(''app.tenant_id'', ''<tenant>'', false)',
|
||||
ERRCODE = 'P0001';
|
||||
END IF;
|
||||
RETURN v_tenant;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION authority_app.require_current_tenant() FROM PUBLIC;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 3: Core Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- Tenants table (NOT RLS-protected - defines tenant boundaries)
|
||||
CREATE TABLE IF NOT EXISTS authority.tenants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'deleted')),
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT,
|
||||
updated_by TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tenants_status ON authority.tenants(status);
|
||||
CREATE INDEX idx_tenants_created_at ON authority.tenants(created_at);
|
||||
|
||||
COMMENT ON TABLE authority.tenants IS
|
||||
'Tenant registry. Not RLS-protected - defines tenant boundaries for the system.';
|
||||
|
||||
-- Users table
|
||||
CREATE TABLE IF NOT EXISTS authority.users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
username TEXT NOT NULL,
|
||||
email TEXT,
|
||||
display_name TEXT,
|
||||
password_hash TEXT,
|
||||
password_salt TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
password_algorithm TEXT DEFAULT 'argon2id',
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'locked', 'deleted')),
|
||||
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
mfa_secret TEXT,
|
||||
mfa_backup_codes TEXT,
|
||||
failed_login_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until TIMESTAMPTZ,
|
||||
last_login_at TIMESTAMPTZ,
|
||||
password_changed_at TIMESTAMPTZ,
|
||||
last_password_change_at TIMESTAMPTZ,
|
||||
password_expires_at TIMESTAMPTZ,
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT,
|
||||
updated_by TEXT,
|
||||
UNIQUE(tenant_id, username),
|
||||
UNIQUE(tenant_id, email)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_tenant_id ON authority.users(tenant_id);
|
||||
CREATE INDEX idx_users_status ON authority.users(tenant_id, status);
|
||||
CREATE INDEX idx_users_email ON authority.users(tenant_id, email);
|
||||
|
||||
-- Roles table
|
||||
CREATE TABLE IF NOT EXISTS authority.roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
description TEXT,
|
||||
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(tenant_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_roles_tenant_id ON authority.roles(tenant_id);
|
||||
|
||||
-- Permissions table
|
||||
CREATE TABLE IF NOT EXISTS authority.permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
name TEXT NOT NULL,
|
||||
resource TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(tenant_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_permissions_tenant_id ON authority.permissions(tenant_id);
|
||||
CREATE INDEX idx_permissions_resource ON authority.permissions(tenant_id, resource);
|
||||
|
||||
-- Role-Permission assignments
|
||||
CREATE TABLE IF NOT EXISTS authority.role_permissions (
|
||||
role_id UUID NOT NULL REFERENCES authority.roles(id) ON DELETE CASCADE,
|
||||
permission_id UUID NOT NULL REFERENCES authority.permissions(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (role_id, permission_id)
|
||||
);
|
||||
|
||||
-- User-Role assignments
|
||||
CREATE TABLE IF NOT EXISTS authority.user_roles (
|
||||
user_id UUID NOT NULL REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES authority.roles(id) ON DELETE CASCADE,
|
||||
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
granted_by TEXT,
|
||||
expires_at TIMESTAMPTZ,
|
||||
PRIMARY KEY (user_id, role_id)
|
||||
);
|
||||
|
||||
-- API Keys table
|
||||
CREATE TABLE IF NOT EXISTS authority.api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL,
|
||||
key_prefix TEXT NOT NULL,
|
||||
scopes TEXT[] NOT NULL DEFAULT '{}',
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked', 'expired')),
|
||||
last_used_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_by TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_api_keys_tenant_id ON authority.api_keys(tenant_id);
|
||||
CREATE INDEX idx_api_keys_key_prefix ON authority.api_keys(key_prefix);
|
||||
CREATE INDEX idx_api_keys_user_id ON authority.api_keys(user_id);
|
||||
CREATE INDEX idx_api_keys_status ON authority.api_keys(tenant_id, status);
|
||||
|
||||
-- Tokens table (access tokens)
|
||||
CREATE TABLE IF NOT EXISTS authority.tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
token_type TEXT NOT NULL DEFAULT 'access' CHECK (token_type IN ('access', 'refresh', 'api')),
|
||||
scopes TEXT[] NOT NULL DEFAULT '{}',
|
||||
client_id TEXT,
|
||||
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_by TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tokens_tenant_id ON authority.tokens(tenant_id);
|
||||
CREATE INDEX idx_tokens_user_id ON authority.tokens(user_id);
|
||||
CREATE INDEX idx_tokens_expires_at ON authority.tokens(expires_at);
|
||||
CREATE INDEX idx_tokens_token_hash ON authority.tokens(token_hash);
|
||||
|
||||
-- Refresh Tokens table
|
||||
CREATE TABLE IF NOT EXISTS authority.refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID NOT NULL REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
access_token_id UUID REFERENCES authority.tokens(id) ON DELETE SET NULL,
|
||||
client_id TEXT,
|
||||
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_by TEXT,
|
||||
replaced_by UUID,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_refresh_tokens_tenant_id ON authority.refresh_tokens(tenant_id);
|
||||
CREATE INDEX idx_refresh_tokens_user_id ON authority.refresh_tokens(user_id);
|
||||
CREATE INDEX idx_refresh_tokens_expires_at ON authority.refresh_tokens(expires_at);
|
||||
|
||||
-- Sessions table
|
||||
CREATE TABLE IF NOT EXISTS authority.sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID NOT NULL REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
session_token_hash TEXT NOT NULL UNIQUE,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_activity_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
ended_at TIMESTAMPTZ,
|
||||
end_reason TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sessions_tenant_id ON authority.sessions(tenant_id);
|
||||
CREATE INDEX idx_sessions_user_id ON authority.sessions(user_id);
|
||||
CREATE INDEX idx_sessions_expires_at ON authority.sessions(expires_at);
|
||||
|
||||
-- Audit log table
|
||||
CREATE TABLE IF NOT EXISTS authority.audit (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
user_id UUID,
|
||||
action TEXT NOT NULL,
|
||||
resource_type TEXT NOT NULL,
|
||||
resource_id TEXT,
|
||||
old_value JSONB,
|
||||
new_value JSONB,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
correlation_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_tenant_id ON authority.audit(tenant_id);
|
||||
CREATE INDEX idx_audit_user_id ON authority.audit(user_id);
|
||||
CREATE INDEX idx_audit_action ON authority.audit(action);
|
||||
CREATE INDEX idx_audit_resource ON authority.audit(resource_type, resource_id);
|
||||
CREATE INDEX idx_audit_created_at ON authority.audit(created_at);
|
||||
CREATE INDEX idx_audit_correlation_id ON authority.audit(correlation_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 4: OIDC and Mongo Store Equivalent Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- Bootstrap invites
|
||||
CREATE TABLE IF NOT EXISTS authority.bootstrap_invites (
|
||||
id TEXT PRIMARY KEY,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
type TEXT NOT NULL,
|
||||
provider TEXT,
|
||||
target TEXT,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
issued_by TEXT,
|
||||
reserved_until TIMESTAMPTZ,
|
||||
reserved_by TEXT,
|
||||
consumed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
-- Service accounts
|
||||
CREATE TABLE IF NOT EXISTS authority.service_accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
account_id TEXT NOT NULL UNIQUE,
|
||||
tenant TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
allowed_scopes TEXT[] NOT NULL DEFAULT '{}',
|
||||
authorized_clients TEXT[] NOT NULL DEFAULT '{}',
|
||||
attributes JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_service_accounts_tenant ON authority.service_accounts(tenant);
|
||||
|
||||
-- Clients
|
||||
CREATE TABLE IF NOT EXISTS authority.clients (
|
||||
id TEXT PRIMARY KEY,
|
||||
client_id TEXT NOT NULL UNIQUE,
|
||||
client_secret TEXT,
|
||||
secret_hash TEXT,
|
||||
display_name TEXT,
|
||||
description TEXT,
|
||||
plugin TEXT,
|
||||
sender_constraint TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
redirect_uris TEXT[] NOT NULL DEFAULT '{}',
|
||||
post_logout_redirect_uris TEXT[] NOT NULL DEFAULT '{}',
|
||||
allowed_scopes TEXT[] NOT NULL DEFAULT '{}',
|
||||
allowed_grant_types TEXT[] NOT NULL DEFAULT '{}',
|
||||
require_client_secret BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
require_pkce BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
allow_plain_text_pkce BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
client_type TEXT,
|
||||
properties JSONB NOT NULL DEFAULT '{}',
|
||||
certificate_bindings JSONB NOT NULL DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Revocations
|
||||
CREATE TABLE IF NOT EXISTS authority.revocations (
|
||||
id TEXT PRIMARY KEY,
|
||||
category TEXT NOT NULL,
|
||||
revocation_id TEXT NOT NULL,
|
||||
subject_id TEXT,
|
||||
client_id TEXT,
|
||||
token_id TEXT,
|
||||
reason TEXT NOT NULL,
|
||||
reason_description TEXT,
|
||||
revoked_at TIMESTAMPTZ NOT NULL,
|
||||
effective_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_revocations_category_revocation_id
|
||||
ON authority.revocations(category, revocation_id);
|
||||
|
||||
-- Login attempts
|
||||
CREATE TABLE IF NOT EXISTS authority.login_attempts (
|
||||
id TEXT PRIMARY KEY,
|
||||
subject_id TEXT,
|
||||
client_id TEXT,
|
||||
event_type TEXT NOT NULL,
|
||||
outcome TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
occurred_at TIMESTAMPTZ NOT NULL,
|
||||
properties JSONB NOT NULL DEFAULT '[]'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_login_attempts_subject ON authority.login_attempts(subject_id, occurred_at DESC);
|
||||
|
||||
-- OIDC tokens
|
||||
CREATE TABLE IF NOT EXISTS authority.oidc_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
token_id TEXT NOT NULL UNIQUE,
|
||||
subject_id TEXT,
|
||||
client_id TEXT,
|
||||
token_type TEXT NOT NULL,
|
||||
reference_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ,
|
||||
redeemed_at TIMESTAMPTZ,
|
||||
payload TEXT,
|
||||
properties JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_tokens_subject ON authority.oidc_tokens(subject_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_tokens_client ON authority.oidc_tokens(client_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_tokens_reference ON authority.oidc_tokens(reference_id);
|
||||
|
||||
-- OIDC refresh tokens
|
||||
CREATE TABLE IF NOT EXISTS authority.oidc_refresh_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
token_id TEXT NOT NULL UNIQUE,
|
||||
subject_id TEXT,
|
||||
client_id TEXT,
|
||||
handle TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ,
|
||||
consumed_at TIMESTAMPTZ,
|
||||
payload TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_refresh_tokens_subject ON authority.oidc_refresh_tokens(subject_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_refresh_tokens_handle ON authority.oidc_refresh_tokens(handle);
|
||||
|
||||
-- Airgap audit
|
||||
CREATE TABLE IF NOT EXISTS authority.airgap_audit (
|
||||
id TEXT PRIMARY KEY,
|
||||
event_type TEXT NOT NULL,
|
||||
operator_id TEXT,
|
||||
component_id TEXT,
|
||||
outcome TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
occurred_at TIMESTAMPTZ NOT NULL,
|
||||
properties JSONB NOT NULL DEFAULT '[]'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_airgap_audit_occurred_at ON authority.airgap_audit(occurred_at DESC);
|
||||
|
||||
-- Revocation export state (singleton row with optimistic concurrency)
|
||||
CREATE TABLE IF NOT EXISTS authority.revocation_export_state (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
sequence BIGINT NOT NULL DEFAULT 0,
|
||||
bundle_id TEXT,
|
||||
issued_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- Offline Kit Audit
|
||||
CREATE TABLE IF NOT EXISTS authority.offline_kit_audit (
|
||||
event_id UUID PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
timestamp TIMESTAMPTZ NOT NULL,
|
||||
actor TEXT NOT NULL,
|
||||
details JSONB NOT NULL,
|
||||
result TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_ts ON authority.offline_kit_audit(timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_type ON authority.offline_kit_audit(event_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_tenant_ts ON authority.offline_kit_audit(tenant_id, timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_result ON authority.offline_kit_audit(tenant_id, result, timestamp DESC);
|
||||
|
||||
-- Verdict manifests table
|
||||
CREATE TABLE IF NOT EXISTS authority.verdict_manifests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
manifest_id TEXT NOT NULL,
|
||||
tenant TEXT NOT NULL,
|
||||
asset_digest TEXT NOT NULL,
|
||||
vulnerability_id TEXT NOT NULL,
|
||||
inputs_json JSONB NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('affected', 'not_affected', 'fixed', 'under_investigation')),
|
||||
confidence DOUBLE PRECISION NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
|
||||
result_json JSONB NOT NULL,
|
||||
policy_hash TEXT NOT NULL,
|
||||
lattice_version TEXT NOT NULL,
|
||||
evaluated_at TIMESTAMPTZ NOT NULL,
|
||||
manifest_digest TEXT NOT NULL,
|
||||
signature_base64 TEXT,
|
||||
rekor_log_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_verdict_manifest_id UNIQUE (tenant, manifest_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_asset_vuln
|
||||
ON authority.verdict_manifests(tenant, asset_digest, vulnerability_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_policy
|
||||
ON authority.verdict_manifests(tenant, policy_hash, lattice_version);
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_time
|
||||
ON authority.verdict_manifests USING BRIN (evaluated_at);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_verdict_replay
|
||||
ON authority.verdict_manifests(tenant, asset_digest, vulnerability_id, policy_hash, lattice_version);
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_digest
|
||||
ON authority.verdict_manifests(manifest_digest);
|
||||
|
||||
COMMENT ON TABLE authority.verdict_manifests IS 'VEX verdict manifests for deterministic replay verification';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 4b: Bootstrap Default Tenant
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO authority.tenants (tenant_id, name, display_name, status)
|
||||
VALUES ('default', 'Default', 'Default Tenant', 'active')
|
||||
ON CONFLICT (tenant_id) DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 5: Triggers
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TRIGGER trg_tenants_updated_at
|
||||
BEFORE UPDATE ON authority.tenants
|
||||
FOR EACH ROW EXECUTE FUNCTION authority.update_updated_at();
|
||||
|
||||
CREATE TRIGGER trg_users_updated_at
|
||||
BEFORE UPDATE ON authority.users
|
||||
FOR EACH ROW EXECUTE FUNCTION authority.update_updated_at();
|
||||
|
||||
CREATE TRIGGER trg_roles_updated_at
|
||||
BEFORE UPDATE ON authority.roles
|
||||
FOR EACH ROW EXECUTE FUNCTION authority.update_updated_at();
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 6: Row-Level Security
|
||||
-- ============================================================================
|
||||
|
||||
-- authority.users
|
||||
ALTER TABLE authority.users ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.users FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY users_tenant_isolation ON authority.users
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.roles
|
||||
ALTER TABLE authority.roles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.roles FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY roles_tenant_isolation ON authority.roles
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.permissions
|
||||
ALTER TABLE authority.permissions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.permissions FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY permissions_tenant_isolation ON authority.permissions
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.role_permissions (FK-based, inherits from roles)
|
||||
ALTER TABLE authority.role_permissions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.role_permissions FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY role_permissions_tenant_isolation ON authority.role_permissions
|
||||
FOR ALL
|
||||
USING (
|
||||
role_id IN (
|
||||
SELECT id FROM authority.roles
|
||||
WHERE tenant_id = authority_app.require_current_tenant()
|
||||
)
|
||||
);
|
||||
|
||||
-- authority.user_roles (FK-based, inherits from users)
|
||||
ALTER TABLE authority.user_roles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.user_roles FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY user_roles_tenant_isolation ON authority.user_roles
|
||||
FOR ALL
|
||||
USING (
|
||||
user_id IN (
|
||||
SELECT id FROM authority.users
|
||||
WHERE tenant_id = authority_app.require_current_tenant()
|
||||
)
|
||||
);
|
||||
|
||||
-- authority.api_keys
|
||||
ALTER TABLE authority.api_keys ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.api_keys FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY api_keys_tenant_isolation ON authority.api_keys
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.tokens
|
||||
ALTER TABLE authority.tokens ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.tokens FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tokens_tenant_isolation ON authority.tokens
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.refresh_tokens
|
||||
ALTER TABLE authority.refresh_tokens ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.refresh_tokens FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY refresh_tokens_tenant_isolation ON authority.refresh_tokens
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.sessions
|
||||
ALTER TABLE authority.sessions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.sessions FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY sessions_tenant_isolation ON authority.sessions
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.audit
|
||||
ALTER TABLE authority.audit ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.audit FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY audit_tenant_isolation ON authority.audit
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.offline_kit_audit
|
||||
ALTER TABLE authority.offline_kit_audit ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.offline_kit_audit FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY offline_kit_audit_tenant_isolation ON authority.offline_kit_audit
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.verdict_manifests
|
||||
ALTER TABLE authority.verdict_manifests ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY verdict_tenant_isolation ON authority.verdict_manifests
|
||||
USING (tenant = current_setting('app.current_tenant', true))
|
||||
WITH CHECK (tenant = current_setting('app.current_tenant', true));
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 7: Roles and Permissions
|
||||
-- SECTION 2: Guarded default tenant seed
|
||||
-- ============================================================================
|
||||
--
|
||||
-- Only inserts if `authority.tenants` already exists (i.e. the migration
|
||||
-- runner has already applied 001_initial_schema.sql in a previous boot).
|
||||
-- On a fresh volume where Authority has not yet started, this block is a
|
||||
-- no-op and the `default` tenant is seeded by the migration path instead.
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authority_admin') THEN
|
||||
CREATE ROLE authority_admin WITH NOLOGIN BYPASSRLS;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Grant permissions (if role exists)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'stellaops_app') THEN
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON authority.verdict_manifests TO stellaops_app;
|
||||
GRANT USAGE ON SCHEMA authority TO stellaops_app;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'authority'
|
||||
AND table_name = 'tenants'
|
||||
) THEN
|
||||
INSERT INTO authority.tenants (tenant_id, name, display_name, status)
|
||||
VALUES ('default', 'Default', 'Default Tenant', 'active')
|
||||
ON CONFLICT (tenant_id) DO NOTHING;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 8: Bootstrap Notes
|
||||
-- Bootstrap Notes
|
||||
-- ============================================================================
|
||||
|
||||
-- First-party clients are seeded by the Authority standard plugin at runtime.
|
||||
-- The first administrator is created through the setup wizard, not compose SQL.
|
||||
--
|
||||
-- First-party clients are seeded by the Authority standard plugin at runtime
|
||||
-- and by the S001_demo_seed.sql migration. The first administrator is created
|
||||
-- through the setup wizard, not compose SQL.
|
||||
|
||||
@@ -1,455 +1,32 @@
|
||||
-- Authority Schema: Applied to dedicated stellaops_authority database.
|
||||
-- Authority Schema (dedicated DB): Bootstrap Fallback.
|
||||
--
|
||||
-- This is the same schema as 04-authority-schema.sql but targets
|
||||
-- the dedicated Authority database for connection pool isolation.
|
||||
-- 04-authority-schema.sql continues to run against stellaops_platform
|
||||
-- for backward compatibility.
|
||||
-- Counterpart to 04-authority-schema.sql. Targets the dedicated
|
||||
-- `stellaops_authority` database created by 00-create-authority-db.sql for
|
||||
-- OIDC connection-pool isolation.
|
||||
--
|
||||
-- AGENTS.md §2.7 / SPRINT_20260422_003 (AUTH-MIGRATE-003): Schema DDL is
|
||||
-- owned by the Authority service migration runner (embedded SQL in
|
||||
-- `StellaOps.Authority.Persistence.dll`). This file only creates the
|
||||
-- `authority` / `authority_app` schemas so the service can connect, and
|
||||
-- guards a `default` tenant fallback seed.
|
||||
|
||||
\c stellaops_authority
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 1: Schema Creation
|
||||
-- ============================================================================
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS authority;
|
||||
CREATE SCHEMA IF NOT EXISTS authority_app;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 2: Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION authority.update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
-- Guarded default tenant seed (no-op if authority.tenants does not yet exist).
|
||||
DO $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION authority_app.require_current_tenant()
|
||||
RETURNS TEXT
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
v_tenant TEXT;
|
||||
BEGIN
|
||||
v_tenant := current_setting('app.tenant_id', true);
|
||||
IF v_tenant IS NULL OR v_tenant = '' THEN
|
||||
RAISE EXCEPTION 'app.tenant_id session variable not set'
|
||||
USING HINT = 'Set via: SELECT set_config(''app.tenant_id'', ''<tenant>'', false)',
|
||||
ERRCODE = 'P0001';
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'authority'
|
||||
AND table_name = 'tenants'
|
||||
) THEN
|
||||
INSERT INTO authority.tenants (tenant_id, name, display_name, status)
|
||||
VALUES ('default', 'Default', 'Default Tenant', 'active')
|
||||
ON CONFLICT (tenant_id) DO NOTHING;
|
||||
END IF;
|
||||
RETURN v_tenant;
|
||||
END;
|
||||
END
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION authority_app.require_current_tenant() FROM PUBLIC;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 3: Core Tables
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.tenants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'deleted')),
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT,
|
||||
updated_by TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_tenants_status ON authority.tenants(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tenants_created_at ON authority.tenants(created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
username TEXT NOT NULL,
|
||||
email TEXT,
|
||||
display_name TEXT,
|
||||
password_hash TEXT,
|
||||
password_salt TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
password_algorithm TEXT DEFAULT 'argon2id',
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'locked', 'deleted')),
|
||||
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
mfa_secret TEXT,
|
||||
mfa_backup_codes TEXT,
|
||||
failed_login_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until TIMESTAMPTZ,
|
||||
last_login_at TIMESTAMPTZ,
|
||||
password_changed_at TIMESTAMPTZ,
|
||||
last_password_change_at TIMESTAMPTZ,
|
||||
password_expires_at TIMESTAMPTZ,
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT,
|
||||
updated_by TEXT,
|
||||
UNIQUE(tenant_id, username),
|
||||
UNIQUE(tenant_id, email)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_tenant_id ON authority.users(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_status ON authority.users(tenant_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON authority.users(tenant_id, email);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
description TEXT,
|
||||
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(tenant_id, name)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_tenant_id ON authority.roles(tenant_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
name TEXT NOT NULL,
|
||||
resource TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(tenant_id, name)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_permissions_tenant_id ON authority.permissions(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_permissions_resource ON authority.permissions(tenant_id, resource);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.role_permissions (
|
||||
role_id UUID NOT NULL REFERENCES authority.roles(id) ON DELETE CASCADE,
|
||||
permission_id UUID NOT NULL REFERENCES authority.permissions(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (role_id, permission_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.user_roles (
|
||||
user_id UUID NOT NULL REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES authority.roles(id) ON DELETE CASCADE,
|
||||
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
granted_by TEXT,
|
||||
expires_at TIMESTAMPTZ,
|
||||
PRIMARY KEY (user_id, role_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL,
|
||||
key_prefix TEXT NOT NULL,
|
||||
scopes TEXT[] NOT NULL DEFAULT '{}',
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked', 'expired')),
|
||||
last_used_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_by TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_tenant_id ON authority.api_keys(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_key_prefix ON authority.api_keys(key_prefix);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON authority.api_keys(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_status ON authority.api_keys(tenant_id, status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
token_type TEXT NOT NULL DEFAULT 'access' CHECK (token_type IN ('access', 'refresh', 'api')),
|
||||
scopes TEXT[] NOT NULL DEFAULT '{}',
|
||||
client_id TEXT,
|
||||
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_by TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_tokens_tenant_id ON authority.tokens(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tokens_user_id ON authority.tokens(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tokens_expires_at ON authority.tokens(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_tokens_token_hash ON authority.tokens(token_hash);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID NOT NULL REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
access_token_id UUID REFERENCES authority.tokens(id) ON DELETE SET NULL,
|
||||
client_id TEXT,
|
||||
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_by TEXT,
|
||||
replaced_by UUID,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_tenant_id ON authority.refresh_tokens(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON authority.refresh_tokens(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON authority.refresh_tokens(expires_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID NOT NULL REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
session_token_hash TEXT NOT NULL UNIQUE,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_activity_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
ended_at TIMESTAMPTZ,
|
||||
end_reason TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_tenant_id ON authority.sessions(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON authority.sessions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON authority.sessions(expires_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.audit (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
user_id UUID,
|
||||
action TEXT NOT NULL,
|
||||
resource_type TEXT NOT NULL,
|
||||
resource_id TEXT,
|
||||
old_value JSONB,
|
||||
new_value JSONB,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
correlation_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_tenant_id ON authority.audit(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_user_id ON authority.audit(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_action ON authority.audit(action);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_resource ON authority.audit(resource_type, resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_created_at ON authority.audit(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_correlation_id ON authority.audit(correlation_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 4: OIDC Tables
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.bootstrap_invites (
|
||||
id TEXT PRIMARY KEY, token TEXT NOT NULL UNIQUE, type TEXT NOT NULL,
|
||||
provider TEXT, target TEXT, expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
issued_by TEXT, reserved_until TIMESTAMPTZ, reserved_by TEXT,
|
||||
consumed BOOLEAN NOT NULL DEFAULT FALSE, status TEXT NOT NULL DEFAULT 'pending',
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.service_accounts (
|
||||
id TEXT PRIMARY KEY, account_id TEXT NOT NULL UNIQUE, tenant TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL, description TEXT, enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
allowed_scopes TEXT[] NOT NULL DEFAULT '{}', authorized_clients TEXT[] NOT NULL DEFAULT '{}',
|
||||
attributes JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_service_accounts_tenant ON authority.service_accounts(tenant);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.clients (
|
||||
id TEXT PRIMARY KEY, client_id TEXT NOT NULL UNIQUE, client_secret TEXT,
|
||||
secret_hash TEXT, display_name TEXT, description TEXT, plugin TEXT,
|
||||
sender_constraint TEXT, enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
redirect_uris TEXT[] NOT NULL DEFAULT '{}', post_logout_redirect_uris TEXT[] NOT NULL DEFAULT '{}',
|
||||
allowed_scopes TEXT[] NOT NULL DEFAULT '{}', allowed_grant_types TEXT[] NOT NULL DEFAULT '{}',
|
||||
require_client_secret BOOLEAN NOT NULL DEFAULT TRUE, require_pkce BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
allow_plain_text_pkce BOOLEAN NOT NULL DEFAULT FALSE, client_type TEXT,
|
||||
properties JSONB NOT NULL DEFAULT '{}', certificate_bindings JSONB NOT NULL DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.revocations (
|
||||
id TEXT PRIMARY KEY, category TEXT NOT NULL, revocation_id TEXT NOT NULL,
|
||||
subject_id TEXT, client_id TEXT, token_id TEXT, reason TEXT NOT NULL,
|
||||
reason_description TEXT, revoked_at TIMESTAMPTZ NOT NULL,
|
||||
effective_at TIMESTAMPTZ NOT NULL, expires_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_revocations_category_revocation_id ON authority.revocations(category, revocation_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.login_attempts (
|
||||
id TEXT PRIMARY KEY, subject_id TEXT, client_id TEXT, event_type TEXT NOT NULL,
|
||||
outcome TEXT NOT NULL, reason TEXT, ip_address TEXT, user_agent TEXT,
|
||||
occurred_at TIMESTAMPTZ NOT NULL, properties JSONB NOT NULL DEFAULT '[]'
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_login_attempts_subject ON authority.login_attempts(subject_id, occurred_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.oidc_tokens (
|
||||
id TEXT PRIMARY KEY, token_id TEXT NOT NULL UNIQUE, subject_id TEXT,
|
||||
client_id TEXT, token_type TEXT NOT NULL, reference_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL, expires_at TIMESTAMPTZ,
|
||||
redeemed_at TIMESTAMPTZ, payload TEXT, properties JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_tokens_subject ON authority.oidc_tokens(subject_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_tokens_client ON authority.oidc_tokens(client_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_tokens_reference ON authority.oidc_tokens(reference_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.oidc_refresh_tokens (
|
||||
id TEXT PRIMARY KEY, token_id TEXT NOT NULL UNIQUE, subject_id TEXT,
|
||||
client_id TEXT, handle TEXT, created_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ, consumed_at TIMESTAMPTZ, payload TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_refresh_tokens_subject ON authority.oidc_refresh_tokens(subject_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_refresh_tokens_handle ON authority.oidc_refresh_tokens(handle);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.airgap_audit (
|
||||
id TEXT PRIMARY KEY, event_type TEXT NOT NULL, operator_id TEXT,
|
||||
component_id TEXT, outcome TEXT NOT NULL, reason TEXT,
|
||||
occurred_at TIMESTAMPTZ NOT NULL, properties JSONB NOT NULL DEFAULT '[]'
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_airgap_audit_occurred_at ON authority.airgap_audit(occurred_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.revocation_export_state (
|
||||
id INT PRIMARY KEY DEFAULT 1, sequence BIGINT NOT NULL DEFAULT 0,
|
||||
bundle_id TEXT, issued_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.offline_kit_audit (
|
||||
event_id UUID PRIMARY KEY, tenant_id TEXT NOT NULL, event_type TEXT NOT NULL,
|
||||
timestamp TIMESTAMPTZ NOT NULL, actor TEXT NOT NULL,
|
||||
details JSONB NOT NULL, result TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_ts ON authority.offline_kit_audit(timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_type ON authority.offline_kit_audit(event_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_tenant_ts ON authority.offline_kit_audit(tenant_id, timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_result ON authority.offline_kit_audit(tenant_id, result, timestamp DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authority.verdict_manifests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
manifest_id TEXT NOT NULL, tenant TEXT NOT NULL,
|
||||
asset_digest TEXT NOT NULL, vulnerability_id TEXT NOT NULL,
|
||||
inputs_json JSONB NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('affected', 'not_affected', 'fixed', 'under_investigation')),
|
||||
confidence DOUBLE PRECISION NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
|
||||
result_json JSONB NOT NULL, policy_hash TEXT NOT NULL,
|
||||
lattice_version TEXT NOT NULL, evaluated_at TIMESTAMPTZ NOT NULL,
|
||||
manifest_digest TEXT NOT NULL, signature_base64 TEXT, rekor_log_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_verdict_manifest_id UNIQUE (tenant, manifest_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_asset_vuln ON authority.verdict_manifests(tenant, asset_digest, vulnerability_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_policy ON authority.verdict_manifests(tenant, policy_hash, lattice_version);
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_time ON authority.verdict_manifests USING BRIN (evaluated_at);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_verdict_replay ON authority.verdict_manifests(tenant, asset_digest, vulnerability_id, policy_hash, lattice_version);
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_digest ON authority.verdict_manifests(manifest_digest);
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 5: Bootstrap Default Tenant
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO authority.tenants (tenant_id, name, display_name, status)
|
||||
VALUES ('default', 'Default', 'Default Tenant', 'active')
|
||||
ON CONFLICT (tenant_id) DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 6: Triggers
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TRIGGER trg_tenants_updated_at BEFORE UPDATE ON authority.tenants FOR EACH ROW EXECUTE FUNCTION authority.update_updated_at();
|
||||
CREATE TRIGGER trg_users_updated_at BEFORE UPDATE ON authority.users FOR EACH ROW EXECUTE FUNCTION authority.update_updated_at();
|
||||
CREATE TRIGGER trg_roles_updated_at BEFORE UPDATE ON authority.roles FOR EACH ROW EXECUTE FUNCTION authority.update_updated_at();
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 7: Row-Level Security
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE authority.users ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.users FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY users_tenant_isolation ON authority.users FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
ALTER TABLE authority.roles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.roles FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY roles_tenant_isolation ON authority.roles FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
ALTER TABLE authority.permissions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.permissions FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY permissions_tenant_isolation ON authority.permissions FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
ALTER TABLE authority.role_permissions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.role_permissions FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY role_permissions_tenant_isolation ON authority.role_permissions FOR ALL
|
||||
USING (role_id IN (SELECT id FROM authority.roles WHERE tenant_id = authority_app.require_current_tenant()));
|
||||
|
||||
ALTER TABLE authority.user_roles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.user_roles FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY user_roles_tenant_isolation ON authority.user_roles FOR ALL
|
||||
USING (user_id IN (SELECT id FROM authority.users WHERE tenant_id = authority_app.require_current_tenant()));
|
||||
|
||||
ALTER TABLE authority.api_keys ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.api_keys FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY api_keys_tenant_isolation ON authority.api_keys FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
ALTER TABLE authority.tokens ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.tokens FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tokens_tenant_isolation ON authority.tokens FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
ALTER TABLE authority.refresh_tokens ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.refresh_tokens FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY refresh_tokens_tenant_isolation ON authority.refresh_tokens FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
ALTER TABLE authority.sessions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.sessions FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY sessions_tenant_isolation ON authority.sessions FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
ALTER TABLE authority.audit ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.audit FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY audit_tenant_isolation ON authority.audit FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
ALTER TABLE authority.offline_kit_audit ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.offline_kit_audit FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY offline_kit_audit_tenant_isolation ON authority.offline_kit_audit FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
ALTER TABLE authority.verdict_manifests ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY verdict_tenant_isolation ON authority.verdict_manifests
|
||||
USING (tenant = current_setting('app.current_tenant', true))
|
||||
WITH CHECK (tenant = current_setting('app.current_tenant', true));
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 8: Roles and Permissions
|
||||
-- ============================================================================
|
||||
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authority_admin') THEN
|
||||
CREATE ROLE authority_admin WITH NOLOGIN BYPASSRLS;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 9: Bootstrap Notes
|
||||
-- ============================================================================
|
||||
|
||||
-- First-party clients are seeded by the Authority standard plugin at runtime.
|
||||
-- The first administrator is created through the setup wizard, not compose SQL.
|
||||
|
||||
Reference in New Issue
Block a user