Isolate Authority DB from Concelier write pressure

Problem: All 46+ services share one PostgreSQL database and connection
pool. When Concelier runs advisory sync jobs (heavy writes), the shared
pool starves Authority's OIDC token validation, causing login timeouts.

Fix: Create a dedicated stellaops_authority database on the same Postgres
instance. Authority gets its own connection string with an independent
Npgsql connection pool (Maximum Pool Size=20, Minimum Pool Size=2).

Changes:
- 00-create-authority-db.sql: Creates stellaops_authority database
- 04b-authority-dedicated-schema.sql: Applies full Authority schema
  (tables, indexes, RLS, triggers, seed data) to the dedicated DB
- docker-compose.stella-ops.yml: New x-postgres-authority-connection
  anchor pointing to stellaops_authority. Authority service env updated.
  Shared pool reduced to Maximum Pool Size=50.

The existing stellaops_platform.authority schema remains for backward
compatibility. Authority reads/writes from the isolated database.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-01 12:32:03 +03:00
parent 79a214d259
commit 88eba753ee
3 changed files with 518 additions and 2 deletions

View File

@@ -0,0 +1,496 @@
-- Authority Schema: Applied to dedicated stellaops_authority database.
--
-- 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.
\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 $$
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';
END IF;
RETURN v_tenant;
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: Seed Data
-- ============================================================================
INSERT INTO authority.tenants (tenant_id, name, display_name, status)
VALUES ('demo-prod', 'Production', 'Demo Production', '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: Demo Seed Data
-- ============================================================================
INSERT INTO authority.roles (id, tenant_id, name, display_name, description, is_system)
VALUES
('a0000002-0000-0000-0000-000000000001', 'demo-prod', 'admin', 'Administrator', 'Full platform access', true),
('a0000002-0000-0000-0000-000000000002', 'demo-prod', 'operator', 'Operator', 'Release and deployment operations', true),
('a0000002-0000-0000-0000-000000000003', 'demo-prod', 'viewer', 'Viewer', 'Read-only access', true)
ON CONFLICT (tenant_id, name) DO NOTHING;
INSERT INTO authority.clients (id, client_id, display_name, description, enabled, redirect_uris, post_logout_redirect_uris, allowed_scopes, allowed_grant_types, require_client_secret, require_pkce, properties)
VALUES
('demo-client-ui', 'stella-ops-ui', 'Stella Ops Console', 'Web UI application', true,
ARRAY['https://stella-ops.local/auth/callback', 'https://stella-ops.local/auth/silent-refresh', 'https://127.1.0.1/auth/callback', 'https://127.1.0.1/auth/silent-refresh'],
ARRAY['https://stella-ops.local/', 'https://127.1.0.1/'],
ARRAY['openid', 'profile', 'email', 'offline_access',
'ui.read', 'ui.admin', 'ui.preferences.read', 'ui.preferences.write',
'authority:tenants.read', 'authority:tenants.write',
'authority:users.read', 'authority:users.write',
'authority:roles.read', 'authority:roles.write',
'authority:clients.read', 'authority:clients.write',
'authority:tokens.read', 'authority:tokens.revoke',
'authority:branding.read', 'authority:branding.write',
'authority.audit.read',
'graph:read', 'sbom:read', 'scanner:read',
'policy:read', 'policy:simulate', 'policy:author', 'policy:review', 'policy:approve',
'policy:run', 'policy:activate', 'policy:audit', 'policy:edit', 'policy:operate', 'policy:publish',
'airgap:seal', 'airgap:status:read',
'orch:read', 'analytics.read', 'advisory:read', 'advisory-ai:view', 'advisory-ai:operate',
'vex:read', 'vexhub:read',
'exceptions:read', 'exceptions:approve', 'aoc:verify', 'findings:read',
'release:read', 'release:write', 'release:publish', 'scheduler:read', 'scheduler:operate',
'notify.viewer', 'notify.operator', 'notify.admin', 'notify.escalate',
'evidence:read',
'export.viewer', 'export.operator', 'export.admin',
'vuln:view', 'vuln:investigate', 'vuln:operate', 'vuln:audit',
'platform.context.read', 'platform.context.write',
'platform.idp.read', 'platform.idp.admin',
'doctor:run', 'doctor:admin', 'ops.health',
'integration:read', 'integration:write', 'integration:operate', 'registry.admin',
'timeline:read', 'timeline:write',
'signer:read', 'signer:sign', 'signer:rotate', 'signer:admin',
'trust:read', 'trust:write', 'trust:admin'],
ARRAY['authorization_code', 'refresh_token'],
false, true, '{"tenant": "demo-prod"}'::jsonb)
ON CONFLICT (client_id) DO NOTHING;