20260817_506_tenant_controlled_quota.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """Bind decimal quota reserve, settle, and release to consumed control claims."""
  2. from alembic import op
  3. revision = "20260817_506"
  4. down_revision = "20260817_505"
  5. branch_labels = None
  6. depends_on = None
  7. def upgrade() -> None:
  8. op.execute(
  9. r"""
  10. ALTER TABLE public.tenant_control_claims DROP CONSTRAINT tenant_control_claims_action_check;
  11. ALTER TABLE public.tenant_control_claims ADD CONSTRAINT tenant_control_claims_action_check
  12. CHECK (action IN ('quota_reserve','quota_settle','quota_release','lifecycle_transition'));
  13. CREATE OR REPLACE FUNCTION public.tenant_control_issue_claim(p_payload jsonb)
  14. RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $tenant_claim_issue$
  15. DECLARE v_principal uuid; v_host text; v_action text; v_tenant text; v_claim uuid; v_nonce uuid;
  16. DECLARE v_request jsonb; v_digest text; v_expires timestamptz;
  17. BEGIN
  18. IF NOT pg_has_role(session_user,'dataops_tenant_control','MEMBER') THEN RAISE EXCEPTION 'tenant_control_identity_required'; END IF;
  19. IF jsonb_typeof(p_payload)<>'object' OR NOT (p_payload ?& ARRAY['claim_uid','nonce','principal_id','host','action','request'])
  20. OR p_payload-ARRAY['claim_uid','nonce','principal_id','host','action','request']<>'{}'::jsonb THEN RAISE EXCEPTION 'tenant_payload_closed'; END IF;
  21. 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}$'
  22. 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}$'
  23. 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}$'
  24. OR p_payload->>'host' !~ '^[a-z0-9][a-z0-9.-]{0,252}$' OR p_payload->>'host' <> lower(p_payload->>'host')
  25. OR p_payload->>'action' NOT IN ('quota_reserve','quota_settle','quota_release','lifecycle_transition')
  26. OR jsonb_typeof(p_payload->'request')<>'object' THEN RAISE EXCEPTION 'tenant_payload_invalid'; END IF;
  27. v_claim := (p_payload->>'claim_uid')::uuid; v_nonce := (p_payload->>'nonce')::uuid;
  28. v_principal := (p_payload->>'principal_id')::uuid; v_host := p_payload->>'host';
  29. v_action := p_payload->>'action'; v_request := p_payload->'request';
  30. SELECT m.tenant_id INTO v_tenant FROM public.tenant_control_memberships m JOIN public.tenants t ON t.tenant_id=m.tenant_id
  31. WHERE m.principal_id=v_principal AND m.host=v_host AND t.state='active';
  32. IF NOT FOUND THEN RAISE EXCEPTION 'tenant_membership_denied'; END IF;
  33. v_digest := encode(sha256(convert_to(v_request::text,'utf8')),'hex'); v_expires := clock_timestamp()+interval '120 seconds';
  34. INSERT INTO public.tenant_control_claims(claim_uid,nonce,tenant_id,principal_id,host,action,request,request_digest,expires_at)
  35. VALUES(v_claim,v_nonce,v_tenant,v_principal,v_host,v_action,v_request,v_digest,v_expires);
  36. PERFORM set_config('dataops.tenant_id',v_tenant,true);
  37. INSERT INTO public.tenant_audit_events(tenant_id,event_type,payload_digest,lease_fence) VALUES(v_tenant,'tenant.claim.issued',v_digest,0);
  38. RETURN jsonb_build_object('claim_uid',v_claim,'tenant_id',v_tenant,'action',v_action,'request_digest',v_digest,'expires_at',v_expires);
  39. END; $tenant_claim_issue$;
  40. CREATE OR REPLACE FUNCTION public.tenant_control_consume_claim(p_claim_uid uuid,p_nonce uuid,p_action text,p_request jsonb)
  41. RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $tenant_claim_consume$
  42. DECLARE v_claim public.tenant_control_claims%ROWTYPE;
  43. BEGIN
  44. IF NOT pg_has_role(session_user,'dataops_tenant_control','MEMBER') THEN RAISE EXCEPTION 'tenant_control_identity_required'; END IF;
  45. IF p_action NOT IN ('quota_reserve','quota_settle','quota_release','lifecycle_transition') OR jsonb_typeof(p_request)<>'object' THEN RAISE EXCEPTION 'tenant_payload_closed'; END IF;
  46. SELECT * INTO v_claim FROM public.tenant_control_claims WHERE claim_uid=p_claim_uid FOR UPDATE;
  47. 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;
  48. IF v_claim.status<>'issued' OR v_claim.expires_at<=clock_timestamp() THEN RAISE EXCEPTION 'tenant_claim_replayed_or_expired'; END IF;
  49. UPDATE public.tenant_control_claims SET status='consumed',consumed_at=clock_timestamp() WHERE claim_uid=v_claim.claim_uid AND status='issued';
  50. IF NOT FOUND THEN RAISE EXCEPTION 'tenant_claim_replayed_or_expired'; END IF;
  51. PERFORM set_config('dataops.tenant_id',v_claim.tenant_id,true);
  52. 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);
  53. 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);
  54. END; $tenant_claim_consume$;
  55. CREATE FUNCTION public.tenant_control_quota_mutate(p_claim_uid uuid,p_nonce uuid,p_action text,p_request jsonb)
  56. RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $tenant_quota_control$
  57. DECLARE v_claim jsonb; v_tenant text; v_quota record; v_reservation record; v_amount numeric(30,6); v_fence bigint; v_digest text;
  58. BEGIN
  59. IF p_action NOT IN ('quota_reserve','quota_settle','quota_release') OR jsonb_typeof(p_request)<>'object' THEN RAISE EXCEPTION 'tenant_payload_closed'; END IF;
  60. IF (p_action='quota_reserve' AND (NOT (p_request ?& ARRAY['quota_name','amount','idempotency_key']) OR p_request-ARRAY['quota_name','amount','idempotency_key']<>'{}'::jsonb OR jsonb_typeof(p_request->'amount')<>'string'))
  61. OR (p_action IN ('quota_settle','quota_release') AND (NOT (p_request ?& ARRAY['quota_name','idempotency_key','lease_fence']) OR p_request-ARRAY['quota_name','idempotency_key','lease_fence']<>'{}'::jsonb OR jsonb_typeof(p_request->'lease_fence')<>'string')) THEN RAISE EXCEPTION 'tenant_payload_closed'; END IF;
  62. IF p_request->>'quota_name' NOT IN ('records','storage_objects','background_tasks','events','models','plugins','connectors','api_concurrency')
  63. OR p_request->>'idempotency_key' !~ '^[A-Za-z0-9._:-]{1,120}$'
  64. OR (p_action='quota_reserve' AND (p_request->>'amount' !~ '^[0-9]+([.][0-9]{1,6})?$' OR (p_request->>'amount')::numeric<=0 OR (p_request->>'amount')::numeric>1000000000000000000000000::numeric))
  65. OR (p_action IN ('quota_settle','quota_release') AND p_request->>'lease_fence' !~ '^[1-9][0-9]{0,18}$') THEN RAISE EXCEPTION 'tenant_payload_invalid'; END IF;
  66. v_claim := public.tenant_control_consume_claim(p_claim_uid,p_nonce,p_action,p_request);
  67. v_tenant := v_claim->>'tenant_id'; v_digest := v_claim->>'request_digest';
  68. PERFORM set_config('dataops.tenant_id',v_tenant,true);
  69. PERFORM pg_advisory_xact_lock(hashtext(v_tenant || '-quota-' || (p_request->>'quota_name')));
  70. IF p_action='quota_reserve' THEN
  71. v_amount := (p_request->>'amount')::numeric;
  72. SELECT * INTO v_reservation FROM public.tenant_quota_reservations WHERE tenant_id=v_tenant AND quota_name=p_request->>'quota_name' AND idempotency_key=p_request->>'idempotency_key' FOR UPDATE;
  73. IF FOUND THEN
  74. IF v_reservation.request_digest<>v_digest THEN RAISE EXCEPTION 'tenant_replay_conflict'; END IF;
  75. RETURN jsonb_build_object('tenant_id',v_tenant,'lease_fence',v_reservation.lease_fence,'status',v_reservation.status);
  76. END IF;
  77. SELECT * INTO v_quota FROM public.tenant_quotas WHERE tenant_id=v_tenant AND quota_name=p_request->>'quota_name' FOR UPDATE;
  78. IF NOT FOUND OR NOT EXISTS(SELECT 1 FROM public.tenants WHERE tenant_id=v_tenant AND state='active') THEN RAISE EXCEPTION 'tenant_not_active'; END IF;
  79. IF v_quota.limit_units-v_quota.reserved_units<v_amount THEN RAISE EXCEPTION 'tenant_quota_exhausted'; END IF;
  80. UPDATE public.tenant_quotas SET reserved_units=reserved_units+v_amount,lease_fence=lease_fence+1,updated_at=clock_timestamp() WHERE tenant_id=v_tenant AND quota_name=p_request->>'quota_name' AND lease_fence=v_quota.lease_fence RETURNING lease_fence INTO v_fence;
  81. IF NOT FOUND THEN RAISE EXCEPTION 'tenant_fence_conflict'; END IF;
  82. INSERT INTO public.tenant_quota_reservations(tenant_id,quota_name,idempotency_key,request_digest,amount,lease_fence,status) VALUES(v_tenant,p_request->>'quota_name',p_request->>'idempotency_key',v_digest,v_amount,v_fence,'reserved');
  83. INSERT INTO public.tenant_audit_events(tenant_id,event_type,payload_digest,lease_fence) VALUES(v_tenant,'quota.reserve',v_digest,v_fence);
  84. RETURN jsonb_build_object('tenant_id',v_tenant,'lease_fence',v_fence,'status','reserved');
  85. END IF;
  86. SELECT * INTO v_reservation FROM public.tenant_quota_reservations WHERE tenant_id=v_tenant AND quota_name=p_request->>'quota_name' AND idempotency_key=p_request->>'idempotency_key' FOR UPDATE;
  87. IF NOT FOUND OR v_reservation.lease_fence<>(p_request->>'lease_fence')::bigint THEN RAISE EXCEPTION 'tenant_fence_conflict'; END IF;
  88. IF v_reservation.status<> 'reserved' THEN
  89. IF (p_action='quota_settle' AND v_reservation.status='settled') OR (p_action='quota_release' AND v_reservation.status='released') THEN RETURN jsonb_build_object('tenant_id',v_tenant,'lease_fence',v_reservation.lease_fence,'status',v_reservation.status); END IF;
  90. RAISE EXCEPTION 'tenant_quota_terminal';
  91. END IF;
  92. IF p_action='quota_settle' THEN
  93. UPDATE public.tenant_quota_reservations SET status='settled',updated_at=clock_timestamp() WHERE reservation_uid=v_reservation.reservation_uid;
  94. INSERT INTO public.tenant_audit_events(tenant_id,event_type,payload_digest,lease_fence) VALUES(v_tenant,'quota.settle',v_digest,v_reservation.lease_fence);
  95. RETURN jsonb_build_object('tenant_id',v_tenant,'lease_fence',v_reservation.lease_fence,'status','settled');
  96. END IF;
  97. SELECT * INTO v_quota FROM public.tenant_quotas WHERE tenant_id=v_tenant AND quota_name=p_request->>'quota_name' FOR UPDATE;
  98. UPDATE public.tenant_quotas SET reserved_units=reserved_units-v_reservation.amount,lease_fence=lease_fence+1,updated_at=clock_timestamp() WHERE tenant_id=v_tenant AND quota_name=p_request->>'quota_name' AND lease_fence=v_quota.lease_fence RETURNING lease_fence INTO v_fence;
  99. IF NOT FOUND THEN RAISE EXCEPTION 'tenant_fence_conflict'; END IF;
  100. UPDATE public.tenant_quota_reservations SET status='released',updated_at=clock_timestamp() WHERE reservation_uid=v_reservation.reservation_uid;
  101. INSERT INTO public.tenant_audit_events(tenant_id,event_type,payload_digest,lease_fence) VALUES(v_tenant,'quota.release',v_digest,v_fence);
  102. RETURN jsonb_build_object('tenant_id',v_tenant,'lease_fence',v_fence,'status','released');
  103. END; $tenant_quota_control$;
  104. ALTER FUNCTION public.tenant_control_issue_claim(jsonb) OWNER TO dataops_tenant_foundation_owner;
  105. ALTER FUNCTION public.tenant_control_consume_claim(uuid,uuid,text,jsonb) OWNER TO dataops_tenant_foundation_owner;
  106. ALTER FUNCTION public.tenant_control_quota_mutate(uuid,uuid,text,jsonb) OWNER TO dataops_tenant_foundation_owner;
  107. REVOKE ALL ON FUNCTION public.tenant_control_quota_mutate(uuid,uuid,text,jsonb) FROM PUBLIC,dataops_app_runtime;
  108. GRANT EXECUTE ON FUNCTION public.tenant_control_quota_mutate(uuid,uuid,text,jsonb) TO dataops_tenant_control;
  109. """
  110. )
  111. def downgrade() -> None:
  112. op.execute(
  113. """
  114. DO $$ BEGIN
  115. IF EXISTS (SELECT 1 FROM public.tenant_control_claims WHERE action IN ('quota_settle','quota_release')) THEN
  116. RAISE EXCEPTION 'tenant controlled quota downgrade refused while newer claims exist';
  117. END IF;
  118. END $$;
  119. REVOKE EXECUTE ON FUNCTION public.tenant_control_quota_mutate(uuid,uuid,text,jsonb) FROM dataops_tenant_control;
  120. DROP FUNCTION public.tenant_control_quota_mutate(uuid,uuid,text,jsonb);
  121. ALTER TABLE public.tenant_control_claims DROP CONSTRAINT tenant_control_claims_action_check;
  122. ALTER TABLE public.tenant_control_claims ADD CONSTRAINT tenant_control_claims_action_check
  123. CHECK (action IN ('quota_reserve','lifecycle_transition'));
  124. """
  125. )