149 lines
5.9 KiB
Python
149 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from datetime import datetime, timezone
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_core.core.dsar import (
|
|
DsarErasureActionRef,
|
|
DsarExecutionResultRef,
|
|
DsarRecordRef,
|
|
DsarSubjectRef,
|
|
dsar_capability_name,
|
|
)
|
|
from govoplan_helpdesk.backend.db.models import HelpdeskProfileHistory, HelpdeskServiceProfile
|
|
|
|
|
|
HELPDESK_DSAR_CAPABILITY = dsar_capability_name("helpdesk")
|
|
_CONFLICT = object()
|
|
|
|
|
|
class HelpdeskDsarProvider:
|
|
provider_id = "helpdesk"
|
|
module_id = "helpdesk"
|
|
|
|
def search_subject(self, session: object, *, tenant_id: str, subject: DsarSubjectRef) -> Sequence[DsarRecordRef]:
|
|
db = _session(session)
|
|
actor_ids = _actor_ids(subject)
|
|
if actor_ids is None:
|
|
return ()
|
|
rows = (
|
|
db.query(HelpdeskProfileHistory, HelpdeskServiceProfile)
|
|
.join(HelpdeskServiceProfile, HelpdeskProfileHistory.profile_id == HelpdeskServiceProfile.id)
|
|
.filter(
|
|
HelpdeskProfileHistory.tenant_id == tenant_id,
|
|
HelpdeskProfileHistory.actor_id.in_(actor_ids),
|
|
)
|
|
.order_by(HelpdeskProfileHistory.occurred_at.asc())
|
|
.limit(5_001)
|
|
.all()
|
|
)
|
|
if len(rows) > 5_000:
|
|
raise ValueError("Helpdesk DSAR attribution limit exceeded; narrow the selectors.")
|
|
return tuple(
|
|
DsarRecordRef(
|
|
provider_id="helpdesk",
|
|
module_id="helpdesk",
|
|
resource_type="service_profile_actor_attribution",
|
|
resource_id=history.id,
|
|
category="configuration_accountability_evidence",
|
|
title=f"Helpdesk profile lifecycle attribution: {profile.profile_key}",
|
|
data={
|
|
"activity": "configured_helpdesk_service_profile",
|
|
"profile_id": profile.id,
|
|
"profile_key": profile.profile_key,
|
|
"revision": history.revision,
|
|
"occurred_at": _iso(history.occurred_at),
|
|
},
|
|
observed_at=_aware(history.occurred_at),
|
|
immutable_evidence=True,
|
|
retention_reason="Helpdesk service-profile change attribution is immutable configuration accountability evidence.",
|
|
)
|
|
for history, profile in rows
|
|
)
|
|
|
|
def plan_erasure(self, session: object, *, tenant_id: str, subject: DsarSubjectRef, records: Sequence[DsarRecordRef]) -> Sequence[DsarErasureActionRef]:
|
|
del tenant_id
|
|
_session(session)
|
|
if _actor_ids(subject) is None:
|
|
raise ValueError("Helpdesk DSAR subject selectors conflict.")
|
|
return tuple(
|
|
DsarErasureActionRef(
|
|
action_id=f"helpdesk:retain:{record.resource_id}",
|
|
provider_id="helpdesk",
|
|
module_id="helpdesk",
|
|
kind="retain",
|
|
resource_type=record.resource_type,
|
|
resource_id=record.resource_id,
|
|
title=f"Retain {record.title}",
|
|
rationale=record.retention_reason or "Configuration attribution is immutable evidence.",
|
|
executable=False,
|
|
)
|
|
for record in records
|
|
if _valid_record(record)
|
|
)
|
|
|
|
def execute_erasure(self, session: object, *, tenant_id: str, subject: DsarSubjectRef, actions: Sequence[DsarErasureActionRef], request_id: str) -> Sequence[DsarExecutionResultRef]:
|
|
del tenant_id
|
|
_session(session)
|
|
if _actor_ids(subject) is None:
|
|
raise ValueError("Helpdesk DSAR subject selectors conflict.")
|
|
results = []
|
|
for action in actions:
|
|
if action.provider_id != "helpdesk" or action.module_id != "helpdesk" or action.kind != "retain" or action.executable:
|
|
raise ValueError("Helpdesk DSAR publishes retain-only actions.")
|
|
results.append(
|
|
DsarExecutionResultRef(
|
|
action_id=action.action_id,
|
|
status="blocked",
|
|
summary="Helpdesk configuration attribution remains immutable evidence.",
|
|
evidence={"request_id": request_id},
|
|
)
|
|
)
|
|
return tuple(results)
|
|
|
|
|
|
def _actor_ids(subject: DsarSubjectRef) -> tuple[str, ...] | None:
|
|
refs = subject.external_references
|
|
values = (
|
|
_coalesce(subject.account_id, refs.get("helpdesk.account"), refs.get("access.account")),
|
|
_coalesce(subject.identity_id, refs.get("helpdesk.identity"), refs.get("identity.id")),
|
|
_coalesce(subject.membership_id, refs.get("helpdesk.membership"), refs.get("tenancy.membership")),
|
|
)
|
|
if any(value is _CONFLICT for value in values):
|
|
return None
|
|
result = tuple(dict.fromkeys(value for value in values if isinstance(value, str) and value))
|
|
return result or None
|
|
|
|
|
|
def _coalesce(*values: str | None) -> str | None | object:
|
|
normalized = {str(value).strip() for value in values if str(value or "").strip()}
|
|
return _CONFLICT if len(normalized) > 1 else next(iter(normalized), None)
|
|
|
|
|
|
def _valid_record(record: DsarRecordRef) -> bool:
|
|
if record.provider_id != "helpdesk" or record.module_id != "helpdesk" or record.resource_type != "service_profile_actor_attribution":
|
|
raise ValueError("Helpdesk DSAR record identity is invalid.")
|
|
return True
|
|
|
|
|
|
def _aware(value: datetime | None) -> datetime | None:
|
|
if value is not None and value.tzinfo is None:
|
|
return value.replace(tzinfo=timezone.utc)
|
|
return value
|
|
|
|
|
|
def _iso(value: datetime | None) -> str | None:
|
|
result = _aware(value)
|
|
return result.isoformat() if result else None
|
|
|
|
|
|
def _session(value: object) -> Session:
|
|
if not isinstance(value, Session):
|
|
raise TypeError("Helpdesk DSAR requires a SQLAlchemy Session.")
|
|
return value
|
|
|
|
|
|
__all__ = ["HELPDESK_DSAR_CAPABILITY", "HelpdeskDsarProvider"]
|