feat: harden file sharing and integrity

This commit is contained in:
2026-07-30 14:26:47 +02:00
parent 85606d5580
commit 835eacfc5d
28 changed files with 2714 additions and 102 deletions

View File

@@ -0,0 +1,219 @@
from __future__ import annotations
import unittest
from datetime import timedelta
from unittest.mock import patch
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from govoplan_access.backend.db.models import Account, Group, User
from govoplan_core.core.change_sequence import ChangeSequenceEntry
from govoplan_core.db.base import Base
from govoplan_files.backend.db.models import FileAsset, FileShare
from govoplan_files.backend.storage.common import FileStorageError, utcnow
from govoplan_files.backend.storage.files import (
get_asset_for_user,
list_file_shares,
revoke_file_share,
share_file,
)
TENANT_ID = "tenant-1"
OWNER_ID = "owner-1"
RECIPIENT_ID = "recipient-1"
GROUP_ID = "group-1"
class FileShareLifecycleTests(unittest.TestCase):
def setUp(self) -> None:
self.engine = create_engine("sqlite:///:memory:", future=True)
Base.metadata.create_all(
bind=self.engine,
tables=[
Account.__table__,
User.__table__,
Group.__table__,
ChangeSequenceEntry.__table__,
FileAsset.__table__,
FileShare.__table__,
],
)
self.session = sessionmaker(bind=self.engine, future=True)()
self.asset = FileAsset(
id="file-1",
tenant_id=TENANT_ID,
owner_type="user",
owner_user_id=OWNER_ID,
display_path="shared.pdf",
filename="shared.pdf",
)
self.session.add(self.asset)
self.session.commit()
def tearDown(self) -> None:
self.session.close()
self.engine.dispose()
@patch(
"govoplan_files.backend.storage.files.user_group_ids",
return_value=[],
)
def test_expired_share_stops_access_immediately(self, _groups) -> None:
self.session.add(
FileShare(
id="expired-share",
tenant_id=TENANT_ID,
file_asset_id=self.asset.id,
target_type="user",
target_id=RECIPIENT_ID,
permission="read",
expires_at=utcnow() - timedelta(seconds=1),
)
)
self.session.commit()
with self.assertRaisesRegex(FileStorageError, "No access"):
get_asset_for_user(
self.session,
tenant_id=TENANT_ID,
user_id=RECIPIENT_ID,
asset_id=self.asset.id,
)
self.assertEqual(
[],
list_file_shares(
self.session,
tenant_id=TENANT_ID,
asset_id=self.asset.id,
),
)
@patch(
"govoplan_files.backend.storage.files.user_group_ids",
return_value=[GROUP_ID],
)
def test_independent_active_grant_survives_other_expiry(self, _groups) -> None:
self.session.add_all(
[
FileShare(
id="expired-user-share",
tenant_id=TENANT_ID,
file_asset_id=self.asset.id,
target_type="user",
target_id=RECIPIENT_ID,
permission="read",
expires_at=utcnow() - timedelta(seconds=1),
),
FileShare(
id="active-group-share",
tenant_id=TENANT_ID,
file_asset_id=self.asset.id,
target_type="group",
target_id=GROUP_ID,
permission="read",
expires_at=utcnow() + timedelta(hours=1),
),
]
)
self.session.commit()
result = get_asset_for_user(
self.session,
tenant_id=TENANT_ID,
user_id=RECIPIENT_ID,
asset_id=self.asset.id,
)
self.assertEqual(self.asset.id, result.id)
self.assertEqual(
["active-group-share"],
[
share.id
for share in list_file_shares(
self.session,
tenant_id=TENANT_ID,
asset_id=self.asset.id,
)
],
)
@patch(
"govoplan_files.backend.storage.files.ensure_share_target_exists",
return_value=None,
)
def test_revoke_is_idempotent_and_future_expiry_is_persisted(
self, _target_exists
) -> None:
expiry = utcnow() + timedelta(days=1)
share = share_file(
self.session,
tenant_id=TENANT_ID,
asset=self.asset,
target_type="user",
target_id=RECIPIENT_ID,
permission="read",
user_id=OWNER_ID,
expires_at=expiry,
)
self.session.commit()
revoked, first_changed = revoke_file_share(
self.session,
tenant_id=TENANT_ID,
asset_id=self.asset.id,
share_id=share.id,
user_id=OWNER_ID,
)
self.session.commit()
repeated, second_changed = revoke_file_share(
self.session,
tenant_id=TENANT_ID,
asset_id=self.asset.id,
share_id=share.id,
user_id=OWNER_ID,
)
self.assertTrue(first_changed)
self.assertFalse(second_changed)
self.assertEqual(OWNER_ID, revoked.revoked_by_user_id)
self.assertEqual(revoked.revoked_at, repeated.revoked_at)
self.assertEqual([], list_file_shares(
self.session,
tenant_id=TENANT_ID,
asset_id=self.asset.id,
))
self.assertEqual(
[share.id],
[
item.id
for item in list_file_shares(
self.session,
tenant_id=TENANT_ID,
asset_id=self.asset.id,
include_inactive=True,
)
],
)
@patch(
"govoplan_files.backend.storage.files.ensure_share_target_exists",
return_value=None,
)
def test_past_expiry_is_rejected(self, _target_exists) -> None:
with self.assertRaisesRegex(FileStorageError, "future"):
share_file(
self.session,
tenant_id=TENANT_ID,
asset=self.asset,
target_type="user",
target_id=RECIPIENT_ID,
permission="read",
user_id=OWNER_ID,
expires_at=utcnow() - timedelta(seconds=1),
)
if __name__ == "__main__":
unittest.main()