20260818_558_plugin_review_generation_gate.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. """Bind approvals to the current independent plugin review generation."""
  2. from alembic import op
  3. revision = "20260818_558"
  4. down_revision = "20260818_557"
  5. branch_labels = None
  6. depends_on = None
  7. _FACT_TABLES = (
  8. "plugin_registry_versions", "plugin_approvals", "plugin_runtime_leases",
  9. "plugin_runs", "plugin_dead_letters", "plugin_audit_outbox",
  10. "plugin_request_claims", "plugin_runtime_breakers", "plugin_recovery_claims",
  11. )
  12. def upgrade() -> None:
  13. op.get_bind().exec_driver_sql(r'''
  14. ALTER TABLE public.plugin_registry_versions
  15. ADD COLUMN submitter_ref text CHECK(submitter_ref ~ '^[A-Za-z0-9_.:-]{1,120}$'),
  16. ADD COLUMN review_generation bigint NOT NULL DEFAULT 0 CHECK(review_generation >= 0);
  17. UPDATE public.plugin_registry_versions r
  18. SET submitter_ref=(SELECT a.actor_ref FROM public.plugin_audit_outbox a
  19. WHERE a.plugin_uid=r.plugin_uid AND a.version=r.version
  20. AND a.event_type='registered'
  21. ORDER BY a.created_at ASC LIMIT 1)
  22. WHERE submitter_ref IS NULL;
  23. ALTER TABLE public.plugin_approvals
  24. ADD COLUMN review_generation bigint NOT NULL DEFAULT 0 CHECK(review_generation >= 0),
  25. ADD COLUMN reviewed_at timestamptz;
  26. DO $drop_old_approval_unique$
  27. DECLARE constraint_name text;
  28. BEGIN
  29. SELECT c.conname INTO constraint_name
  30. FROM pg_constraint c
  31. WHERE c.conrelid='public.plugin_approvals'::regclass AND c.contype='u'
  32. AND (SELECT array_agg(a.attname::text ORDER BY k.ordinality)
  33. FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum,ordinality)
  34. JOIN pg_attribute a ON a.attrelid=c.conrelid AND a.attnum=k.attnum)
  35. = ARRAY['plugin_uid','version','action_name','actor_ref','tenant_ref','domain_ref','manifest_digest']::text[];
  36. IF constraint_name IS NOT NULL THEN
  37. EXECUTE format('ALTER TABLE public.plugin_approvals DROP CONSTRAINT %%I', constraint_name);
  38. END IF;
  39. END $drop_old_approval_unique$;
  40. ALTER TABLE public.plugin_approvals ADD CONSTRAINT plugin_approvals_review_generation_unique UNIQUE(plugin_uid,version,action_name,actor_ref,tenant_ref,domain_ref,manifest_digest,review_generation);
  41. CREATE FUNCTION public.plugin_platform_control_v4(p jsonb) RETURNS jsonb
  42. LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $control$
  43. DECLARE r record; a record; result jsonb; expected_action text;
  44. BEGIN
  45. IF NOT pg_has_role(session_user,'dataops_plugin_platform_control','MEMBER') OR jsonb_typeof(p)<>'object' THEN RAISE EXCEPTION 'plugin_control_denied'; END IF;
  46. IF p->>'action'='register' THEN
  47. result:=public.plugin_platform_control_v3(p);
  48. UPDATE public.plugin_registry_versions
  49. SET submitter_ref=p->>'actor_ref',review_actor=NULL,reviewed_at=NULL,review_generation=0
  50. WHERE plugin_uid=p->>'plugin_uid' AND version=p->>'version';
  51. RETURN result;
  52. ELSIF p->>'action'='review' THEN
  53. SELECT * INTO r FROM public.plugin_registry_versions WHERE plugin_uid=p->>'plugin_uid' AND version=p->>'version' FOR UPDATE;
  54. IF NOT FOUND OR r.state<>'draft' OR r.submitter_ref IS NULL OR r.submitter_ref=p->>'actor_ref' THEN RAISE EXCEPTION 'plugin_review_independence_denied'; END IF;
  55. result:=public.plugin_platform_control_v3(p);
  56. UPDATE public.plugin_registry_versions SET review_generation=review_generation+1
  57. WHERE plugin_uid=p->>'plugin_uid' AND version=p->>'version';
  58. RETURN result;
  59. ELSIF p->>'action'='issue_approval' THEN
  60. IF p-ARRAY['action','plugin_uid','version','actor_ref','approval_action','tenant_ref','domain_ref','expires_in_seconds']<>'{}'::jsonb THEN RAISE EXCEPTION 'plugin_approval_closed'; END IF;
  61. SELECT * INTO r FROM public.plugin_registry_versions WHERE plugin_uid=p->>'plugin_uid' AND version=p->>'version' FOR UPDATE;
  62. IF NOT FOUND OR r.state<>'reviewed' OR r.submitter_ref IS NULL OR r.review_actor IS NULL OR r.submitter_ref=r.review_actor OR r.review_actor=p->>'actor_ref' THEN RAISE EXCEPTION 'plugin_approval_review_denied'; END IF;
  63. result:=public.plugin_platform_control_v3(p);
  64. UPDATE public.plugin_approvals SET review_generation=r.review_generation,reviewed_at=r.reviewed_at
  65. WHERE approval_uid=(result->>'approval_uid')::uuid;
  66. RETURN result;
  67. ELSIF p->>'action'='transition' THEN
  68. IF p-ARRAY['action','plugin_uid','version','actor_ref','tenant_ref','domain_ref','approval_uid','target_state','expected_fence','incident_uid']<>'{}'::jsonb THEN RAISE EXCEPTION 'plugin_transition_closed'; END IF;
  69. expected_action:=CASE p->>'target_state' WHEN 'approved' THEN 'approve' WHEN 'active' THEN 'activate' WHEN 'rolled_back' THEN 'rollback' ELSE p->>'target_state' END;
  70. SELECT * INTO r FROM public.plugin_registry_versions WHERE plugin_uid=p->>'plugin_uid' AND version=p->>'version' FOR UPDATE;
  71. SELECT * INTO a FROM public.plugin_approvals WHERE approval_uid=(p->>'approval_uid')::uuid FOR UPDATE;
  72. IF NOT FOUND OR r.submitter_ref IS NULL OR r.review_actor IS NULL OR r.submitter_ref=r.review_actor
  73. OR a.plugin_uid<>r.plugin_uid OR a.version<>r.version OR a.actor_ref<>p->>'actor_ref'
  74. OR a.reviewer_ref<>r.review_actor OR a.review_generation<>r.review_generation
  75. OR a.reviewed_at IS NULL OR r.reviewed_at IS NULL OR a.reviewed_at<>r.reviewed_at
  76. OR a.created_at<r.reviewed_at OR a.manifest_digest<>r.manifest_digest
  77. OR a.action_name<>expected_action OR a.tenant_ref<>p->>'tenant_ref' OR a.domain_ref<>p->>'domain_ref'
  78. OR a.consumed_at IS NOT NULL OR a.expires_at<=clock_timestamp() THEN RAISE EXCEPTION 'plugin_approval_review_binding_denied'; END IF;
  79. RETURN public.plugin_platform_control_v3(p);
  80. END IF;
  81. RETURN public.plugin_platform_control_v3(p);
  82. END $control$;
  83. ALTER FUNCTION public.plugin_platform_control_v4(jsonb) OWNER TO dataops_plugin_platform_owner;
  84. REVOKE ALL ON FUNCTION public.plugin_platform_control_v3(jsonb) FROM PUBLIC,dataops_app,dataops_app_runtime,dataops_plugin_platform_control;
  85. GRANT EXECUTE ON FUNCTION public.plugin_platform_control_v4(jsonb) TO dataops_plugin_platform_control;
  86. ''')
  87. def downgrade() -> None:
  88. bind = op.get_bind()
  89. checks = " OR ".join(f"EXISTS(SELECT 1 FROM public.{table} LIMIT 1)" for table in _FACT_TABLES)
  90. if bind.exec_driver_sql(f"SELECT {checks}").scalar():
  91. raise RuntimeError("downgrade refused: review-bound WP13 facts are nonempty")
  92. bind.exec_driver_sql('''
  93. REVOKE ALL ON FUNCTION public.plugin_platform_control_v4(jsonb) FROM PUBLIC,dataops_app,dataops_app_runtime,dataops_plugin_platform_control;
  94. DROP FUNCTION public.plugin_platform_control_v4(jsonb);
  95. ALTER TABLE public.plugin_approvals DROP CONSTRAINT plugin_approvals_review_generation_unique;
  96. ALTER TABLE public.plugin_approvals DROP COLUMN reviewed_at,DROP COLUMN review_generation;
  97. ALTER TABLE public.plugin_approvals ADD CONSTRAINT plugin_approvals_plugin_uid_version_action_name_actor_ref_tenant_ref_domain_ref_manifest_digest_key UNIQUE(plugin_uid,version,action_name,actor_ref,tenant_ref,domain_ref,manifest_digest);
  98. ALTER TABLE public.plugin_registry_versions DROP COLUMN review_generation,DROP COLUMN submitter_ref;
  99. GRANT EXECUTE ON FUNCTION public.plugin_platform_control_v3(jsonb) TO dataops_plugin_platform_control;
  100. ''')