fix(addresses): require group visibility for book creation

This commit is contained in:
2026-07-20 17:03:58 +02:00
parent f19350e65d
commit 3d52cc86f9
2 changed files with 34 additions and 1 deletions

View File

@@ -175,7 +175,7 @@ def _scope_id_for_create(principal: ApiPrincipal, payload: AddressBookCreateRequ
group_id = _trim(payload.group_id) group_id = _trim(payload.group_id)
if not group_id: if not group_id:
raise AddressBookError("Group address books require a group id.") raise AddressBookError("Group address books require a group id.")
if principal.group_ids and group_id not in principal.group_ids and not principal.has("addresses:address_book:admin"): if group_id not in principal.group_ids and not principal.has("addresses:address_book:admin"):
raise AddressBookError("The selected group is not visible to the current principal.") raise AddressBookError("The selected group is not visible to the current principal.")
return tenant_id, group_id return tenant_id, group_id
raise AddressBookError("Unsupported address book scope.") raise AddressBookError("Unsupported address book scope.")

View File

@@ -49,6 +49,7 @@ from govoplan_addresses.backend.schemas import (
) )
from govoplan_addresses.backend.manifest import manifest from govoplan_addresses.backend.manifest import manifest
from govoplan_addresses.backend.service import ( from govoplan_addresses.backend.service import (
AddressBookError,
address_book_contact_counts, address_book_contact_counts,
address_list_entry_counts, address_list_entry_counts,
create_address_book, create_address_book,
@@ -146,6 +147,15 @@ class ReadOnlyPrincipal(Principal):
} }
class NoGroupPrincipal(Principal):
group_ids = frozenset()
class AddressBookAdminPrincipal(NoGroupPrincipal):
def has(self, scope: str) -> bool:
return scope == "addresses:address_book:admin" or super().has(scope)
class AddressServiceTest(unittest.TestCase): class AddressServiceTest(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
engine = create_engine("sqlite:///:memory:") engine = create_engine("sqlite:///:memory:")
@@ -169,6 +179,29 @@ class AddressServiceTest(unittest.TestCase):
self.session = sessionmaker(bind=engine)() self.session = sessionmaker(bind=engine)()
self.principal = Principal() self.principal = Principal()
def test_group_book_creation_requires_membership_or_admin_scope(self) -> None:
with self.assertRaisesRegex(AddressBookError, "selected group is not visible"):
create_address_book(
self.session,
NoGroupPrincipal(),
AddressBookCreateRequest(scope_type="group", group_id="group-2", name="Unassigned"),
)
with self.assertRaisesRegex(AddressBookError, "selected group is not visible"):
create_address_book(
self.session,
self.principal,
AddressBookCreateRequest(scope_type="group", group_id="group-2", name="Other group"),
)
admin_book = create_address_book(
self.session,
AddressBookAdminPrincipal(),
AddressBookCreateRequest(scope_type="group", group_id="group-2", name="Admin-created"),
)
self.assertEqual(admin_book.scope_id, "group-2")
def test_scoped_book_contact_lookup_and_soft_delete(self) -> None: def test_scoped_book_contact_lookup_and_soft_delete(self) -> None:
book = create_address_book(self.session, self.principal, AddressBookCreateRequest(scope_type="user", name="Personal")) book = create_address_book(self.session, self.principal, AddressBookCreateRequest(scope_type="user", name="Personal"))
self.session.commit() self.session.commit()