"""Fixed local authority operations and serialized last-administrator protection.""" import sqlalchemy as sa from alembic import op revision = "0006_authority_operations" down_revision = "0005_identity_authority" branch_labels = None depends_on = None OWNER = "kf_oj_identity_authority_owner" def upgrade() -> None: op.execute(f""" DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='{OWNER}') THEN CREATE ROLE {OWNER} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS; ELSIF EXISTS (SELECT 1 FROM pg_roles WHERE rolname='{OWNER}' AND (rolcanlogin OR rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls)) OR EXISTS (SELECT 1 FROM pg_auth_members m JOIN pg_roles r ON r.oid=m.member WHERE r.rolname='{OWNER}') THEN RAISE EXCEPTION 'unsafe existing authority function owner'; END IF; END $$ """) op.create_table( "identity_authority_guard", sa.Column("id", sa.SmallInteger(), primary_key=True), sa.Column("generation", sa.BigInteger(), nullable=False, server_default="0"), sa.CheckConstraint("id = 1", name="singleton"), sa.CheckConstraint("generation >= 0", name="generation"), ) op.execute("INSERT INTO public.identity_authority_guard(id) VALUES(1)") for role in (OWNER, "kf_oj_identity_operator"): op.execute(f"REVOKE ALL ON ALL TABLES IN SCHEMA public FROM {role}") op.execute(f""" REVOKE SELECT(id,username,password_hash,enabled,created_at, is_administrator,authorization_version), UPDATE(id,username,password_hash,enabled,created_at, is_administrator,authorization_version) ON public.accounts FROM {role} """) op.execute(f""" REVOKE SELECT(token_hash,account_id,csrf_hash,expires_at,revoked_at,created_at), UPDATE(token_hash,account_id,csrf_hash,expires_at,revoked_at,created_at) ON public.sessions FROM {role} """) op.execute(f"GRANT USAGE ON SCHEMA public TO {OWNER}") op.execute(f""" GRANT SELECT(id,username,enabled,is_administrator,authorization_version), UPDATE(enabled,is_administrator) ON public.accounts TO {OWNER} """) op.execute(f"GRANT SELECT(account_id,revoked_at),UPDATE(revoked_at) ON sessions TO {OWNER}") op.execute(f"GRANT SELECT,UPDATE(generation) ON identity_authority_guard TO {OWNER}") # Every account write obtains the guard BEFORE it can lock an account row. # Updating (rather than merely locking) the singleton also makes an old # REPEATABLE READ/SERIALIZABLE snapshot fail safely with a serialization error. op.execute(""" CREATE FUNCTION public.kf_serialize_account_authority() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$ BEGIN UPDATE public.identity_authority_guard SET generation=generation+1 WHERE id=1; IF NOT FOUND THEN RAISE EXCEPTION 'authority guard unavailable' USING ERRCODE='23514'; END IF; RETURN NULL; END $$ """) op.execute(""" CREATE FUNCTION public.kf_protect_last_administrator() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$ BEGIN IF OLD.is_administrator AND OLD.enabled AND (TG_OP='DELETE' OR NOT (NEW.is_administrator AND NEW.enabled)) AND (SELECT count(*) FROM public.accounts WHERE is_administrator AND enabled) <= 1 THEN RAISE EXCEPTION 'last enabled administrator is protected' USING ERRCODE='23514'; END IF; IF TG_OP='DELETE' THEN RETURN OLD; END IF; RETURN NEW; END $$ """) op.execute(""" CREATE OR REPLACE FUNCTION public.kf_account_authority_version() RETURNS trigger LANGUAGE plpgsql SET search_path = pg_catalog, public AS $$ BEGIN IF NEW.authorization_version IS DISTINCT FROM OLD.authorization_version THEN RAISE EXCEPTION 'authorization version is server derived' USING ERRCODE='23514'; END IF; IF OLD.is_administrator AND NEW.enabled IS DISTINCT FROM OLD.enabled AND current_user != 'kf_oj_identity_authority_owner' AND NOT pg_has_role(current_user,'kf_oj_identity_operator','USAGE') THEN RAISE EXCEPTION 'administrator enablement requires the local operator' USING ERRCODE='42501'; END IF; IF NEW.is_administrator IS DISTINCT FROM OLD.is_administrator OR NEW.enabled IS DISTINCT FROM OLD.enabled THEN NEW.authorization_version := OLD.authorization_version + 1; END IF; RETURN NEW; END $$ """) op.execute(""" CREATE FUNCTION public.kf_manage_account_authority(p_username text,p_operation text) RETURNS TABLE(account_id uuid,username text,enabled boolean,is_administrator boolean, authorization_version bigint,changed boolean) LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$ DECLARE target record; desired_admin boolean; desired_enabled boolean; active bigint; BEGIN IF p_username IS NULL OR p_username !~ '^[a-z0-9_]{3,24}$' OR p_operation IS NULL OR p_operation NOT IN ('bootstrap','grant','revoke','enable','disable') THEN RAISE EXCEPTION 'invalid authority operation' USING ERRCODE='22023'; END IF; UPDATE public.identity_authority_guard g SET generation=g.generation+1 WHERE g.id=1; IF NOT FOUND THEN RAISE EXCEPTION 'authority guard unavailable' USING ERRCODE='23514'; END IF; SELECT a.id,a.username,a.enabled,a.is_administrator,a.authorization_version INTO target FROM public.accounts a WHERE a.username=p_username FOR UPDATE; IF NOT FOUND THEN RAISE EXCEPTION 'account not found' USING ERRCODE='P0002'; END IF; desired_admin:=target.is_administrator; desired_enabled:=target.enabled; SELECT count(*) INTO active FROM public.accounts a WHERE a.is_administrator AND a.enabled; IF p_operation='bootstrap' THEN IF active>0 AND NOT (target.is_administrator AND target.enabled) THEN RAISE EXCEPTION 'bootstrap requires no enabled administrator' USING ERRCODE='23514'; END IF; desired_admin:=true; desired_enabled:=true; ELSIF p_operation='grant' THEN IF active=0 THEN RAISE EXCEPTION 'first administrator requires explicit bootstrap' USING ERRCODE='23514'; END IF; IF NOT target.enabled THEN RAISE EXCEPTION 'enable the account before granting authority' USING ERRCODE='23514'; END IF; desired_admin:=true; ELSIF p_operation='revoke' THEN desired_admin:=false; ELSIF p_operation='enable' THEN desired_enabled:=true; ELSE desired_enabled:=false; END IF; changed:=desired_admin IS DISTINCT FROM target.is_administrator OR desired_enabled IS DISTINCT FROM target.enabled; IF changed THEN UPDATE public.accounts a SET enabled=desired_enabled,is_administrator=desired_admin WHERE a.id=target.id; END IF; RETURN QUERY SELECT a.id,a.username::text,a.enabled,a.is_administrator, a.authorization_version,changed FROM public.accounts a WHERE a.id=target.id; END $$ """) # A narrow NOLOGIN owner holds only these column privileges. The operator # receives EXECUTE, never table writes or access to password/session digests. op.execute(f"GRANT CREATE ON SCHEMA public TO {OWNER}") for signature in ( "kf_serialize_account_authority()", "kf_protect_last_administrator()", "kf_manage_account_authority(text,text)", ): op.execute(f"ALTER FUNCTION public.{signature} OWNER TO {OWNER}") op.execute(f"REVOKE ALL ON FUNCTION public.{signature} FROM PUBLIC") op.execute(f"REVOKE CREATE ON SCHEMA public FROM {OWNER}") op.execute(""" GRANT EXECUTE ON FUNCTION public.kf_manage_account_authority(text,text) TO kf_oj_identity_operator """) op.execute(""" CREATE TRIGGER kf_serialize_account_authority BEFORE INSERT OR UPDATE OR DELETE ON public.accounts FOR EACH STATEMENT EXECUTE FUNCTION public.kf_serialize_account_authority() """) op.execute(""" CREATE TRIGGER kf_protect_last_administrator BEFORE UPDATE OR DELETE ON public.accounts FOR EACH ROW EXECUTE FUNCTION public.kf_protect_last_administrator() """) def downgrade() -> None: op.execute(""" DO $$ BEGIN IF EXISTS(SELECT 1 FROM public.accounts WHERE is_administrator) THEN RAISE EXCEPTION 'cannot remove active authority safeguards' USING ERRCODE='23514'; END IF; END $$ """) for name in ("kf_protect_last_administrator", "kf_serialize_account_authority"): op.execute(f"DROP TRIGGER {name} ON public.accounts") op.execute(f"DROP FUNCTION public.{name}()") op.execute("DROP FUNCTION public.kf_manage_account_authority(text,text)") op.drop_table("identity_authority_guard") op.execute(f"REVOKE ALL ON accounts,sessions FROM {OWNER}") op.execute(f""" REVOKE SELECT(id,username,enabled,is_administrator,authorization_version), UPDATE(enabled,is_administrator) ON accounts FROM {OWNER} """) op.execute(f"REVOKE SELECT(account_id,revoked_at),UPDATE(revoked_at) ON sessions FROM {OWNER}") op.execute("GRANT SELECT ON accounts,sessions TO kf_oj_identity_operator") op.execute("GRANT UPDATE(is_administrator,enabled) ON accounts TO kf_oj_identity_operator") op.execute("GRANT UPDATE(revoked_at) ON sessions TO kf_oj_identity_operator") # Preserve the v0005 trigger policy without referring to the function owner. op.execute(""" CREATE OR REPLACE FUNCTION public.kf_account_authority_version() RETURNS trigger LANGUAGE plpgsql SET search_path = pg_catalog, public AS $$ BEGIN IF NEW.authorization_version IS DISTINCT FROM OLD.authorization_version THEN RAISE EXCEPTION 'authorization version is server derived' USING ERRCODE='23514'; END IF; IF OLD.is_administrator AND NEW.enabled IS DISTINCT FROM OLD.enabled AND NOT pg_has_role(current_user,'kf_oj_identity_operator','USAGE') THEN RAISE EXCEPTION 'administrator enablement requires the local operator' USING ERRCODE='42501'; END IF; IF NEW.is_administrator IS DISTINCT FROM OLD.is_administrator OR NEW.enabled IS DISTINCT FROM OLD.enabled THEN NEW.authorization_version := OLD.authorization_version + 1; END IF; RETURN NEW; END $$ """)