feat(records): enforce purpose-bound restricted access
Module Package Release / publish-packages (push) Successful in 13s

This commit is contained in:
2026-08-24 19:07:04 +02:00
parent ecfcdd1fea
commit a2dd280c47
24 changed files with 2058 additions and 123 deletions
+77 -1
View File
@@ -14,7 +14,11 @@ from govoplan_core.core.search import (
SearchBackfillRequest,
SearchResourceReference,
)
from govoplan_records.backend.db.models import RecordIdentity, RecordRevision
from govoplan_records.backend.db.models import (
RecordAccessGrantRevision,
RecordIdentity,
RecordRevision,
)
from govoplan_records.backend.search_source import (
ADMIN_SCOPE,
PROVIDER_ID,
@@ -32,6 +36,7 @@ class RecordsSearchSourceTests(unittest.TestCase):
self.engine = create_engine("sqlite+pysqlite:///:memory:")
RecordIdentity.__table__.create(self.engine)
RecordRevision.__table__.create(self.engine)
RecordAccessGrantRevision.__table__.create(self.engine)
self.session = Session(self.engine)
identity = RecordIdentity(
id="identity-1",
@@ -160,6 +165,77 @@ class RecordsSearchSourceTests(unittest.TestCase):
)[other_tenant_reference.key]
)
def test_restricted_search_requires_an_explicit_search_purpose_grant(self) -> None:
self.session.add_all(
(
RecordIdentity(
id="identity-restricted",
tenant_id="tenant-1",
record_id="record-restricted",
record_number="EA-RESTRICTED",
),
RecordRevision(
tenant_id="tenant-1",
record_id="record-restricted",
identity_id="identity-restricted",
revision=1,
class_id="class-1",
file_plan_node_id="plan-1",
title="Restricted decision",
state="open",
source_authority_mode="native_authoritative",
access_mode="restricted",
purpose="case-work",
institutional_context={},
external_reference={},
search_text="restricted decision",
valid_from=NOW,
recorded_at=NOW,
snapshot={},
),
)
)
self.session.commit()
reference = SearchResourceReference(
tenant_id="tenant-1",
module_id="records",
resource_type=RESOURCE_TYPE,
resource_id="record-restricted",
)
request = SearchAuthorizationRequest(reference=reference, source_revision="1")
self.assertFalse(
self.source.authorize(
self.session, _principal({READ_SCOPE}), requests=(request,)
)[reference.key]
)
self.session.add(
RecordAccessGrantRevision(
tenant_id="tenant-1",
grant_id="search-grant-1",
record_id="record-restricted",
revision=1,
status="active",
subject_type="account",
subject_id="account-1",
actions=["read"],
allowed_purposes=["records.search"],
reason="Search is required for assigned work.",
valid_from=NOW,
recorded_at=NOW,
changed_by="account-1",
institutional_context={},
idempotency_key="search-grant-create-1",
request_sha256="a" * 64,
)
)
self.session.commit()
self.assertTrue(
self.source.authorize(
self.session, _principal({READ_SCOPE}), requests=(request,)
)[reference.key]
)
def _principal(scopes: set[str]) -> ApiPrincipal:
return ApiPrincipal(