feat: add personal and group view ownership

This commit is contained in:
2026-07-28 22:13:15 +02:00
parent 34221b633b
commit 3de8d4aa5f
12 changed files with 833 additions and 213 deletions

View File

@@ -0,0 +1,64 @@
from __future__ import annotations
import unittest
from fastapi import HTTPException
from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.access import PrincipalRef
from govoplan_views.backend.router import (
_require_definition_read,
_require_definition_write,
)
def principal(
*,
account_id: str = "account-1",
scopes: tuple[str, ...],
group_ids: tuple[str, ...] = (),
) -> ApiPrincipal:
return ApiPrincipal(
principal=PrincipalRef(
account_id=account_id,
membership_id="membership-1",
tenant_id="tenant-1",
scopes=frozenset(scopes),
group_ids=frozenset(group_ids),
),
account=object(),
user=object(),
)
class ViewDefinitionAuthorizationTests(unittest.TestCase):
def test_personal_writer_cannot_edit_another_accounts_view(self) -> None:
actor = principal(scopes=("views:personal_definition:write",))
_require_definition_write(actor, "user", "account-1")
with self.assertRaises(HTTPException) as caught:
_require_definition_write(actor, "user", "account-2")
self.assertEqual(403, caught.exception.status_code)
def test_group_reader_is_limited_to_current_memberships(self) -> None:
actor = principal(
scopes=("views:group_definition:read",),
group_ids=("group-1",),
)
_require_definition_read(actor, "group", "group-1")
with self.assertRaises(HTTPException) as caught:
_require_definition_read(actor, "group", "group-2")
self.assertEqual(403, caught.exception.status_code)
def test_tenant_manager_can_manage_owned_views(self) -> None:
actor = principal(scopes=("views:definition:write",))
_require_definition_write(actor, "group", "group-2")
_require_definition_write(actor, "user", "account-2")
if __name__ == "__main__":
unittest.main()

View File

@@ -15,6 +15,7 @@ from govoplan_views.backend.service import (
create_definition,
create_revision,
get_revision,
list_definitions,
normalize_visible_surface_ids,
publish_revision,
resolve_effective_view,
@@ -163,6 +164,56 @@ class ViewsServiceTests(unittest.TestCase):
self.assertIn("views.selector", normalized)
self.assertIn("views.module", normalized)
def test_published_group_view_is_available_to_group_members(self) -> None:
definition = create_definition(
self.session,
tenant_id="tenant-1",
scope_type="group",
scope_id="group-1",
definition_key=None,
name="Group workspace",
description=None,
visible_surface_ids=ordinary_surface_ids(),
catalogue=self.catalogue,
actor_id="account-designer",
)
revision = get_revision(self.session, definition_id=definition.id)
publish_revision(
self.session,
definition,
revision,
catalogue=self.catalogue,
actor_id="account-designer",
)
member = resolve_effective_view(
self.session,
tenant_id="tenant-1",
account_id="account-member",
group_ids=("group-1",),
)
outsider = resolve_effective_view(
self.session,
tenant_id="tenant-1",
account_id="account-outsider",
group_ids=(),
)
self.assertEqual([definition.id], [view.id for view in member.available_views])
self.assertEqual((), outsider.available_views)
self.assertEqual(
[definition.id],
[
item.id
for item in list_definitions(
self.session,
tenant_id="tenant-1",
scope_type="group",
scope_id="group-1",
include_inherited=False,
)
],
)
def test_normalization_requires_navigation_and_route(self) -> None:
with self.assertRaisesRegex(ViewsValidationError, "navigation"):
normalize_visible_surface_ids(
@@ -333,9 +384,7 @@ class ViewsServiceTests(unittest.TestCase):
actor_id="account-admin",
)
catalogue_without_admin_route = tuple(
surface
for surface in self.catalogue
if surface.id != "access.route.admin"
surface for surface in self.catalogue if surface.id != "access.route.admin"
)
state = resolve_effective_view(