feat: require explicit dashboard read permission
Module Package Release / publish-packages (push) Successful in 11s
Module Package Release / publish-packages (push) Successful in 11s
This commit is contained in:
@@ -12,6 +12,7 @@ from govoplan_core.auth import ApiPrincipal, get_api_principal
|
||||
from govoplan_core.core.access import PrincipalRef
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_dashboard.backend.db.models import DashboardLayout
|
||||
from govoplan_dashboard.backend.manifest import READ_SCOPE
|
||||
from govoplan_dashboard.backend.router import router
|
||||
|
||||
|
||||
@@ -19,13 +20,14 @@ def principal(
|
||||
*,
|
||||
tenant_id: str = "tenant-1",
|
||||
account_id: str = "account-1",
|
||||
scopes: frozenset[str] = frozenset({READ_SCOPE}),
|
||||
) -> ApiPrincipal:
|
||||
return ApiPrincipal(
|
||||
principal=PrincipalRef(
|
||||
account_id=account_id,
|
||||
membership_id=f"membership:{tenant_id}:{account_id}",
|
||||
tenant_id=tenant_id,
|
||||
scopes=frozenset(),
|
||||
scopes=scopes,
|
||||
group_ids=frozenset(),
|
||||
),
|
||||
account=object(),
|
||||
@@ -67,6 +69,31 @@ class DashboardLayoutApiTests(unittest.TestCase):
|
||||
self.client.close()
|
||||
self.engine.dispose()
|
||||
|
||||
def test_layout_endpoints_require_dashboard_read_permission(self) -> None:
|
||||
self.active_principal = principal(scopes=frozenset())
|
||||
|
||||
for method in ("get", "put", "delete"):
|
||||
response = getattr(self.client, method)(
|
||||
"/api/v1/dashboard/layout",
|
||||
**(
|
||||
{
|
||||
"json": {
|
||||
"expected_revision": 0,
|
||||
"layout_version": 1,
|
||||
"placements": [],
|
||||
"known_widget_ids": [],
|
||||
}
|
||||
}
|
||||
if method == "put"
|
||||
else {}
|
||||
),
|
||||
)
|
||||
self.assertEqual(403, response.status_code)
|
||||
self.assertEqual(
|
||||
f"Missing required scope: {READ_SCOPE}",
|
||||
response.json()["detail"],
|
||||
)
|
||||
|
||||
def test_layouts_are_isolated_by_account_and_view(self) -> None:
|
||||
initial = self.client.get("/api/v1/dashboard/layout")
|
||||
self.assertEqual(200, initial.status_code)
|
||||
|
||||
@@ -7,13 +7,28 @@ from govoplan_core.core.modules import (
|
||||
documentation_structured_translation_issues,
|
||||
localizable_documentation_metadata_keys,
|
||||
)
|
||||
from govoplan_dashboard.backend.manifest import get_manifest
|
||||
from govoplan_dashboard.backend.manifest import READ_SCOPE, get_manifest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class DashboardInterfaceDocumentationContractTests(unittest.TestCase):
|
||||
def test_dashboard_surface_requires_explicit_read_permission(self) -> None:
|
||||
manifest = get_manifest()
|
||||
self.assertEqual({READ_SCOPE}, {item.scope for item in manifest.permissions})
|
||||
self.assertIn(
|
||||
READ_SCOPE,
|
||||
next(
|
||||
item.permissions
|
||||
for item in manifest.role_templates
|
||||
if item.slug == "dashboard_user"
|
||||
),
|
||||
)
|
||||
self.assertEqual((READ_SCOPE,), manifest.nav_items[0].required_all)
|
||||
self.assertEqual((READ_SCOPE,), manifest.frontend.routes[0].required_all) # type: ignore[union-attr]
|
||||
self.assertEqual((READ_SCOPE,), manifest.frontend.nav_items[0].required_all) # type: ignore[union-attr]
|
||||
|
||||
def test_surface_hierarchy_remains_declared(self) -> None:
|
||||
frontend = get_manifest().frontend
|
||||
self.assertIsNotNone(frontend)
|
||||
@@ -39,6 +54,8 @@ class DashboardInterfaceDocumentationContractTests(unittest.TestCase):
|
||||
reference = topics["dashboard.reference.layout-and-widgets"]
|
||||
|
||||
self.assertIn("dashboard.state.browser-fallback", home.metadata["help_contexts"])
|
||||
self.assertEqual("workflow", home.metadata["kind"])
|
||||
self.assertEqual((READ_SCOPE,), home.conditions[0].required_scopes)
|
||||
self.assertIn("dashboard.field.widget-size", reference.metadata["help_contexts"])
|
||||
self.assertIn("save_layout", reference.metadata["consequence_classes"])
|
||||
self.assertIn("remove_widget", reference.metadata["consequence_classes"])
|
||||
|
||||
Reference in New Issue
Block a user