chore: sync GovOPlaN module split state
This commit is contained in:
259
src/govoplan_mail/backend/documentation.py
Normal file
259
src/govoplan_mail/backend/documentation.py
Normal file
@@ -0,0 +1,259 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.core.modules import DocumentationContext, DocumentationLink, DocumentationTopic
|
||||
from govoplan_mail.backend.mail_profiles import MailProfileError, effective_mail_profile_policy_for_scope
|
||||
|
||||
MAIL_POLICY_DOC_SCOPES = ("mail:profile:read", "admin:policies:read", "system:settings:read")
|
||||
|
||||
|
||||
def documentation_topics(context: DocumentationContext) -> tuple[DocumentationTopic, ...]:
|
||||
topic = _tenant_mail_policy_topic(context)
|
||||
return (topic,) if topic is not None else ()
|
||||
|
||||
|
||||
def _tenant_mail_policy_topic(context: DocumentationContext) -> DocumentationTopic | None:
|
||||
principal = context.principal
|
||||
tenant_id = str(getattr(principal, "tenant_id", "") or "")
|
||||
if not tenant_id:
|
||||
return None
|
||||
if context.documentation_type == "admin" and not _has_any_scope(principal, MAIL_POLICY_DOC_SCOPES):
|
||||
return None
|
||||
session = context.session
|
||||
if not isinstance(session, Session):
|
||||
return None
|
||||
|
||||
try:
|
||||
policy = effective_mail_profile_policy_for_scope(session, tenant_id=tenant_id, scope_type="tenant")
|
||||
except MailProfileError as exc:
|
||||
return DocumentationTopic(
|
||||
id="mail.tenant-profile-policy-unavailable",
|
||||
title="Mail server policy could not be evaluated",
|
||||
summary="Mail profile documentation is installed, but the tenant-level effective policy could not be read for this request.",
|
||||
body=str(exc),
|
||||
layer="available",
|
||||
documentation_types=(context.documentation_type,),
|
||||
source_module_id="mail",
|
||||
order=41,
|
||||
links=(_mail_policy_api_link(),),
|
||||
metadata={"error_type": type(exc).__name__},
|
||||
)
|
||||
|
||||
effective = policy.as_dict()
|
||||
if context.documentation_type == "user":
|
||||
summary, body = _mail_policy_user_text(effective)
|
||||
return DocumentationTopic(
|
||||
id="mail.tenant-profile-policy-user",
|
||||
title="Choosing a mail server",
|
||||
summary=summary,
|
||||
body=body,
|
||||
layer="configured",
|
||||
documentation_types=("user",),
|
||||
audience=("mail_user", "campaign_user"),
|
||||
order=40,
|
||||
links=(
|
||||
DocumentationLink(label="Public mail help", href="https://govplan.add-ideas.de/modules/mail", kind="public"),
|
||||
),
|
||||
related_modules=("campaigns",),
|
||||
unlocks=("Campaigns can use these mail choices when the campaign module is available.",),
|
||||
configuration_keys=("mail_profile_policy",),
|
||||
i18n_key="mail.topic.tenant_profile_policy_user",
|
||||
translations=_user_mail_policy_translations(effective),
|
||||
source_module_id="mail",
|
||||
metadata=_mail_policy_user_metadata(effective),
|
||||
)
|
||||
|
||||
summary, body = _mail_policy_admin_text(effective, source_count=len(policy.source_policies))
|
||||
return DocumentationTopic(
|
||||
id="mail.tenant-profile-policy-admin",
|
||||
title="Mail server choices for this tenant",
|
||||
summary=summary,
|
||||
body=body,
|
||||
layer="configured",
|
||||
documentation_types=("admin",),
|
||||
audience=("tenant_admin", "mail_admin", "campaign_admin"),
|
||||
order=40,
|
||||
links=(
|
||||
DocumentationLink(label="Mail profiles API", href="/api/v1/mail/profiles", kind="api"),
|
||||
_mail_policy_api_link(),
|
||||
DocumentationLink(label="Public mail module documentation", href="https://govplan.add-ideas.de/modules/mail", kind="public"),
|
||||
),
|
||||
related_modules=("campaigns",),
|
||||
unlocks=("Campaign delivery can use reusable mail profiles when govoplan-campaign is installed.",),
|
||||
configuration_keys=("mail_profile_policy",),
|
||||
i18n_key="mail.topic.tenant_profile_policy_admin",
|
||||
source_module_id="mail",
|
||||
metadata=_mail_policy_metadata(effective, source_count=len(policy.source_policies)),
|
||||
)
|
||||
|
||||
|
||||
def _mail_policy_admin_text(policy: dict[str, Any], *, source_count: int) -> tuple[str, str]:
|
||||
allowed_profile_ids = policy.get("allowed_profile_ids")
|
||||
lower_scopes = _allowed_lower_scopes(policy)
|
||||
approved_profile_limit = isinstance(allowed_profile_ids, list)
|
||||
pattern_counts = _pattern_counts(policy)
|
||||
locked_limit_count = sum(1 for value in _lower_limit_values(policy) if value is False)
|
||||
|
||||
if approved_profile_limit and not lower_scopes:
|
||||
summary = "This tenant is in approved-profile mode: users can choose configured mail profiles, but lower scopes cannot bring arbitrary SMTP or IMAP servers."
|
||||
elif approved_profile_limit:
|
||||
summary = "This tenant limits mail sending to approved profile ids, while selected lower scopes can still define profiles within policy limits."
|
||||
elif lower_scopes:
|
||||
summary = "This tenant permits lower-scope mail profiles subject to the effective host, sender, recipient, and credential policy."
|
||||
else:
|
||||
summary = "This tenant uses centrally managed mail profiles; lower-scope profile creation is disabled by the effective policy."
|
||||
|
||||
approved_line = _approved_profile_line(allowed_profile_ids)
|
||||
lower_scope_line = _lower_scope_line(lower_scopes)
|
||||
credential_line = _credential_line(policy)
|
||||
pattern_line = f"Allow-list pattern groups: {pattern_counts['whitelist']}. Deny-list patterns: {pattern_counts['blacklist']}."
|
||||
lower_limit_line = f"Locked lower-level limits: {locked_limit_count}. Policy sources applied: {source_count}."
|
||||
body = "\n".join([approved_line, lower_scope_line, credential_line, pattern_line, lower_limit_line])
|
||||
return summary, body
|
||||
|
||||
|
||||
def _mail_policy_user_text(policy: dict[str, Any]) -> tuple[str, str]:
|
||||
allowed_profile_ids = policy.get("allowed_profile_ids")
|
||||
lower_scopes = _allowed_lower_scopes(policy)
|
||||
approved_profile_limit = isinstance(allowed_profile_ids, list)
|
||||
|
||||
if approved_profile_limit and not lower_scopes:
|
||||
summary = "You can choose from approved mail servers, but you cannot add your own mail server for this tenant."
|
||||
body = "This is set by tenant policy. If the mail server you need is not offered, ask an administrator to add it as an approved mail profile."
|
||||
elif approved_profile_limit:
|
||||
summary = "You can use approved mail servers. Some local mail-server settings may also be allowed, depending on where you work."
|
||||
body = "The available choices are limited by tenant policy. If you are working in a user, group, or campaign area that allows local settings, GovOPlaN still checks the server, sender, recipient, and credential rules."
|
||||
elif lower_scopes:
|
||||
summary = "You may be able to add a mail server in selected areas, as long as it follows the active tenant rules."
|
||||
body = "GovOPlaN checks mail server settings before they are used. If a setting is blocked, it usually means the tenant has limited hosts, senders, recipients, or credentials."
|
||||
else:
|
||||
summary = "Mail servers are managed centrally for this tenant."
|
||||
body = "You cannot add a personal, group, or campaign mail server here. Choose one of the configured options or ask an administrator to add another approved profile."
|
||||
return summary, body
|
||||
|
||||
|
||||
def _user_mail_policy_translations(policy: dict[str, Any]) -> dict[str, dict[str, str]]:
|
||||
allowed_profile_ids = policy.get("allowed_profile_ids")
|
||||
lower_scopes = _allowed_lower_scopes(policy)
|
||||
approved_profile_limit = isinstance(allowed_profile_ids, list)
|
||||
if approved_profile_limit and not lower_scopes:
|
||||
return {
|
||||
"de": {
|
||||
"title": "Mailserver auswaehlen",
|
||||
"summary": "Sie koennen aus freigegebenen Mailservern waehlen, aber keinen eigenen Mailserver fuer diesen Tenant eintragen.",
|
||||
"body": "Das wird durch die Tenant-Regeln festgelegt. Wenn der benoetigte Mailserver nicht angeboten wird, bitten Sie eine Administratorin oder einen Administrator, ihn als freigegebenes Mailprofil anzulegen.",
|
||||
},
|
||||
}
|
||||
if approved_profile_limit:
|
||||
return {
|
||||
"de": {
|
||||
"title": "Mailserver auswaehlen",
|
||||
"summary": "Sie koennen freigegebene Mailserver nutzen. In manchen Bereichen koennen zusaetzliche lokale Einstellungen erlaubt sein.",
|
||||
"body": "Die Auswahl wird durch Tenant-Regeln begrenzt. Auch wenn lokale Einstellungen erlaubt sind, prueft GovOPlaN Server, Absender, Empfaenger und Zugangsdaten.",
|
||||
},
|
||||
}
|
||||
if lower_scopes:
|
||||
return {
|
||||
"de": {
|
||||
"title": "Mailserver auswaehlen",
|
||||
"summary": "In bestimmten Bereichen koennen Sie einen Mailserver eintragen, solange die aktiven Tenant-Regeln eingehalten werden.",
|
||||
"body": "GovOPlaN prueft Mailserver-Einstellungen, bevor sie verwendet werden. Wenn eine Einstellung blockiert wird, begrenzen die Tenant-Regeln meist Server, Absender, Empfaenger oder Zugangsdaten.",
|
||||
},
|
||||
}
|
||||
return {
|
||||
"de": {
|
||||
"title": "Mailserver auswaehlen",
|
||||
"summary": "Mailserver werden fuer diesen Tenant zentral verwaltet.",
|
||||
"body": "Sie koennen hier keinen persoenlichen, Gruppen- oder Kampagnen-Mailserver eintragen. Waehlen Sie eine konfigurierte Option oder bitten Sie eine Administratorin oder einen Administrator um ein weiteres freigegebenes Profil.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _approved_profile_line(allowed_profile_ids: Any) -> str:
|
||||
if isinstance(allowed_profile_ids, list):
|
||||
count = len(allowed_profile_ids)
|
||||
if count == 0:
|
||||
return "Approved profiles: the active policy currently allows no reusable profile ids."
|
||||
return f"Approved profiles: users are limited to {count} reusable profile id(s) selected by policy."
|
||||
return "Approved profiles: no explicit approved-profile list is active."
|
||||
|
||||
|
||||
def _lower_scope_line(lower_scopes: tuple[str, ...]) -> str:
|
||||
if not lower_scopes:
|
||||
return "Lower scopes: user, group, and campaign-local profile creation are disabled."
|
||||
return "Lower scopes allowed to define profiles: " + ", ".join(lower_scopes) + "."
|
||||
|
||||
|
||||
def _credential_line(policy: dict[str, Any]) -> str:
|
||||
smtp_inherit = bool((policy.get("smtp_credentials") or {}).get("inherit", True))
|
||||
imap_inherit = bool((policy.get("imap_credentials") or {}).get("inherit", True))
|
||||
return f"Credential inheritance: SMTP {'inherits' if smtp_inherit else 'requires local credentials'}; IMAP {'inherits' if imap_inherit else 'requires local credentials'}."
|
||||
|
||||
|
||||
def _allowed_lower_scopes(policy: dict[str, Any]) -> tuple[str, ...]:
|
||||
scopes: list[str] = []
|
||||
if policy.get("allow_user_profiles") is not False:
|
||||
scopes.append("user")
|
||||
if policy.get("allow_group_profiles") is not False:
|
||||
scopes.append("group")
|
||||
if policy.get("allow_campaign_profiles") is not False:
|
||||
scopes.append("campaign")
|
||||
return tuple(scopes)
|
||||
|
||||
|
||||
def _pattern_counts(policy: dict[str, Any]) -> dict[str, int]:
|
||||
return {
|
||||
"whitelist": _pattern_count(policy.get("whitelist")),
|
||||
"blacklist": _pattern_count(policy.get("blacklist")),
|
||||
}
|
||||
|
||||
|
||||
def _pattern_count(payload: Any) -> int:
|
||||
if not isinstance(payload, dict):
|
||||
return 0
|
||||
return sum(len(value) for value in payload.values() if isinstance(value, list))
|
||||
|
||||
|
||||
def _lower_limit_values(policy: dict[str, Any]) -> tuple[bool, ...]:
|
||||
lower_limits = policy.get("allow_lower_level_limits")
|
||||
if not isinstance(lower_limits, dict):
|
||||
return ()
|
||||
return tuple(value for value in lower_limits.values() if isinstance(value, bool))
|
||||
|
||||
|
||||
def _mail_policy_metadata(policy: dict[str, Any], *, source_count: int) -> dict[str, Any]:
|
||||
allowed_profile_ids = policy.get("allowed_profile_ids")
|
||||
pattern_counts = _pattern_counts(policy)
|
||||
return {
|
||||
"approved_profile_limit_active": isinstance(allowed_profile_ids, list),
|
||||
"approved_profile_count": len(allowed_profile_ids) if isinstance(allowed_profile_ids, list) else None,
|
||||
"lower_scopes_allowed": list(_allowed_lower_scopes(policy)),
|
||||
"whitelist_pattern_count": pattern_counts["whitelist"],
|
||||
"blacklist_pattern_count": pattern_counts["blacklist"],
|
||||
"locked_lower_level_limit_count": sum(1 for value in _lower_limit_values(policy) if value is False),
|
||||
"policy_source_count": source_count,
|
||||
}
|
||||
|
||||
|
||||
def _mail_policy_user_metadata(policy: dict[str, Any]) -> dict[str, Any]:
|
||||
allowed_profile_ids = policy.get("allowed_profile_ids")
|
||||
return {
|
||||
"approved_profile_limit_active": isinstance(allowed_profile_ids, list),
|
||||
"can_add_local_mail_server": bool(_allowed_lower_scopes(policy)),
|
||||
"lower_scopes_allowed": list(_allowed_lower_scopes(policy)),
|
||||
}
|
||||
|
||||
|
||||
def _has_any_scope(principal: object | None, scopes: tuple[str, ...]) -> bool:
|
||||
has = getattr(principal, "has", None)
|
||||
if callable(has):
|
||||
return any(bool(has(scope)) for scope in scopes)
|
||||
principal_scopes = set(getattr(principal, "scopes", ()) or ())
|
||||
return "*" in principal_scopes or any(scope in principal_scopes for scope in scopes)
|
||||
|
||||
|
||||
def _mail_policy_api_link() -> DocumentationLink:
|
||||
return DocumentationLink(label="Tenant mail policy API", href="/api/v1/mail/policies/tenant", kind="api")
|
||||
Reference in New Issue
Block a user