From b2903e6adf74e74f8ab32d76f5cabc531cad0908 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Fri, 10 Jul 2026 12:51:22 +0200 Subject: [PATCH] chore: sync GovOPlaN module split state --- .gitignore | 23 ++++ README.md | 3 + docs/POLICY_DECISION_PROVENANCE.md | 68 +++++++++++ src/govoplan_policy.egg-info/PKG-INFO | 18 --- src/govoplan_policy.egg-info/SOURCES.txt | 16 --- .../dependency_links.txt | 1 - src/govoplan_policy.egg-info/entry_points.txt | 2 - src/govoplan_policy.egg-info/requires.txt | 2 - src/govoplan_policy.egg-info/top_level.txt | 1 - .../__pycache__/__init__.cpython-312.pyc | Bin 196 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 196 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 232 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 232 -> 0 bytes .../__pycache__/manifest.cpython-312.pyc | Bin 805 -> 0 bytes .../__pycache__/manifest.cpython-313.pyc | Bin 803 -> 0 bytes .../api/__pycache__/__init__.cpython-312.pyc | Bin 170 -> 0 bytes .../api/__pycache__/__init__.cpython-313.pyc | Bin 170 -> 0 bytes .../v1/__pycache__/__init__.cpython-312.pyc | Bin 173 -> 0 bytes .../v1/__pycache__/__init__.cpython-313.pyc | Bin 173 -> 0 bytes .../api/v1/__pycache__/routes.cpython-312.pyc | Bin 8458 -> 0 bytes .../api/v1/__pycache__/routes.cpython-313.pyc | Bin 8212 -> 0 bytes .../v1/__pycache__/schemas.cpython-312.pyc | Bin 5834 -> 0 bytes .../v1/__pycache__/schemas.cpython-313.pyc | Bin 5964 -> 0 bytes src/govoplan_policy/backend/api/v1/routes.py | 106 +++++++++++++++++- src/govoplan_policy/backend/api/v1/schemas.py | 21 ++++ 25 files changed, 218 insertions(+), 43 deletions(-) create mode 100644 .gitignore create mode 100644 docs/POLICY_DECISION_PROVENANCE.md delete mode 100644 src/govoplan_policy.egg-info/PKG-INFO delete mode 100644 src/govoplan_policy.egg-info/SOURCES.txt delete mode 100644 src/govoplan_policy.egg-info/dependency_links.txt delete mode 100644 src/govoplan_policy.egg-info/entry_points.txt delete mode 100644 src/govoplan_policy.egg-info/requires.txt delete mode 100644 src/govoplan_policy.egg-info/top_level.txt delete mode 100644 src/govoplan_policy/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/govoplan_policy/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/govoplan_policy/backend/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/govoplan_policy/backend/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/govoplan_policy/backend/__pycache__/manifest.cpython-312.pyc delete mode 100644 src/govoplan_policy/backend/__pycache__/manifest.cpython-313.pyc delete mode 100644 src/govoplan_policy/backend/api/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/govoplan_policy/backend/api/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-312.pyc delete mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-313.pyc delete mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-312.pyc delete mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-313.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fd4061 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.venv/ +build/ +dist/ +node_modules/ +webui/node_modules/ +webui/dist/ +*.tsbuildinfo +.component-test-build/ +.module-test-build/ +.policy-test-build/ +.template-preview-test-build/ +.import-test-build/ +webui/.component-test-build/ +webui/.module-test-build/ +webui/.policy-test-build/ +webui/.template-preview-test-build/ +webui/.import-test-build/ diff --git a/README.md b/README.md index 032dd5f..54e3741 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,6 @@ GovOPlaN module split. The current package delegates to the legacy access administration implementation while route ownership is separated before model migration. +Policy decision and provenance payloads use the shared kernel DTOs documented +in [docs/POLICY_DECISION_PROVENANCE.md](docs/POLICY_DECISION_PROVENANCE.md) +and `/mnt/DATA/git/govoplan-core/docs/POLICY_CONTRACTS.md`. diff --git a/docs/POLICY_DECISION_PROVENANCE.md b/docs/POLICY_DECISION_PROVENANCE.md new file mode 100644 index 0000000..f8d7db6 --- /dev/null +++ b/docs/POLICY_DECISION_PROVENANCE.md @@ -0,0 +1,68 @@ +# Policy Decision And Provenance Contract + +`govoplan-policy` owns policy and retention route contributions. Core keeps the +small shared DTOs that let policy decisions look the same across modules. + +## Backend DTOs + +Use `govoplan_core.core.policy.PolicyDecision` for explainable policy results: + +- `allowed`: effective decision for the checked action. +- `reason`: compact operator-readable explanation. +- `source_path`: ordered policy sources that produced the decision. +- `requirements`: machine-readable blockers or prerequisites. +- `details`: domain-specific structured context, redacted when needed. + +Use `PolicySourceStep` or `policy_source_step()` for each provenance step: + +- `scope_type`: `system`, `tenant`, `user`, `group`, or `campaign`. +- `scope_id`: stable ID for non-system scopes. +- `path`: stable string path generated by `policy_source_path()`. +- `label`: concrete source label such as `System`, `Tenant`, `Owner user`, + `Group`, or `Campaign`. +- `applied_fields`: field names affected by that step. +- `policy`: local policy fragment that explains the applied fields. + +Do not build or split provenance paths manually. Use +`policy_source_path()` and `parse_policy_source_path()` so IDs are URL-encoded +consistently. + +## Retention Explain Endpoint + +Retention policy exposes the shared shape through: + +```text +GET /api/v1/admin/privacy-retention/policies/{scope_type}/explain +``` + +The response contains `decision`, `effective_policy`, `parent_policy`, +`effective_policy_sources`, `parent_policy_sources`, and `blocked_fields`. +Clients can use `blocked_fields` to disable controls before a save attempt. + +## UI Expectations + +Policy UIs should render provenance close to the effective column or field it +explains. The display path should use concrete source labels and local values, +for example: + +```text +System: Allow +> Tenant: Deny without override +``` + +If all lower levels still inherit, continue the path until the effective local +decision: + +```text +System: Allow +> Tenant: Inherit +> Group: Inherit +> Campaign: Deny +``` + +When a parent disallows lower-level limits or changes, the UI should disable +the affected controls and avoid sending those fields in the save payload. + +The shared core WebUI helper `PolicySourcePath` renders the source path shape +for module UIs. Modules may use their own field layout, but the data contract +should remain this shape. diff --git a/src/govoplan_policy.egg-info/PKG-INFO b/src/govoplan_policy.egg-info/PKG-INFO deleted file mode 100644 index 1015a51..0000000 --- a/src/govoplan_policy.egg-info/PKG-INFO +++ /dev/null @@ -1,18 +0,0 @@ -Metadata-Version: 2.4 -Name: govoplan-policy -Version: 0.1.4 -Summary: GovOPlaN policy platform module. -Author: GovOPlaN -Requires-Python: >=3.12 -Description-Content-Type: text/markdown -Requires-Dist: govoplan-core>=0.1.4 -Requires-Dist: govoplan-access>=0.1.4 - -# GovOPlaN Policy - -`govoplan-policy` owns policy and retention API route contributions during the -GovOPlaN module split. - -The current package delegates to the legacy access administration -implementation while route ownership is separated before model migration. - diff --git a/src/govoplan_policy.egg-info/SOURCES.txt b/src/govoplan_policy.egg-info/SOURCES.txt deleted file mode 100644 index 9060033..0000000 --- a/src/govoplan_policy.egg-info/SOURCES.txt +++ /dev/null @@ -1,16 +0,0 @@ -README.md -pyproject.toml -src/govoplan_policy/__init__.py -src/govoplan_policy/py.typed -src/govoplan_policy.egg-info/PKG-INFO -src/govoplan_policy.egg-info/SOURCES.txt -src/govoplan_policy.egg-info/dependency_links.txt -src/govoplan_policy.egg-info/entry_points.txt -src/govoplan_policy.egg-info/requires.txt -src/govoplan_policy.egg-info/top_level.txt -src/govoplan_policy/backend/__init__.py -src/govoplan_policy/backend/manifest.py -src/govoplan_policy/backend/api/__init__.py -src/govoplan_policy/backend/api/v1/__init__.py -src/govoplan_policy/backend/api/v1/routes.py -src/govoplan_policy/backend/api/v1/schemas.py \ No newline at end of file diff --git a/src/govoplan_policy.egg-info/dependency_links.txt b/src/govoplan_policy.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/src/govoplan_policy.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/govoplan_policy.egg-info/entry_points.txt b/src/govoplan_policy.egg-info/entry_points.txt deleted file mode 100644 index eee1d6e..0000000 --- a/src/govoplan_policy.egg-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[govoplan.modules] -policy = govoplan_policy.backend.manifest:get_manifest diff --git a/src/govoplan_policy.egg-info/requires.txt b/src/govoplan_policy.egg-info/requires.txt deleted file mode 100644 index 58ec5d6..0000000 --- a/src/govoplan_policy.egg-info/requires.txt +++ /dev/null @@ -1,2 +0,0 @@ -govoplan-core>=0.1.4 -govoplan-access>=0.1.4 diff --git a/src/govoplan_policy.egg-info/top_level.txt b/src/govoplan_policy.egg-info/top_level.txt deleted file mode 100644 index 8e22451..0000000 --- a/src/govoplan_policy.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -govoplan_policy diff --git a/src/govoplan_policy/__pycache__/__init__.cpython-312.pyc b/src/govoplan_policy/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index b52f7e847485553fafe486200fec254fd48daf63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 196 zcmX@j%ge<81V1{xv*dyFV-N=h7@>^M96-i&h7^V CBQ;F` diff --git a/src/govoplan_policy/__pycache__/__init__.cpython-313.pyc b/src/govoplan_policy/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 9e43ecc7788208e4522e8b55a323e54430d12326..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 196 zcmey&%ge<81V1{xv*dyFV-N=h7@>^M96-iYhG2#whIB?vrYdpw{4)Q5oJ2o`g8ZD! z^M96-i&h7^V`)wo?n(^M96-iYhG2#whIB?vrYd8n#N_PMycC7Zypq)P zqQsKS{5*xU{33;tj8p~p{4)Q5oJ2o`g8ZD!`)wo?n(Kp;gdRFCk{8OSDPKOj@?0(u4m) z{|3?jWG^1W;DX>myeZw&o_sTD3O<;}dvD%+-}fGK)#|9Hq|nHtY^m9fZQeeCu3}Ch#JHoCb5W3&9BZEv&I-(#3L=}u$OJW z^IH~uEZjKHi!v_LBImv)I>Um@r}RmYm-Mm}-8vn{c{-uIOhDewG>CMB@a6!5HTnqB zKoE%g0OH=`m&n&gOf~_F#fo`J84x_UC>F(R8t2ihn5M}xcpWEiX-MXoS%di1_SObQfCA->|B;1mO*>;d%wiMk+aYd6j zDGRn_E}&a*cn91HRnFeEar(pUt%e&YAYs+`!g`^EzcY`6li-mYE>054xo-+BCBn$# zj0$@}8Hb$;kI)&6NAo15Ty*}UOeAh&aT7TXp&qK0G0CTh*s2rlG&{21IH9bQ&x;)jO*CgB#cKw3Tzy$M`@Y{sTl+&hY>M diff --git a/src/govoplan_policy/backend/__pycache__/manifest.cpython-313.pyc b/src/govoplan_policy/backend/__pycache__/manifest.cpython-313.pyc deleted file mode 100644 index adb126a540f74427148f742b4ae3c4c06488dfc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 803 zcmY*X&59F25bmD8Ofr)!$u25k4`I*Sd!$Q&wq&gJw5|?%*8l@^C(xVZ$^i4yI3a-%n1L0PF3rGxY=RZIL0NT6<4U{w zL&CPHaH2HL@+eQTl(#ie?PtUJggwpDoW0LQZI||=G#N3T$0+XBG>mjY==KPMHR+%* zl!MYz+9I#%ebUwkATJ?{1=&1jfCLZ5*&>@wqBNXklO$exZ=?7fONU-GOT5LIr!I0R z$#d63_f;n)?@>R~>(xFdWUXz`_tv1QE?* zt6`i0^OT#aufY|BdZ<}Hz(*OVf diff --git a/src/govoplan_policy/backend/api/__pycache__/__init__.cpython-312.pyc b/src/govoplan_policy/backend/api/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 6cbbae671296e31b6e6bf59375d5182bbd8ff7eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 170 zcmX@j%ge<81ULJ=vq1D?5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!^3>1GE75mx3~|&? z&n(eT&o9d_$Vtr8Ey&NwOs>=~E=q=r#Y4rC5|gu2^HTH^3o`ZN<1_OzOXB183Mzkb f*yQG?l;)(`6|n+MW(4A55aS~=BO_xGGmr%UQD!TB diff --git a/src/govoplan_policy/backend/api/__pycache__/__init__.cpython-313.pyc b/src/govoplan_policy/backend/api/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 727ddffb02dc49e829bb6a4421a3521e98be2a53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 170 zcmey&%ge<81ULJ=vq1D?5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~iSer{fgzKdgs zqkej3iGF&1S$;uIVxDe6eokg`rG9ZyGF&VkDwdR(oSmANqMulhsUIJonU`4-AFo$X hd5gm)H$SB`C)KWq6=*WZ=3)@zBQql-V-Yiu1prXTD^dUe diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-312.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 8e6b8e13d97e2a71588a76941a8cf9abca85b5ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173 zcmX@j%ge<81ULJ=vq1D?5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!^3l)DE75mx3~|&? z&n(eT&o9d_$Vtr8Ey&NwOs>=~E=q=r#Y4rC5|gu2^HTH^3o`Y~4E5vVGxIV_;^XxS iDt~d<uWe1ma>4<0CU8BV!RWkOctT%q&&_ diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-313.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index aa48d0e62a40fc556453965cb33cfcdcebb6535e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173 zcmey&%ge<81ULJ=vq1D?5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~i4er{fgzKdgs zqkej3iGF&1S$;uIVxDe6eokg`rG9ZyGF&VkDwdR(oSmANqMulhsb6NOA0MBYmst`Y kuUAlci^C>2KczG$)vkyYXgJ97Vi4maGb1Bo5i^hl0Ne~LD*ylh diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-312.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-312.pyc deleted file mode 100644 index 64bd49ad598847afa103ed7f0efe94c78c114683..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8458 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 TOalf)432k+{h9-i?%{s|C&I+i diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-313.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-313.pyc deleted file mode 100644 index 68ae94d30311f1663473abcaaf4512eb7c66c72f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8212 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 diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-312.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-312.pyc deleted file mode 100644 index 619bf004ec0de52f3a6e1de87180891c76d8bcea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5834 zcmcIoTWlN06`duQZ;I4|lJ&46#&+s357CZaavVFcEy+?W(+|lBOxNA4I3s!O<&v3Q z%9SbuLX!Xr5TI`J5fy1swf%`p0qdVP5Y9*v_I(wy(<}@?n#LSekW1S&!s=O zlR?@+z3VQ+c=HHdYL05#_1qzbSGG*jZm#L2vni&hivBA~3?^`>w00OOpAf5J z>nA@WDTfC4HE462%tDxb?D)tdgXT6F1u2QzEfRAi#$dU-E)9$gB*uo%CzK24M}|*d zQ3i(xM*5Yr16M}D?7YdQO$b;LeR4LQHmrF6$+461NzIB+W~MS(T{T|LW^^qz9XENZ zqEkw`aW!?588ohDwfNNjxS6`n(yG~;owlQtO{h8DQdC{f+*06XToK=?=vrE{Ox^}B z?SR|t1TjY*Kl|EW0>>A2&!4^%`EdBl!126%{6A2bGMGQ~x5+7-C0^igQcvQ=w;)kk-23= zE1455x;HBF?bT?^E1YHfHB&Q8OEpr=mMJEt|1DL|u>sCA+;%}0r%l^wTAa7QoNReK zlhI>t+XV@mV|K%Yrt6SlA}mbXZ)A8HGI@qk9>L}wF=a+=UrJ}Hp9NBv+6pl#Y23)+zjBC$s^Ii6yp7RwHU13508?Ke}IEK{uMEK5A`gGZyY z(L}~zwzp`|sO{3#ai-su>~J~LC)5;x>GX_G*G%gwbh>7T)NEGQ0AEEUnNLC(HE4P% z3dn!Wb{7LJ=C{3yVyJ1RD7Ifw(ixi5u^v>EcXO&H*cj!E{!uNhV;Fge02M=a$H zC_*R@Fl~7vr|UC6C~=Idd+~AaOVPcs>qERUiuJT+Xn;j~ukj}A_Y#OF;f+v59J^+> zZwINidz``48BAP;$YrV0mI9*?f}sHkF0Ji^3T)(+&1qH=fl+D;(Cti&-IaJ0nn8ge z2A}Ip=_c=ix{2+g(IN`&R=yhsXHIOt(!Ed7|8;yRuErc$caz3f9z6HZDuhP>H$@KI zmBq9;?!&elHjW%O#6zrw?*|=!1;qgrUThM?h0&Fi;B>;J9CJ`4@3y`xdINa^KZOFh zw;f37s%fT~bv;A*SyUrMs><{}d_;k7H~$3U1G3W6y*N~8d3kQ+t3A)><*tRXCHeW4 z1APx8kDe_Y7|F|f76Ac9R^kUgAA6`4;zM~ky7nv31CW>p#uR%;0woDaE$zILvrTjrvsAm*qA|2 z+BQK+*(iegG`_AM#P^1cL4F3u7(y`&q8ckihRmG)j-`1TK)8q_*8)PW5I&dpo-4-c z2-suI^^%__aAsnapElCVq%-+F0|^Wi84;5PpVq9)B>jANQ#U6SMf$21=TOR z1#Gq+dslt8k0Q6b#NN#@piGsd5WAjOP(hv&G2iySM9Q_%w20)JQ1~)7LGd=+CX&i( z_k7)ZAm5)V^wPP(eDKABOp7su?dz|`99XJXV_RzM2EyeLXi_1R zsfY0LOs8Pz_2@s)oXsy$O!0$Y+O0JsmDMr-<~R{aE~xnxoVo~>Ls(0qxQb#^uwaaE z6xZN35lN7o55fLh#mztsHwJfAMnR*jpm|`j+IkR{D-V zqJ_TEyxfuRI#iHHi?M~n*N-jyYp9N`t(jI!+C^@)R2b}~oUtXDiqsX@DxX$(&Pc>O zn->XLQ4tt_gf+2dn^pxEgQJ*)+iU|-okCwl+UL&9j}_$h;`d?0`rp?Mmg?`@$Q_7j z*^bnHNim2yh8_|-6V7lFF?sX!>&^Ea=)zH?;WqID!HdDpg4|i0A)4TLf}ewup9B$e zZ;(hqe2au%*e{{I4SxnFazHNOXOJT_a^%yt8-7eRV^R=Kj%Nh6H#pyu#9DRu+3URC91thKS=lANY6jXsU>o1)$ML`=CuUtD;T~MyNiL2uq z57hFcYb_*6C)T_Hscn6yOFFu6be%x4-WZn7NsArp1k3e3VJRUkYWS3ZC#S@u&%lP| jde9?{NuR~m36|>-r_`|k4^XTJC8=xS=)VaR!WsVsHnp1^ diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-313.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-313.pyc deleted file mode 100644 index 64b18c82d8914a010da841a69bb05bc96e9f3faa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5964 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 diff --git a/src/govoplan_policy/backend/api/v1/routes.py b/src/govoplan_policy/backend/api/v1/routes.py index df4d67f..dfcb370 100644 --- a/src/govoplan_policy/backend/api/v1/routes.py +++ b/src/govoplan_policy/backend/api/v1/routes.py @@ -1,13 +1,22 @@ from __future__ import annotations +from typing import Any + from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session -from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal, has_scope, require_scope +from govoplan_access.auth import ApiPrincipal, get_api_principal, has_scope, require_scope from govoplan_core.audit.logging import audit_from_principal +from govoplan_core.core.configuration_control import ( + ConfigurationControlError, + ensure_configuration_change_allowed, + record_configuration_change_applied, +) +from govoplan_core.core.policy import PolicyDecision, PolicySourceStep from govoplan_core.db.session import get_session from govoplan_core.privacy.retention import ( PrivacyPolicyError, + RETENTION_POLICY_FIELD_KEYS, apply_retention_policy, effective_privacy_policy, effective_privacy_policy_sources, @@ -18,6 +27,7 @@ from govoplan_core.privacy.retention import ( ) from .schemas import ( + PrivacyRetentionPolicyExplainResponse, PrivacyRetentionPolicyItem, PrivacyRetentionPolicyScopeRequest, PrivacyRetentionPolicyScopeResponse, @@ -47,6 +57,13 @@ def _require_privacy_policy_write(principal: ApiPrincipal, scope_type: str) -> N _require_permission(principal, "admin:policies:write") +def _configuration_control_http_error(exc: ConfigurationControlError) -> HTTPException: + return HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={"code": exc.code, "message": str(exc), "plan": exc.plan}, + ) + + @router.get("/privacy-retention/policies/{scope_type}", response_model=PrivacyRetentionPolicyScopeResponse) def read_privacy_retention_policy( scope_type: str, @@ -83,13 +100,51 @@ def write_privacy_retention_policy( ): clean_scope = scope_type.strip().casefold() _require_privacy_policy_write(principal, clean_scope) + policy_value = payload.policy.model_dump(mode="json", exclude_none=True) + before_value: dict[str, Any] | None = None + if clean_scope == "system": + before_value = get_privacy_policy_for_scope(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id) + try: + approval = ensure_configuration_change_allowed( + session, + key="privacy_retention_policy", + value=policy_value, + actor_user_id=principal.user.id, + actor_scopes=tuple(principal.scopes), + change_request_id=payload.change_request_id, + target={"scope_type": clean_scope, "scope_id": scope_id}, + ) + except ConfigurationControlError as exc: + raise _configuration_control_http_error(exc) from exc + else: + approval = None try: policy = set_privacy_policy_for_scope( session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id, - policy=payload.policy.model_dump(mode="json", exclude_none=True), + policy=policy_value, + ) + if clean_scope == "system": + record_configuration_change_applied( + session, + key="privacy_retention_policy", + before_value=before_value, + after_value=policy, + actor_user_id=principal.user.id, + approval=approval, + target={"scope_type": clean_scope, "scope_id": scope_id}, + audit_event="privacy_retention.policy_updated", + ) + audit_from_principal( + session, + principal, + action="privacy_retention.policy_updated", + scope="system" if clean_scope == "system" else "tenant", + object_type="privacy_retention_policy", + object_id=clean_scope if scope_id is None else f"{clean_scope}:{scope_id}", + details={"scope_type": clean_scope, "scope_id": scope_id}, ) session.commit() effective = _effective_privacy_policy_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id) @@ -105,7 +160,52 @@ def write_privacy_retention_policy( ) except PrivacyPolicyError as exc: session.rollback() - raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc + + +@router.get("/privacy-retention/policies/{scope_type}/explain", response_model=PrivacyRetentionPolicyExplainResponse) +def explain_privacy_retention_policy( + scope_type: str, + scope_id: str | None = Query(default=None), + session: Session = Depends(get_session), + principal: ApiPrincipal = Depends(get_api_principal), +): + clean_scope = scope_type.strip().casefold() + _require_privacy_policy_read(principal, clean_scope) + try: + effective = _effective_privacy_policy_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id) + parent = _parent_privacy_policy_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id) + effective_sources = _effective_privacy_policy_sources_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id) + parent_sources = _parent_privacy_policy_sources_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id) + blocked_fields = _blocked_privacy_retention_fields(parent) + decision_sources = parent_sources or effective_sources + decision = PolicyDecision( + allowed=not blocked_fields, + reason="Parent retention policy locks lower-level changes." if blocked_fields else None, + source_path=tuple(PolicySourceStep.from_mapping(source) for source in decision_sources), + requirements=tuple(blocked_fields), + details={"blocked_fields": blocked_fields}, + ) + return PrivacyRetentionPolicyExplainResponse( + scope_type=clean_scope, + scope_id=scope_id, + decision=decision.to_dict(), + effective_policy=PrivacyRetentionPolicyItem.model_validate(effective.model_dump(mode="json")), + parent_policy=PrivacyRetentionPolicyItem.model_validate(parent.model_dump(mode="json")) if parent else None, + effective_policy_sources=effective_sources, + parent_policy_sources=parent_sources, + blocked_fields=blocked_fields, + ) + except PrivacyPolicyError as exc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc + + +def _blocked_privacy_retention_fields(parent) -> list[str]: + if parent is None: + return [] + payload = parent.model_dump(mode="json") + allow_lower_level_limits = payload.get("allow_lower_level_limits") or {} + return [key for key in RETENTION_POLICY_FIELD_KEYS if allow_lower_level_limits.get(key) is False] def _parent_privacy_policy_sources_for_response(session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None): diff --git a/src/govoplan_policy/backend/api/v1/schemas.py b/src/govoplan_policy/backend/api/v1/schemas.py index 30ff092..039d5ea 100644 --- a/src/govoplan_policy/backend/api/v1/schemas.py +++ b/src/govoplan_policy/backend/api/v1/schemas.py @@ -43,6 +43,7 @@ def normalize_allow_lower_level_limits(value: Any, *, fill_defaults: bool) -> di class PolicySourceStepItem(BaseModel): scope_type: str scope_id: str | None = None + path: str label: str applied_fields: list[str] = Field(default_factory=list) policy: dict[str, Any] = Field(default_factory=dict) @@ -88,6 +89,7 @@ class PrivacyRetentionPolicyScopeRequest(BaseModel): model_config = ConfigDict(extra="forbid") policy: PrivacyRetentionPolicyPatchItem = Field(default_factory=PrivacyRetentionPolicyPatchItem) + change_request_id: str | None = None class PrivacyRetentionPolicyScopeResponse(BaseModel): @@ -100,6 +102,25 @@ class PrivacyRetentionPolicyScopeResponse(BaseModel): parent_policy_sources: list[PolicySourceStepItem] = Field(default_factory=list) +class PolicyDecisionItem(BaseModel): + allowed: bool + reason: str | None = None + source_path: list[PolicySourceStepItem] = Field(default_factory=list) + requirements: list[str] = Field(default_factory=list) + details: dict[str, Any] = Field(default_factory=dict) + + +class PrivacyRetentionPolicyExplainResponse(BaseModel): + scope_type: Literal["system", "tenant", "user", "group", "campaign"] + scope_id: str | None = None + decision: PolicyDecisionItem + effective_policy: PrivacyRetentionPolicyItem + parent_policy: PrivacyRetentionPolicyItem | None = None + effective_policy_sources: list[PolicySourceStepItem] = Field(default_factory=list) + parent_policy_sources: list[PolicySourceStepItem] = Field(default_factory=list) + blocked_fields: list[str] = Field(default_factory=list) + + class RetentionRunRequest(BaseModel): model_config = ConfigDict(extra="forbid")