feat(policy): add scheduling participant privacy contract
This commit is contained in:
@@ -5,8 +5,10 @@ from typing import Any, Iterable, Literal, Mapping, Protocol, cast, runtime_chec
|
||||
from urllib.parse import quote, unquote
|
||||
|
||||
PolicyScopeType = Literal["system", "tenant", "user", "group", "campaign"]
|
||||
SchedulingParticipantVisibility = Literal["aggregates_only", "names_and_statuses"]
|
||||
|
||||
CAPABILITY_POLICY_PRIVACY_RETENTION = "policy.privacyRetention"
|
||||
CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY = "policy.schedulingParticipantPrivacy"
|
||||
|
||||
POLICY_SCOPE_TYPES: tuple[PolicyScopeType, ...] = ("system", "tenant", "user", "group", "campaign")
|
||||
|
||||
@@ -126,6 +128,55 @@ class PolicyDecision:
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SchedulingParticipantPrivacyRequest:
|
||||
"""Context for resolving what one Scheduling participant may see.
|
||||
|
||||
``requested_visibility`` is the Scheduling-owned configuration result. A
|
||||
policy provider may preserve or reduce it, but must not broaden it. Core
|
||||
deliberately defines no fallback when the optional capability is absent;
|
||||
that secure default remains the responsibility of Scheduling.
|
||||
"""
|
||||
|
||||
tenant_id: str
|
||||
scheduling_request_id: str
|
||||
participant_id: str
|
||||
requested_visibility: SchedulingParticipantVisibility
|
||||
actor_user_id: str | None = None
|
||||
context: Mapping[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SchedulingParticipantPrivacyDecision:
|
||||
"""Policy-resolved visibility of other participants' names and statuses."""
|
||||
|
||||
effective_visibility: SchedulingParticipantVisibility
|
||||
reason: str | None = None
|
||||
source_path: tuple[PolicySourceStep, ...] = ()
|
||||
details: Mapping[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"effective_visibility": self.effective_visibility,
|
||||
"reason": self.reason,
|
||||
"source_path": [step.to_dict() for step in self.source_path],
|
||||
"details": dict(self.details),
|
||||
}
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SchedulingParticipantPrivacyPolicy(Protocol):
|
||||
"""Optional policy hook for Scheduling participant-roster disclosure."""
|
||||
|
||||
def resolve_scheduling_participant_visibility(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
request: SchedulingParticipantPrivacyRequest,
|
||||
) -> SchedulingParticipantPrivacyDecision:
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class PrivacyRetentionService(Protocol):
|
||||
def privacy_policy_from_settings(self, *args: Any, **kwargs: Any) -> Any:
|
||||
@@ -160,3 +211,16 @@ class PrivacyRetentionService(Protocol):
|
||||
|
||||
def apply_retention_policy(self, *args: Any, **kwargs: Any) -> Any:
|
||||
...
|
||||
|
||||
|
||||
def scheduling_participant_privacy_policy(
|
||||
registry: object | None,
|
||||
) -> SchedulingParticipantPrivacyPolicy | None:
|
||||
"""Return the optional provider without selecting a visibility fallback."""
|
||||
|
||||
if registry is None or not hasattr(registry, "has_capability"):
|
||||
return None
|
||||
if not registry.has_capability(CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY):
|
||||
return None
|
||||
capability = registry.capability(CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY)
|
||||
return capability if isinstance(capability, SchedulingParticipantPrivacyPolicy) else None
|
||||
|
||||
Reference in New Issue
Block a user