test_unified_responsibility_postgres.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. from __future__ import annotations
  2. import os
  3. import uuid
  4. from datetime import UTC, datetime, timedelta
  5. import pytest
  6. from sqlalchemy import create_engine, text
  7. from sqlalchemy.orm import Session
  8. pytestmark = pytest.mark.integration
  9. OBJECT_TYPES = (
  10. "data_asset",
  11. "semantic_term",
  12. "data_standard",
  13. "quality_policy",
  14. "data_product",
  15. "agent",
  16. )
  17. def test_unified_responsibility_chain_delegation_policy_and_audit():
  18. database_url = os.environ.get("TEST_DATABASE_URL")
  19. if not database_url:
  20. pytest.skip("TEST_DATABASE_URL is required")
  21. from app.core.governance.responsibilities import (
  22. ResponsibilityService,
  23. SqlAlchemyResponsibilityRepository,
  24. )
  25. from app.core.governance.unified_responsibilities import (
  26. UnifiedResponsibilityService,
  27. )
  28. from app.core.governance.unified_responsibility_repository import (
  29. SqlAlchemyUnifiedResponsibilityRepository,
  30. )
  31. engine = create_engine(database_url)
  32. marker = uuid.uuid4().hex[:10]
  33. users = [str(uuid.uuid4()) for _ in range(3)]
  34. actor, owner_a, owner_b = users
  35. organization_uid = f"wp06-org-{marker}"
  36. domain_a = f"wp06-domain-a-{marker}"
  37. domain_b = f"wp06-domain-b-{marker}"
  38. object_uids = {
  39. object_type: f"wp06-{object_type}-{marker}"
  40. for object_type in OBJECT_TYPES
  41. }
  42. session = Session(engine)
  43. try:
  44. for index, user_uid in enumerate(users):
  45. session.execute(
  46. text(
  47. """
  48. INSERT INTO public.users (
  49. id, username, display_name, password_hash, status
  50. ) VALUES (
  51. CAST(:id AS uuid), :username, :display_name,
  52. 'not-a-login-secret', 'active'
  53. )
  54. """
  55. ),
  56. {
  57. "id": user_uid,
  58. "username": f"wp06_{marker}_{index}",
  59. "display_name": f"WP06 user {index}",
  60. },
  61. )
  62. session.commit()
  63. matrix = ResponsibilityService(SqlAlchemyResponsibilityRepository(session))
  64. unified = UnifiedResponsibilityService(
  65. SqlAlchemyUnifiedResponsibilityRepository(session),
  66. commit=session.commit,
  67. rollback=session.rollback,
  68. )
  69. matrix.replace(
  70. resource_type="organization",
  71. resource_uid=organization_uid,
  72. assignments=[
  73. {
  74. "user_id": owner_a,
  75. "responsibility_role": "organization_owner",
  76. "raci_role": "accountable",
  77. }
  78. ],
  79. expected_revision=0,
  80. actor_uid=actor,
  81. )
  82. matrix.replace(
  83. resource_type="business_domain",
  84. resource_uid=domain_b,
  85. assignments=[
  86. {
  87. "user_id": owner_b,
  88. "responsibility_role": "domain_owner",
  89. "raci_role": "accountable",
  90. }
  91. ],
  92. expected_revision=0,
  93. actor_uid=actor,
  94. )
  95. session.commit()
  96. unified.set_parent(
  97. {
  98. "resource_type": "business_domain",
  99. "resource_uid": domain_a,
  100. "parent_type": "organization",
  101. "parent_uid": organization_uid,
  102. },
  103. expected_revision=0,
  104. actor_uid=actor,
  105. )
  106. unified.set_parent(
  107. {
  108. "resource_type": "business_domain",
  109. "resource_uid": domain_b,
  110. "parent_type": "organization",
  111. "parent_uid": organization_uid,
  112. },
  113. expected_revision=0,
  114. actor_uid=actor,
  115. )
  116. for object_type, object_uid in object_uids.items():
  117. unified.set_parent(
  118. {
  119. "resource_type": object_type,
  120. "resource_uid": object_uid,
  121. "parent_type": "business_domain",
  122. "parent_uid": domain_a,
  123. },
  124. expected_revision=0,
  125. actor_uid=actor,
  126. )
  127. resolved = {
  128. object_type: unified.resolve(object_type, object_uid)
  129. for object_type, object_uid in object_uids.items()
  130. }
  131. assert all(item["status"] == "resolved" for item in resolved.values())
  132. assert {
  133. item["final_owners"][0]["effective_user_id"]
  134. for item in resolved.values()
  135. } == {owner_a}
  136. now = datetime.now(UTC)
  137. delegation = unified.create_delegation(
  138. {
  139. "source_user_uid": owner_a,
  140. "delegate_user_uid": owner_b,
  141. "scope_type": "business_domain",
  142. "scope_uid": domain_a,
  143. "responsibility_role": "organization_owner",
  144. "delegation_type": "temporary",
  145. "starts_at": (now - timedelta(minutes=1)).isoformat(),
  146. "ends_at": (now + timedelta(minutes=1)).isoformat(),
  147. "reason": "WP06 PostgreSQL acceptance",
  148. },
  149. actor_uid=actor,
  150. )
  151. delegated = unified.resolve("data_asset", object_uids["data_asset"])
  152. assert delegated["final_owners"][0]["effective_user_id"] == owner_b
  153. unified.expire_delegations(
  154. at=now + timedelta(minutes=2), actor_uid=actor
  155. )
  156. assert unified.resolve(
  157. "data_asset", object_uids["data_asset"], at=now + timedelta(minutes=2)
  158. )["final_owners"][0]["effective_user_id"] == owner_a
  159. central = unified.create_policy(
  160. {
  161. "code": f"WP06_CENTRAL_{marker.upper()}",
  162. "name": "WP06 central policy",
  163. "policy_type": "central_policy",
  164. "scope_type": "organization",
  165. "scope_uid": organization_uid,
  166. "definition": {
  167. "required_responsibility_roles": ["organization_owner"],
  168. "require_unique_accountable": True,
  169. "max_delegation_days": 30,
  170. },
  171. },
  172. actor_uid=actor,
  173. )
  174. unified.publish_policy(central["uid"], expected_version=1, actor_uid=actor)
  175. joint = unified.create_policy(
  176. {
  177. "code": f"WP06_JOINT_{marker.upper()}",
  178. "name": "WP06 joint review",
  179. "policy_type": "joint_review",
  180. "scope_type": "data_product",
  181. "scope_uid": object_uids["data_product"],
  182. "definition": {
  183. "business_domain_uids": [domain_a, domain_b],
  184. "min_approvals": 2,
  185. "require_all_domains": True,
  186. },
  187. },
  188. actor_uid=actor,
  189. )
  190. unified.publish_policy(joint["uid"], expected_version=1, actor_uid=actor)
  191. review = unified.evaluate_joint_review(
  192. "data_product",
  193. object_uids["data_product"],
  194. [
  195. {"user_uid": owner_a, "domain_uid": domain_a, "decision": "approve"},
  196. {"user_uid": owner_b, "domain_uid": domain_b, "decision": "approve"},
  197. ],
  198. )
  199. assert review["status"] == "approved"
  200. assert review["deterministic"] is True
  201. audit_count = session.execute(
  202. text(
  203. """
  204. SELECT COUNT(*)
  205. FROM public.governance_responsibility_audit_events
  206. WHERE actor_uid = CAST(:actor_uid AS uuid)
  207. """
  208. ),
  209. {"actor_uid": actor},
  210. ).scalar_one()
  211. assert audit_count >= 14
  212. assert delegation["current_version"] == 1
  213. finally:
  214. session.rollback()
  215. session.execute(
  216. text(
  217. """
  218. UPDATE public.governance_responsibility_policies
  219. SET active_version_uid = NULL
  220. WHERE created_by = CAST(:actor_uid AS uuid);
  221. DELETE FROM public.governance_responsibility_policy_versions
  222. WHERE created_by = CAST(:actor_uid AS uuid);
  223. DELETE FROM public.governance_responsibility_policies
  224. WHERE created_by = CAST(:actor_uid AS uuid);
  225. DELETE FROM public.governance_responsibility_delegations
  226. WHERE created_by = CAST(:actor_uid AS uuid);
  227. DELETE FROM public.governance_responsibility_hierarchy
  228. WHERE updated_by = CAST(:actor_uid AS uuid);
  229. DELETE FROM public.governance_responsibility_scopes
  230. WHERE updated_by = CAST(:actor_uid AS uuid);
  231. DELETE FROM public.governance_responsibility_audit_events
  232. WHERE actor_uid = CAST(:actor_uid AS uuid);
  233. DELETE FROM public.users
  234. WHERE id::text = ANY(:user_uids);
  235. """
  236. ),
  237. {"actor_uid": actor, "user_uids": users},
  238. )
  239. session.commit()
  240. session.close()
  241. engine.dispose()