224 lines
9.3 KiB
SQL
224 lines
9.3 KiB
SQL
-- Migration: Identity and Authentication
|
|
-- Description: Creates identity, permission, and policy tables
|
|
-- Version: 20250101000002
|
|
|
|
-- ============================================================================
|
|
-- IDENTITY TABLE
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE identity (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
login TEXT NOT NULL UNIQUE,
|
|
display_name TEXT,
|
|
password_hash TEXT,
|
|
attributes JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_identity_login ON identity(login);
|
|
CREATE INDEX idx_identity_created ON identity(created DESC);
|
|
CREATE INDEX idx_identity_password_hash ON identity(password_hash) WHERE password_hash IS NOT NULL;
|
|
CREATE INDEX idx_identity_attributes_gin ON identity USING GIN (attributes);
|
|
|
|
-- Trigger
|
|
CREATE TRIGGER update_identity_updated
|
|
BEFORE UPDATE ON identity
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_column();
|
|
|
|
-- Comments
|
|
COMMENT ON TABLE identity IS 'Identities represent users or service accounts';
|
|
COMMENT ON COLUMN identity.login IS 'Unique login identifier';
|
|
COMMENT ON COLUMN identity.display_name IS 'Human-readable name';
|
|
COMMENT ON COLUMN identity.password_hash IS 'Argon2 hashed password for authentication (NULL for service accounts or external auth)';
|
|
COMMENT ON COLUMN identity.attributes IS 'Custom attributes (email, groups, etc.)';
|
|
|
|
-- ============================================================================
|
|
-- ADD FOREIGN KEY CONSTRAINTS TO EXISTING TABLES
|
|
-- ============================================================================
|
|
|
|
-- Add foreign key constraint for pack.installed_by now that identity table exists
|
|
ALTER TABLE pack
|
|
ADD CONSTRAINT fk_pack_installed_by
|
|
FOREIGN KEY (installed_by)
|
|
REFERENCES identity(id)
|
|
ON DELETE SET NULL;
|
|
|
|
-- ============================================================================
|
|
|
|
-- ============================================================================
|
|
-- PERMISSION_SET TABLE
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE permission_set (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
ref TEXT NOT NULL UNIQUE,
|
|
pack BIGINT REFERENCES pack(id) ON DELETE CASCADE,
|
|
pack_ref TEXT,
|
|
label TEXT,
|
|
description TEXT,
|
|
grants JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT permission_set_ref_lowercase CHECK (ref = LOWER(ref)),
|
|
CONSTRAINT permission_set_ref_format CHECK (ref ~ '^[^.]+\.[^.]+$')
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_permission_set_ref ON permission_set(ref);
|
|
CREATE INDEX idx_permission_set_pack ON permission_set(pack);
|
|
CREATE INDEX idx_permission_set_created ON permission_set(created DESC);
|
|
|
|
-- Trigger
|
|
CREATE TRIGGER update_permission_set_updated
|
|
BEFORE UPDATE ON permission_set
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_column();
|
|
|
|
-- Comments
|
|
COMMENT ON TABLE permission_set IS 'Permission sets group permissions together (like roles)';
|
|
COMMENT ON COLUMN permission_set.ref IS 'Unique permission set reference (format: pack.name)';
|
|
COMMENT ON COLUMN permission_set.label IS 'Human-readable name';
|
|
COMMENT ON COLUMN permission_set.grants IS 'Array of permission grants';
|
|
|
|
-- ============================================================================
|
|
|
|
-- ============================================================================
|
|
-- PERMISSION_ASSIGNMENT TABLE
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE permission_assignment (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
identity BIGINT NOT NULL REFERENCES identity(id) ON DELETE CASCADE,
|
|
permset BIGINT NOT NULL REFERENCES permission_set(id) ON DELETE CASCADE,
|
|
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Unique constraint to prevent duplicate assignments
|
|
CONSTRAINT unique_identity_permset UNIQUE (identity, permset)
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_permission_assignment_identity ON permission_assignment(identity);
|
|
CREATE INDEX idx_permission_assignment_permset ON permission_assignment(permset);
|
|
CREATE INDEX idx_permission_assignment_created ON permission_assignment(created DESC);
|
|
CREATE INDEX idx_permission_assignment_identity_created ON permission_assignment(identity, created DESC);
|
|
CREATE INDEX idx_permission_assignment_permset_created ON permission_assignment(permset, created DESC);
|
|
|
|
-- Comments
|
|
COMMENT ON TABLE permission_assignment IS 'Links identities to permission sets (many-to-many)';
|
|
COMMENT ON COLUMN permission_assignment.identity IS 'Identity being granted permissions';
|
|
COMMENT ON COLUMN permission_assignment.permset IS 'Permission set being assigned';
|
|
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE identity
|
|
ADD COLUMN frozen BOOLEAN NOT NULL DEFAULT false;
|
|
|
|
CREATE INDEX idx_identity_frozen ON identity(frozen);
|
|
|
|
COMMENT ON COLUMN identity.frozen IS 'If true, authentication is blocked for this identity';
|
|
|
|
CREATE TABLE identity_role_assignment (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
identity BIGINT NOT NULL REFERENCES identity(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL,
|
|
source TEXT NOT NULL DEFAULT 'manual',
|
|
managed BOOLEAN NOT NULL DEFAULT false,
|
|
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT unique_identity_role_assignment UNIQUE (identity, role)
|
|
);
|
|
|
|
CREATE INDEX idx_identity_role_assignment_identity
|
|
ON identity_role_assignment(identity);
|
|
CREATE INDEX idx_identity_role_assignment_role
|
|
ON identity_role_assignment(role);
|
|
CREATE INDEX idx_identity_role_assignment_source
|
|
ON identity_role_assignment(source);
|
|
|
|
CREATE TRIGGER update_identity_role_assignment_updated
|
|
BEFORE UPDATE ON identity_role_assignment
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_column();
|
|
|
|
COMMENT ON TABLE identity_role_assignment IS 'Links identities to role labels from manual assignment or external identity providers';
|
|
COMMENT ON COLUMN identity_role_assignment.role IS 'Opaque role/group label (e.g. IDP group name)';
|
|
COMMENT ON COLUMN identity_role_assignment.source IS 'Where the role assignment originated (manual, oidc, ldap, sync, etc.)';
|
|
COMMENT ON COLUMN identity_role_assignment.managed IS 'True when the assignment is managed by external sync and should not be edited manually';
|
|
|
|
CREATE TABLE permission_set_role_assignment (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
permset BIGINT NOT NULL REFERENCES permission_set(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL,
|
|
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT unique_permission_set_role_assignment UNIQUE (permset, role)
|
|
);
|
|
|
|
CREATE INDEX idx_permission_set_role_assignment_permset
|
|
ON permission_set_role_assignment(permset);
|
|
CREATE INDEX idx_permission_set_role_assignment_role
|
|
ON permission_set_role_assignment(role);
|
|
|
|
COMMENT ON TABLE permission_set_role_assignment IS 'Links permission sets to role labels for role-based grant expansion';
|
|
COMMENT ON COLUMN permission_set_role_assignment.role IS 'Opaque role/group label associated with the permission set';
|
|
|
|
-- ============================================================================
|
|
|
|
-- ============================================================================
|
|
-- POLICY TABLE
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE policy (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
ref TEXT NOT NULL UNIQUE,
|
|
pack BIGINT REFERENCES pack(id) ON DELETE CASCADE,
|
|
pack_ref TEXT,
|
|
action BIGINT, -- Forward reference to action table, will add constraint in next migration
|
|
action_ref TEXT,
|
|
parameters TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
|
method policy_method_enum NOT NULL,
|
|
threshold INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
tags TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
|
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT policy_ref_lowercase CHECK (ref = LOWER(ref)),
|
|
CONSTRAINT policy_ref_format CHECK (ref ~ '^[^.]+\.[^.]+$'),
|
|
CONSTRAINT policy_threshold_positive CHECK (threshold > 0)
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_policy_ref ON policy(ref);
|
|
CREATE INDEX idx_policy_pack ON policy(pack);
|
|
CREATE INDEX idx_policy_action ON policy(action);
|
|
CREATE INDEX idx_policy_created ON policy(created DESC);
|
|
CREATE INDEX idx_policy_action_created ON policy(action, created DESC);
|
|
CREATE INDEX idx_policy_pack_created ON policy(pack, created DESC);
|
|
CREATE INDEX idx_policy_parameters_gin ON policy USING GIN (parameters);
|
|
CREATE INDEX idx_policy_tags_gin ON policy USING GIN (tags);
|
|
|
|
-- Trigger
|
|
CREATE TRIGGER update_policy_updated
|
|
BEFORE UPDATE ON policy
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_column();
|
|
|
|
-- Comments
|
|
COMMENT ON TABLE policy IS 'Policies define execution controls (rate limiting, concurrency)';
|
|
COMMENT ON COLUMN policy.ref IS 'Unique policy reference (format: pack.name)';
|
|
COMMENT ON COLUMN policy.action IS 'Action this policy applies to';
|
|
COMMENT ON COLUMN policy.parameters IS 'Parameter names used for policy grouping';
|
|
COMMENT ON COLUMN policy.method IS 'How to handle policy violations (cancel/enqueue)';
|
|
COMMENT ON COLUMN policy.threshold IS 'Numeric limit (e.g., max concurrent executions)';
|
|
|
|
-- ============================================================================
|