Enforce tenant policy on public scheduling links

This commit is contained in:
2026-08-04 09:29:36 +02:00
parent 22555f2603
commit 9fd2ccd0a5
3 changed files with 77 additions and 4 deletions
+49 -3
View File
@@ -25,8 +25,12 @@ from govoplan_core.core.people import (
CAPABILITY_ACCESS_PEOPLE_SEARCH,
CAPABILITY_ADDRESSES_PEOPLE_SEARCH,
)
from govoplan_core.core.poll import CAPABILITY_POLL_SCHEDULING
from govoplan_core.core.poll_participation import CAPABILITY_POLL_PARTICIPATION_GATEWAY
from govoplan_core.core.poll import CAPABILITY_POLL_SCHEDULING, PollCapabilityError
from govoplan_core.core.poll_participation import (
CAPABILITY_POLL_PARTICIPATION_GATEWAY,
PollResponseGatewayRef,
poll_participation_gateway_provider,
)
from govoplan_core.core.policy import CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base
@@ -104,7 +108,7 @@ DOCUMENTATION = (
body=(
"Scheduling records participant requirements, quorum and weighting constraints, response deadlines, reminders, and yes/no/maybe availability through Poll. "
"Calendar-aware organizers can inspect conflicts and create tentative holds before deciding. After a decision, Scheduling releases unused holds, creates or links the final event, and records notification handoff state. "
"Signed external links expose only the bounded request information allowed by the request's participation and privacy policy."
"Signed external links expose only the bounded request information allowed by the request's participation and privacy policy. Their governed Poll invitation resolves the tenant before Scheduling runs, so tenant module policy can withdraw public participation without weakening token validation."
),
layer="configured",
documentation_types=("user",),
@@ -212,6 +216,47 @@ def _scheduling_router(context: ModuleContext):
return router
def _public_tenant_resolver(request: object, session: object) -> str | None:
path_params = getattr(request, "path_params", {})
request_id = str(path_params.get("request_id") or "").strip()
token = str(path_params.get("token") or "").strip()
path = str(getattr(getattr(request, "url", None), "path", ""))
if not request_id or not token or "/scheduling/public/" not in path:
return None
from govoplan_scheduling.backend.db.models import SchedulingRequest
app = getattr(request, "app", None)
registry = getattr(getattr(app, "state", None), "govoplan_registry", None)
provider = poll_participation_gateway_provider(registry)
if provider is None:
return None
gateway = PollResponseGatewayRef(
module_id=MODULE_ID,
resource_type="scheduling_request",
resource_id=request_id,
)
try:
invitation = provider.resolve_public_invitation(
session,
token=token,
gateway=gateway,
)
except PollCapabilityError:
return None
scheduling_request = (
session.query(SchedulingRequest)
.filter(
SchedulingRequest.id == request_id,
SchedulingRequest.tenant_id == invitation.tenant_id,
SchedulingRequest.poll_id == invitation.poll_id,
SchedulingRequest.deleted_at.is_(None),
)
.one_or_none()
)
return scheduling_request.tenant_id if scheduling_request is not None else None
manifest = ModuleManifest(
id=MODULE_ID,
name=MODULE_NAME,
@@ -279,6 +324,7 @@ manifest = ModuleManifest(
),
),
route_factory=_scheduling_router,
public_tenant_resolver=_public_tenant_resolver,
tenant_summary_providers=(_tenant_summary,),
migration_spec=MigrationSpec(
module_id=MODULE_ID,