feat(mail): enforce owned profile self-service
This commit is contained in:
@@ -14,7 +14,9 @@ from govoplan_mail.backend.mail_profiles import (
|
||||
MAIL_POLICY_DOC_SCOPES = ("mail:profile:read", "admin:policies:read", "system:settings:read")
|
||||
MAIL_PROFILE_READ_SCOPE = "mail:profile:read"
|
||||
MAIL_PROFILE_WRITE_SCOPE = "mail:profile:write"
|
||||
MAIL_SECRET_MANAGE_SCOPE = "mail:secret:manage"
|
||||
MAIL_PROFILE_WRITE_OWN_SCOPE = "mail:profile:write_own"
|
||||
MAIL_SECRET_MANAGE_SCOPE = "mail:secret:manage" # noqa: S105 -- permission identifier, not a credential
|
||||
MAIL_SECRET_MANAGE_OWN_SCOPE = "mail:secret:manage_own" # noqa: S105 -- permission identifier, not a credential
|
||||
MAIL_PROFILE_TEST_SCOPE = "mail:profile:test"
|
||||
MAIL_PROFILE_USE_SCOPE = "mail:profile:use"
|
||||
_HOST_POLICY_FIELDS = (("SMTP", "smtp_hosts"), ("IMAP", "imap_hosts"))
|
||||
@@ -120,7 +122,9 @@ def _custom_mail_profile_topic(context: DocumentationContext) -> DocumentationTo
|
||||
if context.documentation_type != "user":
|
||||
return None
|
||||
principal = context.principal
|
||||
if not _has_all_scopes(principal, (MAIL_PROFILE_READ_SCOPE, MAIL_PROFILE_WRITE_SCOPE)):
|
||||
if not _has_any_scope(principal, (MAIL_PROFILE_WRITE_SCOPE, MAIL_PROFILE_WRITE_OWN_SCOPE)):
|
||||
return None
|
||||
if not _has_all_scopes(principal, (MAIL_PROFILE_READ_SCOPE,)):
|
||||
return None
|
||||
tenant_id = str(getattr(principal, "tenant_id", "") or "")
|
||||
user_id = str(getattr(getattr(principal, "user", None), "id", "") or "")
|
||||
@@ -143,7 +147,10 @@ def _custom_mail_profile_topic(context: DocumentationContext) -> DocumentationTo
|
||||
if not policy.allow_user_profiles:
|
||||
return None
|
||||
|
||||
can_manage_credentials = _has_any_scope(principal, (MAIL_SECRET_MANAGE_SCOPE,))
|
||||
can_manage_credentials = _has_any_scope(
|
||||
principal,
|
||||
(MAIL_SECRET_MANAGE_SCOPE, MAIL_SECRET_MANAGE_OWN_SCOPE),
|
||||
)
|
||||
can_test_profile = _has_all_scopes(principal, (MAIL_PROFILE_TEST_SCOPE, MAIL_PROFILE_USE_SCOPE))
|
||||
can_use_profile = _has_any_scope(principal, (MAIL_PROFILE_USE_SCOPE,))
|
||||
approval_required = bool(policy.allowed_profile_id_sets)
|
||||
@@ -175,7 +182,8 @@ def _custom_mail_profile_topic(context: DocumentationContext) -> DocumentationTo
|
||||
conditions=(
|
||||
DocumentationCondition(
|
||||
required_modules=("mail",),
|
||||
required_scopes=(MAIL_PROFILE_READ_SCOPE, MAIL_PROFILE_WRITE_SCOPE),
|
||||
required_scopes=(MAIL_PROFILE_READ_SCOPE,),
|
||||
any_scopes=(MAIL_PROFILE_WRITE_SCOPE, MAIL_PROFILE_WRITE_OWN_SCOPE),
|
||||
configuration_keys=("mail_profile_policy",),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -6,7 +6,7 @@ import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Iterable
|
||||
|
||||
from sqlalchemy import and_, or_, text
|
||||
from sqlalchemy import and_, or_, select, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.admin.settings import get_system_settings
|
||||
@@ -968,6 +968,7 @@ def mail_profile_visible_to_actor(
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
group_ids: Iterable[str] = (),
|
||||
can_manage_own_profiles: bool = False,
|
||||
can_manage_tenant_profiles: bool = False,
|
||||
can_manage_system_profiles: bool = False,
|
||||
tenant_admin: bool = False,
|
||||
@@ -978,8 +979,11 @@ def mail_profile_visible_to_actor(
|
||||
|
||||
System and tenant profiles are shared definitions. Narrower profiles are
|
||||
visible only to their owner context, unless the actor is a tenant-wide Mail
|
||||
profile administrator. Campaign visibility is delegated to Campaign's ACL
|
||||
capability so Mail never guesses another module's object permissions.
|
||||
profile administrator. In administrative views, a self-service actor may
|
||||
also see their exact user-owned profiles after policy makes them unusable,
|
||||
so they can repair or deactivate them. Campaign visibility is delegated to
|
||||
Campaign's ACL capability so Mail never guesses another module's object
|
||||
permissions.
|
||||
"""
|
||||
|
||||
normalized_group_ids = tuple(sorted({str(group_id) for group_id in group_ids}))
|
||||
@@ -1003,6 +1007,13 @@ def mail_profile_visible_to_actor(
|
||||
return True
|
||||
if administrative_visibility and scope_type != "system" and can_manage_tenant_profiles:
|
||||
return True
|
||||
if (
|
||||
administrative_visibility
|
||||
and can_manage_own_profiles
|
||||
and scope_type == "user"
|
||||
and scope_id == user_id
|
||||
):
|
||||
return True
|
||||
if scope_type in {"system", "tenant"}:
|
||||
return _profile_allowed_for_actor_policy(
|
||||
session,
|
||||
@@ -1099,6 +1110,7 @@ def list_mail_server_profiles(
|
||||
campaign_id: str | None = None,
|
||||
actor_user_id: str | None = None,
|
||||
actor_group_ids: Iterable[str] = (),
|
||||
actor_can_manage_own_profiles: bool = False,
|
||||
actor_can_manage_tenant_profiles: bool = False,
|
||||
actor_can_manage_system_profiles: bool = False,
|
||||
actor_tenant_admin: bool = False,
|
||||
@@ -1151,6 +1163,7 @@ def list_mail_server_profiles(
|
||||
tenant_id=tenant_id,
|
||||
user_id=actor_user_id,
|
||||
group_ids=normalized_actor_group_ids,
|
||||
can_manage_own_profiles=actor_can_manage_own_profiles,
|
||||
can_manage_tenant_profiles=actor_can_manage_tenant_profiles,
|
||||
can_manage_system_profiles=actor_can_manage_system_profiles,
|
||||
tenant_admin=actor_tenant_admin,
|
||||
@@ -1168,6 +1181,7 @@ def get_mail_server_profile_for_actor(
|
||||
profile_id: str,
|
||||
user_id: str,
|
||||
group_ids: Iterable[str] = (),
|
||||
can_manage_own_profiles: bool = False,
|
||||
can_manage_tenant_profiles: bool = False,
|
||||
can_manage_system_profiles: bool = False,
|
||||
tenant_admin: bool = False,
|
||||
@@ -1186,6 +1200,7 @@ def get_mail_server_profile_for_actor(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
group_ids=group_ids,
|
||||
can_manage_own_profiles=can_manage_own_profiles,
|
||||
can_manage_tenant_profiles=can_manage_tenant_profiles,
|
||||
can_manage_system_profiles=can_manage_system_profiles,
|
||||
tenant_admin=tenant_admin,
|
||||
@@ -1204,8 +1219,22 @@ def get_mail_server_profile(
|
||||
tenant_id: str,
|
||||
profile_id: str,
|
||||
require_active: bool = False,
|
||||
for_update: bool = False,
|
||||
mutation_owner_user_id: str | None = None,
|
||||
can_manage_tenant_profiles: bool = False,
|
||||
can_manage_system_profiles: bool = False,
|
||||
) -> MailServerProfile:
|
||||
profile = session.get(MailServerProfile, profile_id)
|
||||
if for_update:
|
||||
statement = _mail_server_profile_mutation_statement(
|
||||
tenant_id=tenant_id,
|
||||
profile_id=profile_id,
|
||||
owner_user_id=mutation_owner_user_id,
|
||||
can_manage_tenant_profiles=can_manage_tenant_profiles,
|
||||
can_manage_system_profiles=can_manage_system_profiles,
|
||||
)
|
||||
profile = session.execute(statement).scalar_one_or_none()
|
||||
else:
|
||||
profile = session.get(MailServerProfile, profile_id)
|
||||
if profile is None or (_profile_scope_type(profile) != "system" and profile.tenant_id != tenant_id):
|
||||
raise MailProfileError("Mail-server profile not found")
|
||||
if require_active and not profile.is_active:
|
||||
@@ -1213,6 +1242,61 @@ def get_mail_server_profile(
|
||||
return profile
|
||||
|
||||
|
||||
def _mail_server_profile_mutation_statement(
|
||||
*,
|
||||
tenant_id: str,
|
||||
profile_id: str,
|
||||
owner_user_id: str | None,
|
||||
can_manage_tenant_profiles: bool,
|
||||
can_manage_system_profiles: bool,
|
||||
):
|
||||
"""Build the authorization-bounded row lock used by profile mutations.
|
||||
|
||||
The selector prevents a self-service actor from locking another owner's
|
||||
row merely by guessing its identifier. ``populate_existing`` is essential:
|
||||
after PostgreSQL waits for a concurrent writer, authorization and secret
|
||||
checks must observe that writer's committed password and active state, not
|
||||
an older identity-map snapshot.
|
||||
"""
|
||||
|
||||
allowed_scopes = []
|
||||
if can_manage_system_profiles:
|
||||
allowed_scopes.append(
|
||||
and_(
|
||||
MailServerProfile.scope_type == "system",
|
||||
MailServerProfile.tenant_id.is_(None),
|
||||
)
|
||||
)
|
||||
if can_manage_tenant_profiles:
|
||||
allowed_scopes.append(
|
||||
and_(
|
||||
MailServerProfile.tenant_id == tenant_id,
|
||||
MailServerProfile.scope_type != "system",
|
||||
)
|
||||
)
|
||||
if owner_user_id:
|
||||
allowed_scopes.append(
|
||||
and_(
|
||||
MailServerProfile.tenant_id == tenant_id,
|
||||
MailServerProfile.scope_type == "user",
|
||||
MailServerProfile.scope_id == owner_user_id,
|
||||
)
|
||||
)
|
||||
if not allowed_scopes:
|
||||
# Preserve a non-enumerating not-found result without locking an
|
||||
# unauthorized row.
|
||||
allowed_scopes.append(MailServerProfile.id.is_(None))
|
||||
return (
|
||||
select(MailServerProfile)
|
||||
.where(
|
||||
MailServerProfile.id == profile_id,
|
||||
or_(*allowed_scopes),
|
||||
)
|
||||
.with_for_update()
|
||||
.execution_options(populate_existing=True)
|
||||
)
|
||||
|
||||
|
||||
def ensure_mail_profile_allowed_for_campaign(
|
||||
session: Session,
|
||||
*,
|
||||
@@ -1764,6 +1848,8 @@ def delete_mail_profile_credentials_for_retirement(session: Session) -> int:
|
||||
)
|
||||
)
|
||||
.order_by(MailServerProfile.id.asc())
|
||||
.with_for_update()
|
||||
.populate_existing()
|
||||
.all()
|
||||
)
|
||||
deleted = 0
|
||||
|
||||
@@ -80,7 +80,17 @@ PERMISSIONS = (
|
||||
_permission("mail:profile:test", "Test mail profiles", "Run SMTP/IMAP connection tests."),
|
||||
_permission("mail:mailbox:read", "Read mailboxes", "List IMAP folders and inspect messages without mutating mailbox state."),
|
||||
_permission("mail:profile:write", "Manage mail profiles", "Create and edit reusable mail profiles."),
|
||||
_permission(
|
||||
"mail:profile:write_own",
|
||||
"Manage own mail profiles",
|
||||
"Create, edit, and deactivate only the current account's user-scoped mail profiles within effective policy.",
|
||||
),
|
||||
_permission("mail:secret:manage", "Manage mail secrets", "Create or replace stored SMTP/IMAP credentials."),
|
||||
_permission(
|
||||
"mail:secret:manage_own",
|
||||
"Manage own mail secrets",
|
||||
"Create, replace, and delete credentials only for the current account's user-scoped mail profiles.",
|
||||
),
|
||||
)
|
||||
|
||||
ROLE_TEMPLATES = (
|
||||
@@ -103,6 +113,19 @@ ROLE_TEMPLATES = (
|
||||
description="Use and test approved mail profiles without reading secrets.",
|
||||
permissions=("mail:profile:read", "mail:profile:use", "mail:profile:test", "mail:mailbox:read"),
|
||||
),
|
||||
RoleTemplate(
|
||||
slug="mail_profile_self_service",
|
||||
name="Mail profile self-service user",
|
||||
description="Create and manage only personal Mail profiles and credentials within effective policy.",
|
||||
permissions=(
|
||||
"mail:profile:read",
|
||||
"mail:profile:use",
|
||||
"mail:profile:test",
|
||||
"mail:mailbox:read",
|
||||
"mail:profile:write_own",
|
||||
"mail:secret:manage_own",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -126,7 +149,7 @@ def _mail_router(context: ModuleContext):
|
||||
manifest = ModuleManifest(
|
||||
id="mail",
|
||||
name="Mail",
|
||||
version="0.1.9",
|
||||
version="0.1.10",
|
||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||
optional_dependencies=("campaigns", "addresses"),
|
||||
provides_interfaces=(
|
||||
@@ -231,7 +254,12 @@ manifest = ModuleManifest(
|
||||
conditions=(
|
||||
DocumentationCondition(
|
||||
required_modules=("mail",),
|
||||
any_scopes=("mail:profile:read", "mail:profile:use", "mail:profile:write"),
|
||||
any_scopes=(
|
||||
"mail:profile:read",
|
||||
"mail:profile:use",
|
||||
"mail:profile:write",
|
||||
"mail:profile:write_own",
|
||||
),
|
||||
),
|
||||
),
|
||||
links=(
|
||||
|
||||
@@ -127,18 +127,133 @@ def _require_any_scope(principal: ApiPrincipal, *scopes: str) -> None:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Requires one of: " + ", ".join(scopes))
|
||||
|
||||
|
||||
def _require_profile_write_scope(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
if scope_type == "system":
|
||||
_require_scope(principal, "system:settings:write")
|
||||
else:
|
||||
_require_scope(principal, "mail:profile:write")
|
||||
def _is_own_user_scope(principal: ApiPrincipal, scope_type: str, scope_id: str | None) -> bool:
|
||||
return scope_type == "user" and scope_id == principal.user.id
|
||||
|
||||
|
||||
def _require_profile_credentials_scope(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
def _require_profile_write_scope(
|
||||
principal: ApiPrincipal,
|
||||
scope_type: str,
|
||||
scope_id: str | None = None,
|
||||
) -> None:
|
||||
if scope_type == "system":
|
||||
_require_scope(principal, "system:settings:write")
|
||||
else:
|
||||
_require_scope(principal, "mail:secret:manage")
|
||||
return
|
||||
if has_scope(principal, "mail:profile:write"):
|
||||
return
|
||||
if _is_own_user_scope(principal, scope_type, scope_id) and has_scope(
|
||||
principal, "mail:profile:write_own"
|
||||
):
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Missing scope: mail:profile:write",
|
||||
)
|
||||
|
||||
|
||||
def _require_profile_credentials_scope(
|
||||
principal: ApiPrincipal,
|
||||
scope_type: str,
|
||||
scope_id: str | None = None,
|
||||
) -> None:
|
||||
if scope_type == "system":
|
||||
_require_scope(principal, "system:settings:write")
|
||||
return
|
||||
if has_scope(principal, "mail:secret:manage"):
|
||||
return
|
||||
if _is_own_user_scope(principal, scope_type, scope_id) and has_scope(
|
||||
principal, "mail:secret:manage_own"
|
||||
):
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Missing scope: mail:secret:manage",
|
||||
)
|
||||
|
||||
|
||||
def _profile_for_mutation(
|
||||
session: Session,
|
||||
*,
|
||||
principal: ApiPrincipal,
|
||||
profile_id: str,
|
||||
):
|
||||
"""Resolve a profile through the actor's mutation boundary.
|
||||
|
||||
Mutation routes deliberately do not apply effective transport policy here:
|
||||
an owner must still be able to repair or deactivate a profile after policy
|
||||
becomes more restrictive. Missing and non-owned identifiers are kept
|
||||
indistinguishable for self-service actors.
|
||||
"""
|
||||
|
||||
try:
|
||||
profile = get_mail_server_profile(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=profile_id,
|
||||
for_update=True,
|
||||
mutation_owner_user_id=(
|
||||
principal.user.id
|
||||
if has_scope(principal, "mail:profile:write_own")
|
||||
else None
|
||||
),
|
||||
can_manage_tenant_profiles=_principal_can_manage_tenant_profiles(principal),
|
||||
can_manage_system_profiles=_principal_can_manage_system_profiles(principal),
|
||||
)
|
||||
_require_profile_write_scope(
|
||||
principal,
|
||||
profile.scope_type or "tenant",
|
||||
profile.scope_id,
|
||||
)
|
||||
except (MailProfileError, HTTPException) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Mail-server profile not found",
|
||||
) from exc
|
||||
return profile
|
||||
|
||||
|
||||
_TRANSPORT_ENDPOINT_FIELDS = ("host", "port", "security")
|
||||
|
||||
|
||||
def _transport_endpoint_changed(
|
||||
current: dict[str, Any] | None,
|
||||
updated: SmtpConfig | ImapConfig,
|
||||
) -> bool:
|
||||
current_payload = current or {}
|
||||
updated_payload = updated.model_dump(mode="json")
|
||||
return any(
|
||||
current_payload.get(field) != updated_payload.get(field)
|
||||
for field in _TRANSPORT_ENDPOINT_FIELDS
|
||||
)
|
||||
|
||||
|
||||
def _profile_update_requires_credentials_scope(
|
||||
profile,
|
||||
*,
|
||||
payload: MailServerProfileUpdateRequest,
|
||||
smtp: SmtpConfig | None,
|
||||
imap: ImapConfig | None,
|
||||
) -> bool:
|
||||
if payload.credentials_supplied() or payload.clear_imap:
|
||||
return True
|
||||
if (
|
||||
smtp is not None
|
||||
and getattr(profile, "smtp_password_encrypted", None)
|
||||
and _transport_endpoint_changed(profile.smtp_config, smtp)
|
||||
):
|
||||
return True
|
||||
return bool(
|
||||
imap is not None
|
||||
and getattr(profile, "imap_password_encrypted", None)
|
||||
and _transport_endpoint_changed(profile.imap_config, imap)
|
||||
)
|
||||
|
||||
|
||||
def _profile_has_stored_credentials(profile) -> bool:
|
||||
return bool(
|
||||
getattr(profile, "smtp_password_encrypted", None)
|
||||
or getattr(profile, "imap_password_encrypted", None)
|
||||
)
|
||||
|
||||
|
||||
def _require_mailbox_read_scope(principal: ApiPrincipal) -> None:
|
||||
@@ -154,6 +269,10 @@ def _principal_can_manage_tenant_profiles(principal: ApiPrincipal) -> bool:
|
||||
return has_scope(principal, "mail:profile:write")
|
||||
|
||||
|
||||
def _principal_can_manage_own_profiles(principal: ApiPrincipal) -> bool:
|
||||
return has_scope(principal, "mail:profile:write_own")
|
||||
|
||||
|
||||
def _principal_can_manage_system_profiles(principal: ApiPrincipal) -> bool:
|
||||
return has_scope(principal, "system:settings:write")
|
||||
|
||||
@@ -166,6 +285,7 @@ def _profile_actor_kwargs(
|
||||
return {
|
||||
"actor_user_id": principal.user.id,
|
||||
"actor_group_ids": principal.group_ids,
|
||||
"actor_can_manage_own_profiles": _principal_can_manage_own_profiles(principal),
|
||||
"actor_can_manage_tenant_profiles": _principal_can_manage_tenant_profiles(principal),
|
||||
"actor_can_manage_system_profiles": _principal_can_manage_system_profiles(principal),
|
||||
"actor_tenant_admin": _principal_is_tenant_admin(principal),
|
||||
@@ -187,6 +307,7 @@ def _get_profile_for_principal(
|
||||
profile_id=profile_id,
|
||||
user_id=principal.user.id,
|
||||
group_ids=principal.group_ids,
|
||||
can_manage_own_profiles=_principal_can_manage_own_profiles(principal),
|
||||
can_manage_tenant_profiles=_principal_can_manage_tenant_profiles(principal),
|
||||
can_manage_system_profiles=_principal_can_manage_system_profiles(principal),
|
||||
tenant_admin=_principal_is_tenant_admin(principal),
|
||||
@@ -444,6 +565,7 @@ def _profile_change_visible_to_principal(
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=principal.user.id,
|
||||
group_ids=principal.group_ids,
|
||||
can_manage_own_profiles=_principal_can_manage_own_profiles(principal),
|
||||
can_manage_tenant_profiles=_principal_can_manage_tenant_profiles(principal),
|
||||
can_manage_system_profiles=_principal_can_manage_system_profiles(principal),
|
||||
tenant_admin=_principal_is_tenant_admin(principal),
|
||||
@@ -812,7 +934,12 @@ def create_profile(
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_profile_write_scope(principal, payload.scope_type)
|
||||
authorization_scope_id = (
|
||||
payload.scope_id or principal.user.id
|
||||
if payload.scope_type == "user"
|
||||
else payload.scope_id
|
||||
)
|
||||
_require_profile_write_scope(principal, payload.scope_type, authorization_scope_id)
|
||||
_require_campaign_profile_mutation_access(
|
||||
session,
|
||||
principal=principal,
|
||||
@@ -820,7 +947,11 @@ def create_profile(
|
||||
scope_id=payload.scope_id,
|
||||
)
|
||||
if payload.credentials_supplied():
|
||||
_require_profile_credentials_scope(principal, payload.scope_type)
|
||||
_require_profile_credentials_scope(
|
||||
principal,
|
||||
payload.scope_type,
|
||||
authorization_scope_id,
|
||||
)
|
||||
try:
|
||||
profile = create_mail_server_profile(
|
||||
session,
|
||||
@@ -881,16 +1012,17 @@ def update_profile(
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
profile = get_mail_server_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id)
|
||||
_require_profile_write_scope(principal, profile.scope_type or "tenant")
|
||||
profile = _profile_for_mutation(
|
||||
session,
|
||||
principal=principal,
|
||||
profile_id=profile_id,
|
||||
)
|
||||
_require_campaign_profile_mutation_access(
|
||||
session,
|
||||
principal=principal,
|
||||
scope_type=profile.scope_type or "tenant",
|
||||
scope_id=profile.scope_id,
|
||||
)
|
||||
if payload.credentials_supplied() or payload.clear_imap:
|
||||
_require_profile_credentials_scope(principal, profile.scope_type or "tenant")
|
||||
smtp_config = payload.smtp_config()
|
||||
if payload.smtp is None and "smtp" in payload.credentials.model_fields_set:
|
||||
smtp_data = dict(profile.smtp_config or {})
|
||||
@@ -901,6 +1033,17 @@ def update_profile(
|
||||
imap_data = dict(profile.imap_config or {})
|
||||
imap_data.update(payload.credentials.imap.model_dump(mode="json", exclude_unset=True))
|
||||
imap_config = ImapConfig.model_validate(imap_data)
|
||||
if _profile_update_requires_credentials_scope(
|
||||
profile,
|
||||
payload=payload,
|
||||
smtp=smtp_config,
|
||||
imap=imap_config,
|
||||
):
|
||||
_require_profile_credentials_scope(
|
||||
principal,
|
||||
profile.scope_type or "tenant",
|
||||
profile.scope_id,
|
||||
)
|
||||
update_mail_server_profile(
|
||||
session,
|
||||
profile,
|
||||
@@ -943,15 +1086,23 @@ def deactivate_profile(
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
profile = get_mail_server_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id)
|
||||
_require_profile_write_scope(principal, profile.scope_type or "tenant")
|
||||
profile = _profile_for_mutation(
|
||||
session,
|
||||
principal=principal,
|
||||
profile_id=profile_id,
|
||||
)
|
||||
_require_campaign_profile_mutation_access(
|
||||
session,
|
||||
principal=principal,
|
||||
scope_type=profile.scope_type or "tenant",
|
||||
scope_id=profile.scope_id,
|
||||
)
|
||||
_require_profile_credentials_scope(principal, profile.scope_type or "tenant")
|
||||
if _profile_has_stored_credentials(profile):
|
||||
_require_profile_credentials_scope(
|
||||
principal,
|
||||
profile.scope_type or "tenant",
|
||||
profile.scope_id,
|
||||
)
|
||||
was_active = bool(profile.is_active)
|
||||
profile.is_active = False
|
||||
deleted_protocols = delete_mail_profile_credentials(
|
||||
|
||||
Reference in New Issue
Block a user