| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- """Persisted tenant membership and one-shot control claims.
- This deliberately supersedes the revoked 501--503 payload-authorized paths.
- The caller supplies a principal and route only as facts to be checked against
- the persistent mapping; a tenant identifier is never accepted by these
- functions. A claim is single-use, DB-clock bounded, and binds the exact
- JSON request that will later be authorized.
- """
- from alembic import op
- revision = "20260817_505"
- down_revision = "20260817_504"
- branch_labels = None
- depends_on = None
- def upgrade() -> None:
- op.execute(
- r"""
- DO $$ BEGIN
- IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_tenant_foundation_owner')
- OR NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_tenant_control') THEN
- RAISE EXCEPTION 'tenant control roles must be provisioned before migration';
- END IF;
- IF NOT pg_has_role(current_user, 'dataops_tenant_foundation_owner', 'MEMBER')
- OR NOT pg_has_role(current_user, 'dataops_tenant_control', 'MEMBER') THEN
- RAISE EXCEPTION 'migration identity lacks tenant control memberships';
- END IF;
- END $$;
- CREATE TABLE public.tenant_control_memberships (
- tenant_id text NOT NULL REFERENCES public.tenants(tenant_id) ON DELETE RESTRICT,
- principal_id uuid NOT NULL,
- host text NOT NULL CHECK (host=lower(host) AND host ~ '^[a-z0-9][a-z0-9.-]{0,252}$'),
- created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
- PRIMARY KEY (tenant_id,principal_id,host),
- UNIQUE (principal_id,host)
- );
- CREATE TABLE public.tenant_control_claims (
- claim_uid uuid PRIMARY KEY,
- nonce uuid NOT NULL UNIQUE,
- tenant_id text NOT NULL REFERENCES public.tenants(tenant_id) ON DELETE RESTRICT,
- principal_id uuid NOT NULL,
- host text NOT NULL CHECK (host=lower(host) AND host ~ '^[a-z0-9][a-z0-9.-]{0,252}$'),
- action text NOT NULL CHECK (action IN ('quota_reserve','lifecycle_transition')),
- request jsonb NOT NULL CHECK (jsonb_typeof(request)='object'),
- request_digest char(64) NOT NULL,
- status text NOT NULL DEFAULT 'issued' CHECK (status IN ('issued','consumed')),
- created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
- expires_at timestamptz NOT NULL,
- consumed_at timestamptz,
- CHECK (expires_at > created_at),
- CHECK ((status='issued' AND consumed_at IS NULL) OR (status='consumed' AND consumed_at IS NOT NULL))
- );
- CREATE INDEX tenant_control_claims_active_idx
- ON public.tenant_control_claims(claim_uid,status,expires_at);
- ALTER TABLE public.tenant_control_memberships OWNER TO dataops_tenant_foundation_owner;
- ALTER TABLE public.tenant_control_claims OWNER TO dataops_tenant_foundation_owner;
- ALTER TABLE public.tenants ENABLE ROW LEVEL SECURITY;
- REVOKE ALL ON TABLE public.tenants,public.tenant_control_memberships,public.tenant_control_claims
- FROM PUBLIC,dataops_app_runtime,dataops_tenant_control;
- CREATE FUNCTION public.tenant_control_issue_claim(p_payload jsonb)
- RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $tenant_claim_issue$
- DECLARE v_principal uuid; v_host text; v_action text; v_tenant text; v_claim uuid; v_nonce uuid;
- DECLARE v_request jsonb; v_digest text; v_expires timestamptz;
- BEGIN
- IF NOT pg_has_role(session_user,'dataops_tenant_control','MEMBER') THEN
- RAISE EXCEPTION 'tenant_control_identity_required';
- END IF;
- IF jsonb_typeof(p_payload)<>'object'
- OR NOT (p_payload ?& ARRAY['claim_uid','nonce','principal_id','host','action','request'])
- OR p_payload-ARRAY['claim_uid','nonce','principal_id','host','action','request']<>'{}'::jsonb THEN
- RAISE EXCEPTION 'tenant_payload_closed';
- END IF;
- IF coalesce(p_payload->>'claim_uid','') !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
- OR coalesce(p_payload->>'nonce','') !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
- OR coalesce(p_payload->>'principal_id','') !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
- OR p_payload->>'host' !~ '^[a-z0-9][a-z0-9.-]{0,252}$'
- OR p_payload->>'host' <> lower(p_payload->>'host')
- OR p_payload->>'action' NOT IN ('quota_reserve','lifecycle_transition')
- OR jsonb_typeof(p_payload->'request')<>'object' THEN
- RAISE EXCEPTION 'tenant_payload_invalid';
- END IF;
- v_claim := (p_payload->>'claim_uid')::uuid;
- v_nonce := (p_payload->>'nonce')::uuid;
- v_principal := (p_payload->>'principal_id')::uuid;
- v_host := p_payload->>'host';
- v_action := p_payload->>'action';
- v_request := p_payload->'request';
- SELECT m.tenant_id INTO v_tenant
- FROM public.tenant_control_memberships m
- JOIN public.tenants t ON t.tenant_id=m.tenant_id
- WHERE m.principal_id=v_principal AND m.host=v_host AND t.state='active';
- IF NOT FOUND THEN RAISE EXCEPTION 'tenant_membership_denied'; END IF;
- v_digest := encode(sha256(convert_to(v_request::text,'utf8')),'hex');
- v_expires := clock_timestamp()+interval '120 seconds';
- INSERT INTO public.tenant_control_claims(
- claim_uid,nonce,tenant_id,principal_id,host,action,request,request_digest,expires_at
- ) VALUES(v_claim,v_nonce,v_tenant,v_principal,v_host,v_action,v_request,v_digest,v_expires);
- PERFORM set_config('dataops.tenant_id',v_tenant,true);
- INSERT INTO public.tenant_audit_events(tenant_id,event_type,payload_digest,lease_fence)
- VALUES(v_tenant,'tenant.claim.issued',v_digest,0);
- RETURN jsonb_build_object('claim_uid',v_claim,'tenant_id',v_tenant,'action',v_action,
- 'request_digest',v_digest,'expires_at',v_expires);
- END; $tenant_claim_issue$;
- ALTER FUNCTION public.tenant_control_issue_claim(jsonb) OWNER TO dataops_tenant_foundation_owner;
- CREATE FUNCTION public.tenant_control_consume_claim(
- p_claim_uid uuid,p_nonce uuid,p_action text,p_request jsonb
- ) RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $tenant_claim_consume$
- DECLARE v_claim public.tenant_control_claims%ROWTYPE;
- BEGIN
- IF NOT pg_has_role(session_user,'dataops_tenant_control','MEMBER') THEN
- RAISE EXCEPTION 'tenant_control_identity_required';
- END IF;
- IF p_action NOT IN ('quota_reserve','lifecycle_transition') OR jsonb_typeof(p_request)<>'object' THEN
- RAISE EXCEPTION 'tenant_payload_closed';
- END IF;
- SELECT * INTO v_claim FROM public.tenant_control_claims
- WHERE claim_uid=p_claim_uid FOR UPDATE;
- IF NOT FOUND OR v_claim.nonce<>p_nonce OR v_claim.action<>p_action
- OR v_claim.request<>p_request THEN
- RAISE EXCEPTION 'tenant_claim_denied';
- END IF;
- IF v_claim.status<>'issued' OR v_claim.expires_at<=clock_timestamp() THEN
- RAISE EXCEPTION 'tenant_claim_replayed_or_expired';
- END IF;
- UPDATE public.tenant_control_claims SET status='consumed',consumed_at=clock_timestamp()
- WHERE claim_uid=v_claim.claim_uid AND status='issued';
- IF NOT FOUND THEN RAISE EXCEPTION 'tenant_claim_replayed_or_expired'; END IF;
- PERFORM set_config('dataops.tenant_id',v_claim.tenant_id,true);
- INSERT INTO public.tenant_audit_events(tenant_id,event_type,payload_digest,lease_fence)
- VALUES(v_claim.tenant_id,'tenant.claim.consumed',v_claim.request_digest,0);
- RETURN jsonb_build_object('claim_uid',v_claim.claim_uid,'tenant_id',v_claim.tenant_id,
- 'principal_id',v_claim.principal_id,'action',v_claim.action,
- 'request_digest',v_claim.request_digest);
- END; $tenant_claim_consume$;
- ALTER FUNCTION public.tenant_control_consume_claim(uuid,uuid,text,jsonb) OWNER TO dataops_tenant_foundation_owner;
- REVOKE ALL ON FUNCTION public.tenant_control_issue_claim(jsonb),
- public.tenant_control_consume_claim(uuid,uuid,text,jsonb) FROM PUBLIC,dataops_app_runtime;
- GRANT EXECUTE ON FUNCTION public.tenant_control_issue_claim(jsonb),
- public.tenant_control_consume_claim(uuid,uuid,text,jsonb) TO dataops_tenant_control;
- """
- )
- def downgrade() -> None:
- op.execute(
- """
- DO $$ BEGIN
- IF EXISTS (SELECT 1 FROM public.tenant_control_claims LIMIT 1)
- OR EXISTS (SELECT 1 FROM public.tenant_control_memberships LIMIT 1) THEN
- RAISE EXCEPTION 'tenant control downgrade refused while control data exists';
- END IF;
- END $$;
- REVOKE EXECUTE ON FUNCTION public.tenant_control_issue_claim(jsonb),
- public.tenant_control_consume_claim(uuid,uuid,text,jsonb) FROM dataops_tenant_control;
- DROP FUNCTION public.tenant_control_consume_claim(uuid,uuid,text,jsonb);
- DROP FUNCTION public.tenant_control_issue_claim(jsonb);
- DROP TABLE public.tenant_control_claims;
- DROP TABLE public.tenant_control_memberships;
- """
- )
|