From 0f601fc693dededb675a86d6bce9f1857102c30b Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 7 Jul 2026 15:49:06 +0200 Subject: [PATCH] Release v0.1.5 --- .gitea/ISSUE_TEMPLATE/bug_report.md | 35 +++ .gitea/ISSUE_TEMPLATE/config.yaml | 1 + .gitea/ISSUE_TEMPLATE/docs_workflow.md | 27 ++ .gitea/ISSUE_TEMPLATE/feature_request.md | 32 +++ .gitea/ISSUE_TEMPLATE/task.md | 28 +++ .gitea/ISSUE_TEMPLATE/tech_debt.md | 25 ++ .gitea/PULL_REQUEST_TEMPLATE.md | 15 ++ README.md | 8 + pyproject.toml | 25 ++ src/govoplan_audit.egg-info/PKG-INFO | 18 ++ src/govoplan_audit.egg-info/SOURCES.txt | 18 ++ .../dependency_links.txt | 1 + src/govoplan_audit.egg-info/entry_points.txt | 2 + src/govoplan_audit.egg-info/requires.txt | 2 + src/govoplan_audit.egg-info/top_level.txt | 1 + src/govoplan_audit/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 193 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 193 bytes src/govoplan_audit/backend/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 229 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 229 bytes .../__pycache__/manifest.cpython-312.pyc | Bin 0 -> 1527 bytes .../__pycache__/manifest.cpython-313.pyc | Bin 0 -> 1478 bytes src/govoplan_audit/backend/api/__init__.py | 0 .../api/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 168 bytes .../api/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 168 bytes src/govoplan_audit/backend/api/v1/__init__.py | 0 .../v1/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 171 bytes .../v1/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 171 bytes .../api/v1/__pycache__/routes.cpython-312.pyc | Bin 0 -> 13445 bytes .../api/v1/__pycache__/routes.cpython-313.pyc | Bin 0 -> 13601 bytes .../v1/__pycache__/schemas.cpython-312.pyc | Bin 0 -> 2185 bytes .../v1/__pycache__/schemas.cpython-313.pyc | Bin 0 -> 2315 bytes src/govoplan_audit/backend/api/v1/routes.py | 233 ++++++++++++++++++ src/govoplan_audit/backend/api/v1/schemas.py | 44 ++++ src/govoplan_audit/backend/db/__init__.py | 2 + .../db/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 225 bytes .../db/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 225 bytes .../db/__pycache__/models.cpython-312.pyc | Bin 0 -> 2161 bytes .../db/__pycache__/models.cpython-313.pyc | Bin 0 -> 2151 bytes src/govoplan_audit/backend/db/models.py | 34 +++ src/govoplan_audit/backend/manifest.py | 36 +++ src/govoplan_audit/py.typed | 1 + 43 files changed, 592 insertions(+) create mode 100644 .gitea/ISSUE_TEMPLATE/bug_report.md create mode 100644 .gitea/ISSUE_TEMPLATE/config.yaml create mode 100644 .gitea/ISSUE_TEMPLATE/docs_workflow.md create mode 100644 .gitea/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitea/ISSUE_TEMPLATE/task.md create mode 100644 .gitea/ISSUE_TEMPLATE/tech_debt.md create mode 100644 .gitea/PULL_REQUEST_TEMPLATE.md create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/govoplan_audit.egg-info/PKG-INFO create mode 100644 src/govoplan_audit.egg-info/SOURCES.txt create mode 100644 src/govoplan_audit.egg-info/dependency_links.txt create mode 100644 src/govoplan_audit.egg-info/entry_points.txt create mode 100644 src/govoplan_audit.egg-info/requires.txt create mode 100644 src/govoplan_audit.egg-info/top_level.txt create mode 100644 src/govoplan_audit/__init__.py create mode 100644 src/govoplan_audit/__pycache__/__init__.cpython-312.pyc create mode 100644 src/govoplan_audit/__pycache__/__init__.cpython-313.pyc create mode 100644 src/govoplan_audit/backend/__init__.py create mode 100644 src/govoplan_audit/backend/__pycache__/__init__.cpython-312.pyc create mode 100644 src/govoplan_audit/backend/__pycache__/__init__.cpython-313.pyc create mode 100644 src/govoplan_audit/backend/__pycache__/manifest.cpython-312.pyc create mode 100644 src/govoplan_audit/backend/__pycache__/manifest.cpython-313.pyc create mode 100644 src/govoplan_audit/backend/api/__init__.py create mode 100644 src/govoplan_audit/backend/api/__pycache__/__init__.cpython-312.pyc create mode 100644 src/govoplan_audit/backend/api/__pycache__/__init__.cpython-313.pyc create mode 100644 src/govoplan_audit/backend/api/v1/__init__.py create mode 100644 src/govoplan_audit/backend/api/v1/__pycache__/__init__.cpython-312.pyc create mode 100644 src/govoplan_audit/backend/api/v1/__pycache__/__init__.cpython-313.pyc create mode 100644 src/govoplan_audit/backend/api/v1/__pycache__/routes.cpython-312.pyc create mode 100644 src/govoplan_audit/backend/api/v1/__pycache__/routes.cpython-313.pyc create mode 100644 src/govoplan_audit/backend/api/v1/__pycache__/schemas.cpython-312.pyc create mode 100644 src/govoplan_audit/backend/api/v1/__pycache__/schemas.cpython-313.pyc create mode 100644 src/govoplan_audit/backend/api/v1/routes.py create mode 100644 src/govoplan_audit/backend/api/v1/schemas.py create mode 100644 src/govoplan_audit/backend/db/__init__.py create mode 100644 src/govoplan_audit/backend/db/__pycache__/__init__.cpython-312.pyc create mode 100644 src/govoplan_audit/backend/db/__pycache__/__init__.cpython-313.pyc create mode 100644 src/govoplan_audit/backend/db/__pycache__/models.cpython-312.pyc create mode 100644 src/govoplan_audit/backend/db/__pycache__/models.cpython-313.pyc create mode 100644 src/govoplan_audit/backend/db/models.py create mode 100644 src/govoplan_audit/backend/manifest.py create mode 100644 src/govoplan_audit/py.typed diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..3cfefae --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: "Bug" +about: "Report a reproducible defect, regression, or incorrect behavior" +title: "[Bug] " +labels: + - type/bug + - status/triage + - module/audit +--- + +## Scope + +- Repository: +- Area/module: +- Affected version or commit: + +## Behavior + +Expected: + +Actual: + +## Reproduction + +1. +2. +3. + +## Evidence + +Logs, screenshots, traces, or failing test output: + +## Verification Target + +Command or workflow that should pass when fixed: diff --git a/.gitea/ISSUE_TEMPLATE/config.yaml b/.gitea/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.gitea/ISSUE_TEMPLATE/docs_workflow.md b/.gitea/ISSUE_TEMPLATE/docs_workflow.md new file mode 100644 index 0000000..e24ce27 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/docs_workflow.md @@ -0,0 +1,27 @@ +--- +name: "Docs / workflow" +about: "Request documentation, process, or developer workflow changes" +title: "[Docs] " +labels: + - type/docs + - status/triage + - module/audit + - area/docs +--- + +## Scope + +- Repository: +- Document or workflow: + +## Current State + +What is missing, unclear, duplicated, or stale? + +## Desired State + +What should the docs or workflow make clear? + +## Verification Target + +How should this be checked? diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.md b/.gitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..b8f3497 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,32 @@ +--- +name: "Feature" +about: "Propose new user-visible behavior or platform capability" +title: "[Feature] " +labels: + - type/feature + - status/triage + - module/audit +--- + +## Problem + +What user, operator, or developer problem should this solve? + +## Proposed Capability + +What should exist when this is done? + +## Ownership + +- Owning repository: +- Related module repositories: +- Extension point or integration boundary: + +## Acceptance Criteria + +- [ ] +- [ ] + +## Verification Target + +Command, scenario, or UI flow that should prove completion: diff --git a/.gitea/ISSUE_TEMPLATE/task.md b/.gitea/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..caedbbe --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/task.md @@ -0,0 +1,28 @@ +--- +name: "Task" +about: "Track implementation, maintenance, or migration work" +title: "[Task] " +labels: + - type/task + - status/triage + - module/audit +--- + +## Objective + +What needs to be completed? + +## Scope + +- Owning repository: +- In-scope: +- Out-of-scope: + +## Checklist + +- [ ] +- [ ] + +## Verification Target + +Command or manual check: diff --git a/.gitea/ISSUE_TEMPLATE/tech_debt.md b/.gitea/ISSUE_TEMPLATE/tech_debt.md new file mode 100644 index 0000000..24ef15a --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/tech_debt.md @@ -0,0 +1,25 @@ +--- +name: "Tech debt" +about: "Track cleanup, refactoring, risk reduction, or deferred engineering work" +title: "[Debt] " +labels: + - type/debt + - status/triage + - module/audit +--- + +## Current Cost + +What does this make harder, riskier, slower, or more fragile? + +## Desired Shape + +What should the code, tests, or architecture look like afterwards? + +## Constraints + +What behavior, compatibility, or module boundary must be preserved? + +## Verification Target + +Focused checks that should pass: diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1984736 --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Issue + +Closes # + +## Summary + +- + +## Verification + +- + +## Notes + +Follow-up issues: diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c50939 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# GovOPlaN Audit + +`govoplan-audit` owns audit API route contributions during the GovOPlaN module +split. + +This repository owns the live `audit_log` table while preserving the +historical table name. It provides audit API route contributions and is the +target boundary for future audit sink/export capability work. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9ffbe60 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "govoplan-audit" +version = "0.1.5" +description = "GovOPlaN audit platform module." +readme = "README.md" +requires-python = ">=3.12" +authors = [{ name = "GovOPlaN" }] +dependencies = [ + "govoplan-core>=0.1.5", + "govoplan-access>=0.1.5", +] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +govoplan_audit = ["py.typed"] + +[project.entry-points."govoplan.modules"] +audit = "govoplan_audit.backend.manifest:get_manifest" + diff --git a/src/govoplan_audit.egg-info/PKG-INFO b/src/govoplan_audit.egg-info/PKG-INFO new file mode 100644 index 0000000..c1e5d60 --- /dev/null +++ b/src/govoplan_audit.egg-info/PKG-INFO @@ -0,0 +1,18 @@ +Metadata-Version: 2.4 +Name: govoplan-audit +Version: 0.1.4 +Summary: GovOPlaN audit 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 Audit + +`govoplan-audit` owns audit API route contributions during the GovOPlaN module +split. + +This repository owns the live `audit_log` table while preserving the +historical table name. It provides audit API route contributions and is the +target boundary for future audit sink/export capability work. diff --git a/src/govoplan_audit.egg-info/SOURCES.txt b/src/govoplan_audit.egg-info/SOURCES.txt new file mode 100644 index 0000000..6128ccb --- /dev/null +++ b/src/govoplan_audit.egg-info/SOURCES.txt @@ -0,0 +1,18 @@ +README.md +pyproject.toml +src/govoplan_audit/__init__.py +src/govoplan_audit/py.typed +src/govoplan_audit.egg-info/PKG-INFO +src/govoplan_audit.egg-info/SOURCES.txt +src/govoplan_audit.egg-info/dependency_links.txt +src/govoplan_audit.egg-info/entry_points.txt +src/govoplan_audit.egg-info/requires.txt +src/govoplan_audit.egg-info/top_level.txt +src/govoplan_audit/backend/__init__.py +src/govoplan_audit/backend/manifest.py +src/govoplan_audit/backend/api/__init__.py +src/govoplan_audit/backend/api/v1/__init__.py +src/govoplan_audit/backend/api/v1/routes.py +src/govoplan_audit/backend/api/v1/schemas.py +src/govoplan_audit/backend/db/__init__.py +src/govoplan_audit/backend/db/models.py \ No newline at end of file diff --git a/src/govoplan_audit.egg-info/dependency_links.txt b/src/govoplan_audit.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/govoplan_audit.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/govoplan_audit.egg-info/entry_points.txt b/src/govoplan_audit.egg-info/entry_points.txt new file mode 100644 index 0000000..6c4e37e --- /dev/null +++ b/src/govoplan_audit.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[govoplan.modules] +audit = govoplan_audit.backend.manifest:get_manifest diff --git a/src/govoplan_audit.egg-info/requires.txt b/src/govoplan_audit.egg-info/requires.txt new file mode 100644 index 0000000..58ec5d6 --- /dev/null +++ b/src/govoplan_audit.egg-info/requires.txt @@ -0,0 +1,2 @@ +govoplan-core>=0.1.4 +govoplan-access>=0.1.4 diff --git a/src/govoplan_audit.egg-info/top_level.txt b/src/govoplan_audit.egg-info/top_level.txt new file mode 100644 index 0000000..e6ae8f4 --- /dev/null +++ b/src/govoplan_audit.egg-info/top_level.txt @@ -0,0 +1 @@ +govoplan_audit diff --git a/src/govoplan_audit/__init__.py b/src/govoplan_audit/__init__.py new file mode 100644 index 0000000..68c5967 --- /dev/null +++ b/src/govoplan_audit/__init__.py @@ -0,0 +1,2 @@ +"""GovOPlaN audit module.""" + diff --git a/src/govoplan_audit/__pycache__/__init__.cpython-312.pyc b/src/govoplan_audit/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..502716bb95ba75f8c3e7fef6e5650b064bbbb643 GIT binary patch literal 193 zcmX@j%ge<81V1{xv*dvEV-N=h7@>^M96-i&h7^V85wUe$P}>vIRHep BGu;3H literal 0 HcmV?d00001 diff --git a/src/govoplan_audit/__pycache__/__init__.cpython-313.pyc b/src/govoplan_audit/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6b96750e90192773e19b439fb60221b07ba5942 GIT binary patch literal 193 zcmey&%ge<81V1{xv*dvEV-N=h7@>^M96-iYhG2#whIB?vrYbS_{4)Q5oJ2o`#L|?^ z5{2CSl+v73JwHvxTkP@iDf!9q@hcfVgEZf=*3Zo=(RXnSanw)GEYVNTFUv2;NzBs) zYt%0;N`?!?LxkevGxIV_;^XxSDsOSvKuoqPVgs55a!oPFB_Eg>85wUe$P}>vIRHw4 BGq?Z% literal 0 HcmV?d00001 diff --git a/src/govoplan_audit/backend/__init__.py b/src/govoplan_audit/backend/__init__.py new file mode 100644 index 0000000..2b2d0e9 --- /dev/null +++ b/src/govoplan_audit/backend/__init__.py @@ -0,0 +1,2 @@ +"""Backend integration for the GovOPlaN audit module.""" + diff --git a/src/govoplan_audit/backend/__pycache__/__init__.cpython-312.pyc b/src/govoplan_audit/backend/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34dcb39065709d787e16f9b40fb061aed6e4f274 GIT binary patch literal 229 zcmX@j%ge<81V1{xv#fyhV-N=h7@>^M96-i&h7^VtksW?&&AV*M4ATWMsU>U{S;Z^M96-iYhG2#whIB?vrYa++#N_PMycC7Zypq)P zqQsKS{5*xU{33;tj8p~p{4)Q5oJ2o`#L|?^5{2CSl+v73JwHvxTkP@iDf!9q@hcfV zgABgqte=}#qVM7u;;5gVS)!kwUzT5xlbELq)~H`xlnfV&hX^G>tksW?&&AV*M4ATWMsU>U{S;Zp{KQO&6-TPdtZ4W|Ke2OHlPou-aR4VSTrE4WIeR>5V`IJiDfzx?AGtQw?vU@dKP z37aSNV?Eb!;_}1V(+KK2Q|T2NbV#tCtq>V%+*~l4n~A=$X@)&W5n0;detmXUO-5@=z(yAz6Fc` z89?b9KGD*_Vi(N0n0q+kz{|gf1zLy65$?d-KW3(=} z>7wlC^(dXmSeEtT&LE2U;O@@-oxwQZgL%ALD&Bxme__f~46afPqN~&N(?ij8RVH`j z`8=VAR1&TNA8WugMKg2dtho6{tv%hn00R;w6#*ji5soe?Y7&tU&X;Nh+Q6CfT&R6JrV0srmCNI(y{a))%KUL0qwoYYWYQh9eGT@*Y zjj6=g?ux4GjxyO<*QHPd6036>y+%tsr0_ODT7{gOUqN9ly#0idzM8I(Q1^>b`4mt? z?p*s%Uq)rsszOzlxb~_m9>VQlgvw`95tP0m1DcA1pCn@{Pp(=(Z%Ap;q!;E^Ud=Y` z!-DKI`wph_f~IL_=$*gN)-x16L;I&_{|w!{Fp65|c@x#wX65Fr(wfz~v-bL|V?FI! zv(DqiM@Sl cZ~r=eZ0?;G^tX)5H(PamYF^&dw4N&PUj${gLI3~& literal 0 HcmV?d00001 diff --git a/src/govoplan_audit/backend/__pycache__/manifest.cpython-313.pyc b/src/govoplan_audit/backend/__pycache__/manifest.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f5099c2be42831b6e9bf60b6813412c55f2eb562 GIT binary patch literal 1478 zcmaJ>PjB2r6d!wcZSUXgzX~NJ#iSvU4>(lnB_xVeKpb|PP}N03VkP6XCtHVI+j?U! zv={a(aESN{@C`U{;3G7rZbw8#LL9gyTDbFOHk)mbs3nizd-LYaZ{F`Wo>i+Qg3t5W zgOgwKg#00##nYG@%8a9NN*v-SUD8oNF@>#m)lLrb(#~}Y9SyXO4!ZR7U87TkVy6V9 zP8rHFR_InbRj8ufbhI^cl80Kayh$A0F`S}P>eYHh;q|JIZZ7~VzfQTQZIWL8k-3a5 zEL>mNRh_C++b#6;H)q{boRwW28cxfpOAO6H!)pH}N?8godr=gpUK+*`LsuKc0Ua_+ z<+lTf6F2qxL+S!eL!cuXrEUW8I1DIoGoipjmWtp!D~rO2rQUGp4l)k{E61yc;Q(Z9 zC!s#NagYt^$8nU>Z&O~IyN6yB9#fX$?)i^AMtx;*O8lz$<_J+{Z8T2FedGgKKt>|P zB%do=#L69kND|l~&Qc19eQOYp<7DVXu9pR2YWF?=YZ?W%mxT8Ct}XKzRKzls>;4>% z^qHt-ntYpqzjL$7&Q+D2(G_4jdCF_9Eao12ej3A5sN?b?zO0JFgiMW<^ZefL<;LW2 zh5^D05)d!k7r34kKCtiF_XKp$_bFqK(JTzTJ{>+g`2<@6nV*JZYA*Y43RajYtGpk7 z6Vbp7JZwMq&6F(Q+%Y!6OrKEmiVpOAFG(|?IJrz`k(aP1acWxyUPRiF88-~LF`}s_ zYV!7STbE@?5<^M@-g+&1Er$AQL6j3^md;fymXEv<<@#7iC60I{pb7FoBR`~!S8ueA z*G3DmxJ;;J^*?&LmLBs(D;JMYi&RJrjZ^aFB2G{n$`LlySn%}|0dc)y1qWUk(9~Vb zzr;S@bY<#~l-FWxq`iDR7zu0?tw)gP) z7w*~mR~IT-`&7LwYi;eaPKwr#>o2T>Gwa~j!I^P%kyGE%F5j+K)QR!WwxYBpz<&X9 CEv7^O literal 0 HcmV?d00001 diff --git a/src/govoplan_audit/backend/api/__init__.py b/src/govoplan_audit/backend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/govoplan_audit/backend/api/__pycache__/__init__.cpython-312.pyc b/src/govoplan_audit/backend/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91960e2248f1469296224be37694424eb7cb67ca GIT binary patch literal 168 zcmX@j%ge<81ULJ=vq1D?5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a@WtzE75mx3~|&? z&n(eT&o9d_$Vtr8O)O0Ta*K>P{wB#AY&>+I)f&o-%5reCLr%KNa~imer{fgzKdgs zqkej3iGF&1S$;uIVxDedX$p{AT$BtKiiZd#B_?O5=B4N-7G&zj$7kkcmc+;F6;$5h fu*uC&Da}c>D`Ewj3$nKu#Q4a}$jDg43}gWS-ykYp literal 0 HcmV?d00001 diff --git a/src/govoplan_audit/backend/api/v1/__init__.py b/src/govoplan_audit/backend/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/govoplan_audit/backend/api/v1/__pycache__/__init__.cpython-312.pyc b/src/govoplan_audit/backend/api/v1/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1524f7288204f07f753490abdc9cb26725c4a051 GIT binary patch literal 171 zcmX@j%ge<81ULJ=vq1D?5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!^3u=EE75mx3~|&? z&n(eT&o9d_$Vtr8O)O0Ta*K>P{wB#AY&>+I)f&o-%5reCLr%KNa~iCer{fgzKdgs zqkej3iGF&1S$;uIVxDedX$p{AT$BtKiiZd#B_?O5=B4N-7G&y|8S2N!XXa&=#K-Fu iRNmsS$<0qG%}KQ@Vg(uvvbq?=_{hx2$XLV-WB~wYk}Ed= literal 0 HcmV?d00001 diff --git a/src/govoplan_audit/backend/api/v1/__pycache__/routes.cpython-312.pyc b/src/govoplan_audit/backend/api/v1/__pycache__/routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a02f93d1032de88520d74b832402fe70612d54fb GIT binary patch literal 13445 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 Wvb;y&v3E*Q&OKTFkHACzqyGUglNDG1 literal 0 HcmV?d00001 diff --git a/src/govoplan_audit/backend/api/v1/__pycache__/schemas.cpython-313.pyc b/src/govoplan_audit/backend/api/v1/__pycache__/schemas.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aed55ce83ba87c5578045db6d32852159d3b0b13 GIT binary patch literal 2315 zcmaJ@U2EHB6nC%-tX)dPRbWJBzOZL>tlJ`n3 z*zCd(SaX?+6?(BBu-)w+>@Tz!iu%IXV2qKwL9&}&?VMMRW0!0KKRV~UU($2VbL69Z zUL|n-yt?)Hrbx)|=$u@c#1M|Y2je+0h#_o}sz8Mj+NsS{HAPe0PH(2GA{DC{n&G~< zDOF`ER~4#Mvou>(sahg)q)d#=JTatQ`9?a~n`%5NLsS`w=6Eyd&>%)dx7nEO#L+Q z>uzJitwk*Jnd^8qE7tL5KD9g-_6TS#dENwpD7=KD1Hk8Gmr%hVG-U`hy$rY&#Kb6;h;@sWs|A;%w83ek6KUL zO~*8uYMRZ!ZhL6wO!Mou;T*y{2%H`f zpNO6ZpUs+Ph&5D$^CRj$YDZ28>o36A5&Q_dTWR`HWn+D3z0z=_N+Wn0v^>lIz(U$9 zA*~HVW)ga2)t)%MU9nnj<>_iAtbGN{h1XggnuGl~;6sEv0MAIQXnS{JeYJP{rFNw+ zuErNjy}SF}zF3ME%e~Em`My|=bH%;Q5qCJ{&En;sHeM~R_3rF{*%#O1iMiL}9KC#U z%vc={L?|5Lx&K$kP+=KfVG>ld2%gM61u7c7q@pD%b1$iAjVg_to@eJyYjMjBqtBhN z1=@5DXW_{1j0d3yr!%lxq4q$d&5)&CALJN;oV`Nccpu;yc{9E6 zgZ*mydT;ADuD_w8;`v1o(-(blF82^McGP-n{q%je_4~x6=ry_bQi4W;}Fkv7K=}b=X_%kW`2nP`Gj1Z|uEiAtl zmy=S*p>4g09l73)+7vo6#g|GtxCGw-*Kbh#3qd6SrnEXXe0AI!l`)1J^2ST?|3}gW zx%_x)_~GN?WXjE?^M67=h85Tl!VQ4G(t;rTLdt)TgC;+>S2_!j%_me-pUzH~JTmv?{g$ literal 0 HcmV?d00001 diff --git a/src/govoplan_audit/backend/api/v1/routes.py b/src/govoplan_audit/backend/api/v1/routes.py new file mode 100644 index 0000000..671d06b --- /dev/null +++ b/src/govoplan_audit/backend/api/v1/routes.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy import false, func, or_ +from sqlalchemy.orm import Session + +from govoplan_access.backend.auth.dependencies import ApiPrincipal, has_scope, require_any_scope +from govoplan_audit.backend.db.models import AuditLog +from govoplan_core.core.access import CAPABILITY_ACCESS_ADMINISTRATION, AccessAdministration +from govoplan_core.core.runtime import get_registry +from govoplan_core.db.session import get_session +from govoplan_tenancy.backend.db.models import Tenant + +from .schemas import AuditAdminItem, AuditAdminListResponse, AuditLogItemResponse, AuditLogListResponse + +router = APIRouter(tags=["audit"]) + + +def _access_administration() -> AccessAdministration: + registry = get_registry() + if registry is None or not registry.has_capability(CAPABILITY_ACCESS_ADMINISTRATION): + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Access administration capability is not configured") + capability = registry.require_capability(CAPABILITY_ACCESS_ADMINISTRATION) + if not isinstance(capability, AccessAdministration): + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Access administration capability is invalid") + return capability + + +def _resolve_tenant( + session: Session, + principal: ApiPrincipal, + tenant_id: str | None, +) -> Tenant: + target_id = tenant_id or principal.tenant_id + if target_id != principal.tenant_id: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Switch to the target tenant before using tenant-administration endpoints.", + ) + tenant = session.get(Tenant, target_id) + if tenant is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found") + return tenant + + +def _parse_audit_filter(value: str | None) -> tuple[str, str]: + if not value: + return "contains", "" + if ":" not in value: + return "contains", value.strip() + operator, raw = value.split(":", 1) + if operator not in {"contains", "eq", "before", "after", "gt", "gte", "lt", "lte"}: + return "contains", value.strip() + return operator, raw.strip() + + +def _audit_text_filter(column, raw: str | None): + operator, value = _parse_audit_filter(raw) + if not value: + return None + normalized = value.casefold() + text = func.lower(func.coalesce(column, "")) + return text == normalized if operator == "eq" else text.contains(normalized) + + +def _text_matches(candidate: str, *, operator: str, value: str) -> bool: + normalized = value.casefold() + text = candidate.casefold() + return text == normalized if operator == "eq" else normalized in text + + +def _audit_actor_filter(access_admin: AccessAdministration, session: Session, raw: str | None): + operator, value = _parse_audit_filter(raw) + if not value: + return None + user_ids = access_admin.user_ids_for_actor_filter(session, operator=operator, value=value) + conditions = [] + if user_ids: + conditions.append(AuditLog.user_id.in_(user_ids)) + if _text_matches("System", operator=operator, value=value): + conditions.append(AuditLog.user_id.is_(None)) + return or_(*conditions) if conditions else false() + + +def _audit_time_filter(raw: str | None): + operator, value = _parse_audit_filter(raw) + if not value: + return None + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as exc: + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid audit date filter.") from exc + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=timezone.utc) + else: + parsed = parsed.astimezone(timezone.utc) + if operator == "eq": + if "T" not in value and " " not in value: + return (AuditLog.created_at >= parsed) & (AuditLog.created_at < parsed + timedelta(days=1)) + return AuditLog.created_at == parsed + if operator in {"before", "lt"}: + return AuditLog.created_at < parsed + if operator == "lte": + return AuditLog.created_at <= parsed + if operator in {"after", "gt"}: + return AuditLog.created_at > parsed + if operator == "gte": + return AuditLog.created_at >= parsed + return AuditLog.created_at == parsed + + +@router.get("/admin/audit", response_model=AuditAdminListResponse) +def list_admin_audit( + tenant_id: str | None = Query(default=None), + all_tenants: bool = Query(default=False), + audit_scope: str | None = Query(default=None, alias="scope"), + limit: int = Query(default=100, ge=1, le=500), + offset: int = Query(default=0, ge=0), + page: int | None = Query(default=None, ge=1), + page_size: int | None = Query(default=None, ge=1, le=500), + sort_by: str = Query(default="time"), + sort_direction: str = Query(default="desc"), + filter_time: str | None = Query(default=None), + filter_actor: str | None = Query(default=None), + filter_action: str | None = Query(default=None), + filter_object: str | None = Query(default=None), + filter_tenant: str | None = Query(default=None), + session: Session = Depends(get_session), + principal: ApiPrincipal = Depends(require_any_scope("audit:read", "system:audit:read")), +): + effective_scope = audit_scope or ("all" if all_tenants else "tenant") + if effective_scope not in {"tenant", "system", "all"}: + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Audit scope must be tenant, system or all.") + if sort_by not in {"time", "actor", "action", "object", "tenant"}: + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Unsupported audit sort column.") + if sort_direction not in {"asc", "desc"}: + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Audit sort direction must be asc or desc.") + + query = session.query(AuditLog) + if effective_scope != "all": + query = query.filter(AuditLog.scope == effective_scope) + if effective_scope == "system": + if not has_scope(principal, "system:audit:read"): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Missing scope: system:audit:read") + elif effective_scope == "all" or all_tenants: + if not has_scope(principal, "system:audit:read"): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Missing scope: system:audit:read") + else: + if not has_scope(principal, "audit:read"): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Missing scope: audit:read") + tenant = _resolve_tenant(session, principal, tenant_id) + query = query.filter(AuditLog.tenant_id == tenant.id) + + object_text = func.coalesce(AuditLog.object_type, "") + " " + func.coalesce(AuditLog.object_id, "") + access_admin = _access_administration() + filters = [ + _audit_time_filter(filter_time), + _audit_actor_filter(access_admin, session, filter_actor), + _audit_text_filter(AuditLog.action, filter_action), + _audit_text_filter(object_text, filter_object), + _audit_text_filter(AuditLog.tenant_id, filter_tenant), + ] + for condition in filters: + if condition is not None: + query = query.filter(condition) + + total = query.count() + effective_page_size = page_size or limit + pages = max(1, (total + effective_page_size - 1) // effective_page_size) + if page is not None or page_size is not None: + effective_page = min(page or 1, pages) + effective_offset = (effective_page - 1) * effective_page_size + else: + effective_page = offset // effective_page_size + 1 + effective_offset = offset + + sort_columns = { + "time": AuditLog.created_at, + "actor": func.coalesce(AuditLog.user_id, "System"), + "action": AuditLog.action, + "object": object_text, + "tenant": func.coalesce(AuditLog.tenant_id, ""), + } + sort_column = sort_columns[sort_by] + order = sort_column.asc() if sort_direction == "asc" else sort_column.desc() + rows = query.order_by(order, AuditLog.id.desc()).offset(effective_offset).limit(effective_page_size).all() + actor_email_by_user_id = access_admin.actor_email_by_user_id(session, {row.user_id for row in rows if row.user_id}) + + return AuditAdminListResponse( + total=total, + page=effective_page, + page_size=effective_page_size, + pages=pages, + items=[ + AuditAdminItem( + id=row.id, + scope=row.scope, + tenant_id=row.tenant_id, + actor_email=actor_email_by_user_id.get(row.user_id) if row.user_id else None, + action=row.action, + object_type=row.object_type, + object_id=row.object_id, + details=row.details or {}, + created_at=row.created_at, + ) + for row in rows + ], + ) + + +@router.get("/audit", response_model=AuditLogListResponse) +def list_audit_log( + limit: int = Query(default=100, ge=1, le=500), + offset: int = Query(default=0, ge=0), + action: str | None = None, + object_type: str | None = None, + object_id: str | None = None, + session: Session = Depends(get_session), + principal: ApiPrincipal = Depends(require_any_scope("audit:read")), +): + if not has_scope(principal, "audit:read"): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Missing scope: audit:read") + query = session.query(AuditLog).filter(AuditLog.tenant_id == principal.tenant_id) + if action: + query = query.filter(AuditLog.action == action) + if object_type: + query = query.filter(AuditLog.object_type == object_type) + if object_id: + query = query.filter(AuditLog.object_id == object_id) + items = query.order_by(AuditLog.created_at.desc()).offset(offset).limit(limit).all() + return AuditLogListResponse(items=[AuditLogItemResponse.model_validate(item) for item in items]) diff --git a/src/govoplan_audit/backend/api/v1/schemas.py b/src/govoplan_audit/backend/api/v1/schemas.py new file mode 100644 index 0000000..2e5f6f6 --- /dev/null +++ b/src/govoplan_audit/backend/api/v1/schemas.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator + + +class AuditAdminItem(BaseModel): + id: str + scope: Literal["tenant", "system"] = "tenant" + tenant_id: str | None = None + actor_email: str | None = None + action: str + object_type: str | None = None + object_id: str | None = None + details: dict[str, Any] = Field(default_factory=dict) + created_at: datetime + + +class AuditAdminListResponse(BaseModel): + items: list[AuditAdminItem] + total: int + page: int = 1 + page_size: int = 100 + pages: int = 1 + + +class AuditLogItemResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + tenant_id: str | None = None + user_id: str | None = None + api_key_id: str | None = None + action: str + object_type: str | None = None + object_id: str | None = None + details: dict[str, Any] | None = None + created_at: datetime + + +class AuditLogListResponse(BaseModel): + items: list[AuditLogItemResponse] diff --git a/src/govoplan_audit/backend/db/__init__.py b/src/govoplan_audit/backend/db/__init__.py new file mode 100644 index 0000000..af0bc55 --- /dev/null +++ b/src/govoplan_audit/backend/db/__init__.py @@ -0,0 +1,2 @@ +"""Audit-owned SQLAlchemy metadata and models.""" + diff --git a/src/govoplan_audit/backend/db/__pycache__/__init__.cpython-312.pyc b/src/govoplan_audit/backend/db/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da341371eee4ab5986a90b50ea29f0f886931be3 GIT binary patch literal 225 zcmX@j%ge<81RTshS;j#6F^B^Lj8MjB4j^MXLkdF_LnWgoQ}zQ`1q9!pFt-6a?{Vv zE75mx3~|&?&n(eT&o9d_$Vtr8O#~UMUtE+77m9}nB_$?jr{<;TrzGje$7kkcmc+;F l6;%G>umM_9nv-f*#0E4L}zQ`1q9!pFt+xa?{Vv zE75mx3~|&?&n(eT&o9d_$Vtr8O#~UMUtE+77m9}nB_$?jr{<;TrzGje$7kkcmc+;F l6;$5humM_9nv-f*#0E4L7fK6rQ!$_S);8{1FxcJDShe*AwWFP8LQB`rMxKV`@r_S58^P`HYEBX2Td^7XS zyf<%tjm08}e;xkGrRM>JUW3v2f}SF5Kjg||gb@}DWD1HPfbKJVijV*M4ZkTWqU#fl zfEiSRW=IK{k|LR5CG7eGM#PjA*^DYt*A5ynGp@wVgp%O4j6)AarOAh`AS^vZI85Rs zu`YV6E6q5JBa1$zrQl0N-+(hEFj=##jIG&v#>!1`+dpg-QX-3tWhl`XtqD?KfvXlK zD@+`pxjw~$Gd9(&#gvZ)uW8vV!7OIF|7tp8UYHLs=b5DO3D`9U*-iIE$~7MNs_ zWfkV7ZG6%w|K(b_D52lw_>cBa#qWzh+^@);j@U_CxPRYHL)^hXVfz=(`7t6$5inAG zH+Tgoejg$t_F+F3*X#RsJpmkaef1Mq@F@ZA5At6KhZ=jw5|K8-jd2RkEQw$l&T|JF z-ND9i97`L!l_0xl{~mM{H{s@umUW*?msbzRx9-dy!)<)_;6D5T-2Op)+{GWKFM^3!gx zB`a?j+PpznK!;mhjCg?D0KYt;%fmC5het+v8;~+QY7m>GeDnw(NMZ3}%kCOI_j{%? z7MUwb`5d7zyeQ3#&h|{*n4DxG@TuG-Ygrv=y3rv8qggUKgjYiFL0mq)gaSKDVwmsaJ9I8*EBdvb4W{%NuN z`4(|*s@0C$rP0+374dd0`RV3~^4HGnT=mQ?M>m~(p_(j~t~!as6|q?B9o&qR&pXpf z_4G|=VacKQs=dplD^9GtA}-gCp4mKKHk~hTSI=sWk#$z?SC2j@jXO=f74bps(6ML1 zwcNT~CR;<*L(`?P)rpEYUF$o$IaWUVV&bJ;?VBu3IL${Y;v~@ME4RKl`Es_J9Ir1w zUh6+!9@(^2C#vFg@1Wc>f+WDhLy4WI7L9}13MpR5a|cf`g*HkPT=2p#WcfQq zp^36c?yjL3>2HWx@G=G2_7VcwW%1qkK!%z$3}HwV!Z literal 0 HcmV?d00001 diff --git a/src/govoplan_audit/backend/db/__pycache__/models.cpython-313.pyc b/src/govoplan_audit/backend/db/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..33aba81c8902d0a8fc49acf636ffdf1c32a38478 GIT binary patch literal 2151 zcmaJ?-ER{|5Wlm}_Sxr;d_z)Ff^h-{5>rTM168F!k_tP7sFzl1<8Ykqi+kaGHhX6h zTOheo)uOgidEpO0yoC0p5ifq^F^^|is_IoyRq;^ejVk;Bow>6EDQZ``xt*Ec&dl!2 z&hCXm0S3mPdtv=255v4brZc?Ggx&iFCJz~r5!n=zWC=Thb63hmT<~|N+)0jbwv0=8 zl3wCX`iL*d6F%uDep~KI1(E_0l0g!*d2cF|43ls&LL$HmqHl;<=SbIKCd!EX5F`52 z;dEq*bNrKT(JuxPF4B{C#e=&jjI&gbb-ifHrdrgEMc~~tdL_=$zNZS5xpG;}Q4i9| zC(Ir~>1?8)n~B+(<(WiYH52*bonlFo^>MkJ1J)qfeUapdGII8oqURF1OrlWCDVi}+ zs!(26HYBu5`oL%qKBE^%jrm;&|6u-D{jU1`-KKEF;*O9WkRRBq4+JpSy{{qWhfJCw ztjIvdS>hgr>=Q2Sfo!=&PV`K{nc%P`UeRaE+UGFoBHpww%A|SBZD;?GVaVyUn*>B5 zEr`JrxVnQ1?W4kC1ajXs&5@vu zhYw%||ApzYF+=+pjQpMbm@p#r)@?_0Lsn1r^D)|QwYdOMz&Z|q%2WnQ^`rPNjo(%&Oqrsud{cRq6sb zRvh&@%je^6GJ-s>mo-h!XbSbHcsZ*92VhLV4e(u=5wFb5&cSWwi=fpMQ;E9>`X!@4 zsy(lLjNBbrEb7W?l`k6#!R1w6oLe4Q{B&V~`cNi;NRUe^hH0A*v>8zB<}etqoW=GL zXKm|LC}!4`tSOn5l0yA$M$Lg5Bd89yo#)i7iMu>A0v%_y69*oIDXW@6x&gcxKSX&+ z(&d68Ni-lya3#wc&V!P4yDV#MO+=E`RAQK#sw;XC)FDYS@pv3P!46ErZJ8vKydg;h z8#dvQ;1QE!hyg=y^g{p*p$Zy0Lxz!@L=s1G8p#NrJ9ts#&NPXqB0= zsh~I9j@=vxb`CPoAy|M$5L&~x%x`^1YuEndLsocvYoi`)@^dX@CV#p0?ERgECja?M zk9)vdTl_n~9Ev@f-#Yg=S)biG*%%OOm+uQrPHYX1J-V}%d0ef3u%lQjQe*IDZSMZ1 zCU>(HdvE(xJ#8(oHqPC!)Phy6G-B1-RV#9=$yHm?#CD*5(OUYVadyR8TerxaMs%b0 zu@ySn9DXNvkW`&z4z(qXUmOPvh6 str: + return str(uuid.uuid4()) + + +class AuditLog(Base, TimestampMixin): + __tablename__ = "audit_log" + __table_args__ = ( + Index("ix_audit_log_scope_created_at", "scope", "created_at"), + Index("ix_audit_log_tenant_scope_created_at", "tenant_id", "scope", "created_at"), + ) + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) + scope: Mapped[str] = mapped_column(String(20), default="tenant", nullable=False, index=True) + tenant_id: Mapped[str | None] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True) + user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True) + api_key_id: Mapped[str | None] = mapped_column(ForeignKey("api_keys.id", ondelete="SET NULL"), nullable=True, index=True) + action: Mapped[str] = mapped_column(String(100), nullable=False, index=True) + object_type: Mapped[str | None] = mapped_column(String(100), index=True) + object_id: Mapped[str | None] = mapped_column(String(100), index=True) + details: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) + + +__all__ = ["AuditLog", "new_uuid"] diff --git a/src/govoplan_audit/backend/manifest.py b/src/govoplan_audit/backend/manifest.py new file mode 100644 index 0000000..04957d3 --- /dev/null +++ b/src/govoplan_audit/backend/manifest.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from govoplan_audit.backend.db import models as audit_models # noqa: F401 - populate Audit ORM metadata +from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard +from govoplan_core.core.modules import MigrationSpec, ModuleContext, ModuleManifest +from govoplan_core.db.base import Base + + +def _route_factory(context: ModuleContext): + del context + from govoplan_audit.backend.api.v1.routes import router + + return router + + +manifest = ModuleManifest( + id="audit", + name="Audit", + version="0.1.5", + dependencies=("access",), + route_factory=_route_factory, + migration_spec=MigrationSpec( + module_id="audit", + metadata=Base.metadata, + retirement_supported=True, + retirement_provider=drop_table_retirement_provider(audit_models.AuditLog, label="Audit"), + retirement_notes="Destructive retirement drops audit-owned database tables after the installer captures a database snapshot.", + ), + uninstall_guard_providers=( + persistent_table_uninstall_guard(audit_models.AuditLog, label="Audit"), + ), +) + + +def get_manifest() -> ModuleManifest: + return manifest diff --git a/src/govoplan_audit/py.typed b/src/govoplan_audit/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/govoplan_audit/py.typed @@ -0,0 +1 @@ +