Add file resource access explanations

This commit is contained in:
2026-07-11 00:39:39 +02:00
parent ba7fb2def7
commit 58c0441763
7 changed files with 625 additions and 5 deletions

View File

@@ -0,0 +1,101 @@
from __future__ import annotations
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from govoplan_access.backend.db.models import Account, Group, User
from govoplan_core.core.access import PrincipalRef
from govoplan_core.db.base import Base
from govoplan_files.backend.capabilities import FilesAccessService, virtual_folder_resource_id
from govoplan_files.backend.db.models import FileAsset, FileFolder, FileShare
TENANT_ID = "tenant-1"
USER_ID = "user-1"
OTHER_USER_ID = "user-2"
GROUP_ID = "group-1"
class FilesAccessProviderTests(unittest.TestCase):
def test_file_access_provider_explains_owner_share_admin_and_missing_resources(self) -> None:
session = _session()
_seed_access_subjects(session)
owned = FileAsset(id="file-owned", tenant_id=TENANT_ID, owner_type="user", owner_user_id=USER_ID, display_path="owned.pdf", filename="owned.pdf")
shared = FileAsset(id="file-shared", tenant_id=TENANT_ID, owner_type="user", owner_user_id=OTHER_USER_ID, display_path="shared.pdf", filename="shared.pdf")
session.add_all([
owned,
shared,
FileShare(id="share-group", tenant_id=TENANT_ID, file_asset_id=shared.id, target_type="group", target_id=GROUP_ID, permission="read"),
])
session.commit()
service = FilesAccessService()
owned_items = service.explain_resource_provenance(session, _principal(), resource_type="file", resource_id=owned.id, action="files:file:read")
shared_items = service.explain_resource_provenance(session, _principal(group_ids={GROUP_ID}), resource_type="file", resource_id=shared.id, action="files:file:read")
admin_items = service.explain_resource_provenance(session, _principal(scopes={"files:file:admin"}), resource_type="file", resource_id=shared.id, action="files:file:read")
missing_items = service.explain_resource_provenance(session, _principal(), resource_type="file", resource_id="missing-file", action="files:file:read")
self.assertTrue(any(item.kind == "resource" and item.source == "files.file" and item.id == owned.id for item in owned_items))
self.assertTrue(any(item.kind == "owner" and item.id == USER_ID for item in owned_items))
self.assertTrue(any(item.kind == "share" and item.id == "share-group" for item in shared_items))
self.assertTrue(any(item.kind == "policy" and item.id == "files:file:admin" for item in admin_items))
self.assertEqual("files.not_found", missing_items[0].source)
self.assertIs(missing_items[0].details["found"], False)
def test_file_access_provider_explains_persisted_and_virtual_folders(self) -> None:
session = _session()
_seed_access_subjects(session)
persisted = FileFolder(id="folder-persisted", tenant_id=TENANT_ID, owner_type="group", owner_group_id=GROUP_ID, path="records")
virtual_child = FileAsset(
id="file-in-virtual-folder",
tenant_id=TENANT_ID,
owner_type="group",
owner_group_id=GROUP_ID,
display_path="inferred/sub/file.pdf",
filename="file.pdf",
)
session.add_all([persisted, virtual_child])
session.commit()
service = FilesAccessService()
principal = _principal(group_ids={GROUP_ID})
persisted_items = service.explain_resource_provenance(session, principal, resource_type="folder", resource_id=persisted.id, action="files:file:read")
virtual_id = virtual_folder_resource_id(tenant_id=TENANT_ID, owner_type="group", owner_id=GROUP_ID, path="inferred/sub")
virtual_items = service.explain_resource_provenance(session, principal, resource_type="folder", resource_id=virtual_id, action="files:file:read")
missing_virtual_id = virtual_folder_resource_id(tenant_id=TENANT_ID, owner_type="group", owner_id=GROUP_ID, path="inferred/missing")
missing_items = service.explain_resource_provenance(session, principal, resource_type="folder", resource_id=missing_virtual_id, action="files:file:read")
self.assertTrue(any(item.kind == "resource" and item.source == "files.folder" and item.id == persisted.id for item in persisted_items))
self.assertTrue(any(item.kind == "owner" and item.id == GROUP_ID for item in persisted_items))
self.assertTrue(any(item.kind == "resource" and item.source == "files.virtual_folder" and item.id == virtual_id for item in virtual_items))
self.assertTrue(any(item.kind == "owner" and item.id == GROUP_ID for item in virtual_items))
self.assertEqual("files.not_found", missing_items[0].source)
def _session():
engine = create_engine("sqlite:///:memory:", future=True)
Base.metadata.create_all(bind=engine, tables=[Account.__table__, User.__table__, Group.__table__, FileAsset.__table__, FileFolder.__table__, FileShare.__table__])
return sessionmaker(bind=engine, future=True)()
def _seed_access_subjects(session) -> None:
session.add_all([
Account(id="account-1", email="one@example.test", normalized_email="one@example.test"),
Account(id="account-2", email="two@example.test", normalized_email="two@example.test"),
User(id=USER_ID, tenant_id=TENANT_ID, account_id="account-1", email="one@example.test"),
User(id=OTHER_USER_ID, tenant_id=TENANT_ID, account_id="account-2", email="two@example.test"),
Group(id=GROUP_ID, tenant_id=TENANT_ID, slug="group", name="Group"),
])
session.commit()
def _principal(*, scopes: set[str] | None = None, group_ids: set[str] | None = None) -> PrincipalRef:
return PrincipalRef(
account_id="account-1",
membership_id=USER_ID,
tenant_id=TENANT_ID,
scopes=frozenset(scopes or {"files:file:read"}),
group_ids=frozenset(group_ids or set()),
)