300 lines
11 KiB
Python
300 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable, Mapping
|
|
|
|
from sqlalchemy import and_, or_
|
|
|
|
from govoplan_core.core.access import AccessDecisionProvenance, PrincipalRef
|
|
from govoplan_core.core.campaigns import (
|
|
CampaignAccessProvider,
|
|
CampaignDeliveryTaskProvider,
|
|
CampaignMailPolicyContext,
|
|
CampaignMailPolicyContextProvider,
|
|
CampaignPolicyContext,
|
|
CampaignPolicyContextProvider,
|
|
CampaignRetentionProvider,
|
|
)
|
|
from govoplan_core.security.module_permissions import scopes_grant_compatible
|
|
|
|
from govoplan_campaign.backend.db.models import Campaign, CampaignShare
|
|
|
|
|
|
READ_ACTIONS = {"campaigns:campaign:read", "campaign:read"}
|
|
|
|
|
|
class CampaignMailPolicyContextService(CampaignMailPolicyContextProvider):
|
|
def get_campaign_mail_policy_context(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str,
|
|
campaign_id: str,
|
|
) -> CampaignMailPolicyContext | None:
|
|
campaign = session.get(Campaign, campaign_id) # type: ignore[attr-defined]
|
|
if campaign is None or campaign.tenant_id != tenant_id:
|
|
return None
|
|
return CampaignMailPolicyContext(
|
|
id=campaign.id,
|
|
tenant_id=campaign.tenant_id,
|
|
owner_user_id=campaign.owner_user_id,
|
|
owner_group_id=campaign.owner_group_id,
|
|
mail_profile_policy=dict(campaign.mail_profile_policy or {}),
|
|
)
|
|
|
|
def set_campaign_mail_profile_policy(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str,
|
|
campaign_id: str,
|
|
policy: Mapping[str, object],
|
|
) -> Mapping[str, object]:
|
|
campaign = session.get(Campaign, campaign_id) # type: ignore[attr-defined]
|
|
if campaign is None or campaign.tenant_id != tenant_id:
|
|
raise ValueError("Campaign policy not found")
|
|
payload = dict(policy)
|
|
campaign.mail_profile_policy = payload
|
|
session.add(campaign) # type: ignore[attr-defined]
|
|
return payload
|
|
|
|
|
|
def mail_policy_context_capability(context: object) -> CampaignMailPolicyContextService:
|
|
return CampaignMailPolicyContextService()
|
|
|
|
|
|
class CampaignAccessService(CampaignAccessProvider):
|
|
def campaign_exists(self, session: object, *, tenant_id: str, campaign_id: str) -> bool:
|
|
campaign = session.get(Campaign, campaign_id) # type: ignore[attr-defined]
|
|
return bool(campaign and campaign.tenant_id == tenant_id)
|
|
|
|
def can_read_campaign(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str,
|
|
campaign_id: str,
|
|
user_id: str,
|
|
group_ids: Iterable[str] = (),
|
|
tenant_admin: bool = False,
|
|
) -> bool:
|
|
campaign = session.get(Campaign, campaign_id) # type: ignore[attr-defined]
|
|
if campaign is None or campaign.tenant_id != tenant_id:
|
|
return False
|
|
if tenant_admin or campaign.owner_user_id == user_id:
|
|
return True
|
|
group_id_set = {str(group_id) for group_id in group_ids}
|
|
if campaign.owner_group_id and campaign.owner_group_id in group_id_set:
|
|
return True
|
|
share_clauses = [
|
|
and_(
|
|
CampaignShare.target_type == "user",
|
|
CampaignShare.target_id == user_id,
|
|
)
|
|
]
|
|
if group_id_set:
|
|
share_clauses.append(
|
|
and_(
|
|
CampaignShare.target_type == "group",
|
|
CampaignShare.target_id.in_(group_id_set),
|
|
)
|
|
)
|
|
share = (
|
|
session.query(CampaignShare) # type: ignore[attr-defined]
|
|
.filter(
|
|
CampaignShare.tenant_id == tenant_id,
|
|
CampaignShare.campaign_id == campaign.id,
|
|
CampaignShare.revoked_at.is_(None),
|
|
or_(*share_clauses),
|
|
)
|
|
.first()
|
|
)
|
|
return share is not None
|
|
|
|
def explain_resource_provenance(
|
|
self,
|
|
session: object,
|
|
principal: PrincipalRef,
|
|
*,
|
|
resource_type: str,
|
|
resource_id: str,
|
|
action: str,
|
|
) -> tuple[AccessDecisionProvenance, ...]:
|
|
normalized_type = resource_type.lower().strip()
|
|
if normalized_type not in {"campaign", "campaign_object", "campaigns:campaign"}:
|
|
return ()
|
|
campaign = session.get(Campaign, resource_id) # type: ignore[attr-defined]
|
|
if campaign is None or (principal.tenant_id and campaign.tenant_id != principal.tenant_id):
|
|
return (
|
|
AccessDecisionProvenance(
|
|
kind="resource",
|
|
id=resource_id,
|
|
tenant_id=principal.tenant_id,
|
|
source="campaigns.not_found",
|
|
details={"resource_type": "campaign", "found": False},
|
|
),
|
|
)
|
|
items = [
|
|
AccessDecisionProvenance(
|
|
kind="resource",
|
|
id=campaign.id,
|
|
label=campaign.name,
|
|
tenant_id=campaign.tenant_id,
|
|
source="campaigns.campaign",
|
|
details={
|
|
"resource_type": "campaign",
|
|
"external_id": campaign.external_id,
|
|
"status": campaign.status,
|
|
},
|
|
)
|
|
]
|
|
items.extend(_owner_provenance(campaign, principal))
|
|
items.extend(_tenant_admin_provenance(principal))
|
|
permission_values = {"read", "write"} if action in READ_ACTIONS else {"write"}
|
|
shares = (
|
|
session.query(CampaignShare) # type: ignore[attr-defined]
|
|
.filter(
|
|
CampaignShare.tenant_id == campaign.tenant_id,
|
|
CampaignShare.campaign_id == campaign.id,
|
|
CampaignShare.revoked_at.is_(None),
|
|
CampaignShare.permission.in_(sorted(permission_values)),
|
|
or_(
|
|
(CampaignShare.target_type == "user") & (CampaignShare.target_id == principal.membership_id),
|
|
(CampaignShare.target_type == "group") & (CampaignShare.target_id.in_(sorted(principal.group_ids))),
|
|
),
|
|
)
|
|
.order_by(CampaignShare.target_type.asc(), CampaignShare.target_id.asc())
|
|
.all()
|
|
)
|
|
for share in shares:
|
|
items.append(
|
|
AccessDecisionProvenance(
|
|
kind="share",
|
|
id=share.id,
|
|
label=share.permission,
|
|
tenant_id=share.tenant_id,
|
|
source="campaigns.share",
|
|
details={
|
|
"target_type": share.target_type,
|
|
"target_id": share.target_id,
|
|
"permission": share.permission,
|
|
},
|
|
)
|
|
)
|
|
return tuple(items)
|
|
|
|
|
|
def access_capability(context: object) -> CampaignAccessService:
|
|
return CampaignAccessService()
|
|
|
|
|
|
def _owner_provenance(campaign: Campaign, principal: PrincipalRef) -> tuple[AccessDecisionProvenance, ...]:
|
|
if campaign.owner_user_id and campaign.owner_user_id == principal.membership_id:
|
|
return (
|
|
AccessDecisionProvenance(
|
|
kind="owner",
|
|
id=campaign.owner_user_id,
|
|
tenant_id=campaign.tenant_id,
|
|
source="campaigns.owner",
|
|
details={"owner_type": "user"},
|
|
),
|
|
)
|
|
if campaign.owner_group_id and campaign.owner_group_id in principal.group_ids:
|
|
return (
|
|
AccessDecisionProvenance(
|
|
kind="owner",
|
|
id=campaign.owner_group_id,
|
|
tenant_id=campaign.tenant_id,
|
|
source="campaigns.owner",
|
|
details={"owner_type": "group"},
|
|
),
|
|
)
|
|
return ()
|
|
|
|
|
|
def _tenant_admin_provenance(principal: PrincipalRef) -> tuple[AccessDecisionProvenance, ...]:
|
|
if not scopes_grant_compatible(principal.scopes, "tenant:*"):
|
|
return ()
|
|
return (
|
|
AccessDecisionProvenance(
|
|
kind="policy",
|
|
id="tenant:*",
|
|
label="tenant:*",
|
|
tenant_id=principal.tenant_id,
|
|
source="campaigns.tenant_admin",
|
|
details={"grant": "tenant_admin"},
|
|
),
|
|
)
|
|
|
|
|
|
class CampaignPolicyContextService(CampaignPolicyContextProvider):
|
|
def get_campaign_policy_context(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str | None = None,
|
|
campaign_id: str,
|
|
) -> CampaignPolicyContext | None:
|
|
campaign = session.get(Campaign, campaign_id) # type: ignore[attr-defined]
|
|
if campaign is None or (tenant_id is not None and campaign.tenant_id != tenant_id):
|
|
return None
|
|
return CampaignPolicyContext(
|
|
id=campaign.id,
|
|
tenant_id=campaign.tenant_id,
|
|
owner_user_id=campaign.owner_user_id,
|
|
owner_group_id=campaign.owner_group_id,
|
|
settings=dict(campaign.settings or {}),
|
|
)
|
|
|
|
def set_campaign_settings(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str,
|
|
campaign_id: str,
|
|
settings: Mapping[str, object],
|
|
) -> Mapping[str, object]:
|
|
campaign = session.get(Campaign, campaign_id) # type: ignore[attr-defined]
|
|
if campaign is None or campaign.tenant_id != tenant_id:
|
|
raise ValueError("Campaign privacy policy not found")
|
|
payload = dict(settings)
|
|
campaign.settings = payload
|
|
session.add(campaign) # type: ignore[attr-defined]
|
|
return payload
|
|
|
|
|
|
def policy_context_capability(context: object) -> CampaignPolicyContextService:
|
|
return CampaignPolicyContextService()
|
|
|
|
|
|
class CampaignDeliveryTaskService(CampaignDeliveryTaskProvider):
|
|
def send_campaign_job(self, session: object, *, job_id: str, enqueue_imap_task: bool = True) -> Mapping[str, object]:
|
|
from govoplan_campaign.backend.sending.jobs import send_campaign_job
|
|
|
|
return send_campaign_job(session, job_id=job_id, enqueue_imap_task=enqueue_imap_task).as_dict() # type: ignore[arg-type]
|
|
|
|
def append_sent_for_job(self, session: object, *, job_id: str) -> Mapping[str, object]:
|
|
from govoplan_campaign.backend.sending.jobs import append_sent_for_job
|
|
|
|
return append_sent_for_job(session, job_id=job_id).as_dict() # type: ignore[arg-type]
|
|
|
|
|
|
def delivery_tasks_capability(context: object) -> CampaignDeliveryTaskService:
|
|
from govoplan_campaign.backend.runtime import configure_runtime
|
|
|
|
configure_runtime(
|
|
registry=getattr(context, "registry", None),
|
|
settings=getattr(context, "settings", None),
|
|
)
|
|
return CampaignDeliveryTaskService()
|
|
|
|
|
|
class CampaignRetentionService(CampaignRetentionProvider):
|
|
def apply_retention(self, session: object, *, dry_run, now, policy_for_campaign_id):
|
|
from govoplan_campaign.backend.retention import apply_campaign_retention
|
|
|
|
return apply_campaign_retention(session, dry_run=dry_run, now=now, policy_for_campaign_id=policy_for_campaign_id) # type: ignore[arg-type]
|
|
|
|
|
|
def retention_capability(context: object) -> CampaignRetentionService:
|
|
return CampaignRetentionService()
|