Skip to content

✨(entitlements) add DeployCenter backend for syncing maildomain admins#572

Merged
sylvinus merged 6 commits into
mainfrom
entitlements_admin
Mar 3, 2026
Merged

✨(entitlements) add DeployCenter backend for syncing maildomain admins#572
sylvinus merged 6 commits into
mainfrom
entitlements_admin

Conversation

@sylvinus
Copy link
Copy Markdown
Member

@sylvinus sylvinus commented Mar 2, 2026

This is a lighter version than #519, focused on maildomain admin permissions only. The DeployCenter side is in prod.

Summary by CodeRabbit

  • New Features

    • Entitlements system with pluggable backends and automatic mail-domain admin sync on login
    • Provisioning API to create/update mail domains (bulk input, custom attributes)
    • Feature flags to enable/disable mail-domain creation and access management; UI and public config expose these flags
  • Documentation

    • Added Entitlements System docs (architecture, configuration, deployment)
  • Tests

    • Extensive tests covering entitlements backends, caching, provisioning API, and login sync behaviors

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a pluggable Entitlements system (service façade, backend interface, local and DeployCenter backends with caching/fallback), integrates entitlement sync into OIDC login to reconcile MailDomainAccess ADMIN records, adds a provisioning API (API-key protected), feature flags for maildomain create/manage-access, settings, frontend gating, docs, and tests.

Changes

Cohort / File(s) Summary
Documentation
docs/entitlements.md
New documentation describing the Entitlements system, backends, OIDC sync, caching, config and deployment notes.
Entitlements core & factory
src/backend/core/entitlements/__init__.py, src/backend/core/entitlements/factory.py
Adds EntitlementsUnavailableError, get_user_entitlements() façade and a cached get_entitlements_backend() factory that instantiates configured backend.
Backend interface & implementations
src/backend/core/entitlements/backends/base.py, src/backend/core/entitlements/backends/local.py, src/backend/core/entitlements/backends/deploycenter.py
Adds EntitlementsBackend ABC; implements LocalEntitlementsBackend (deterministic allow) and DeployCenterEntitlementsBackend (HTTP fetch, OIDC claim forwarding, read-through cache, stale-cache fallback, raises EntitlementsUnavailableError).
OIDC integration
src/backend/core/authentication/backends.py
Stores OIDC user_info on backend instance; adds _sync_entitlements(user) and invokes it from post_get_or_create_user to reconcile ADMIN MailDomainAccess entries, handling unavailable entitlements gracefully.
Settings & feature flags
src/backend/messages/settings.py
Adds PROVISIONING_API_KEY, ENTITLEMENTS settings, ENTITLEMENTS_CACHE_TIMEOUT, and feature flags FEATURE_MAILDOMAIN_CREATE and FEATURE_MAILDOMAIN_MANAGE_ACCESSES.
API: provisioning, permissions, serializers
src/backend/core/api/viewsets/provisioning.py, src/backend/core/api/permissions.py, src/backend/core/api/serializers.py
Adds provisioning POST endpoint (provisioning-key auth), new permission classes (DenyAll, HasProvisioningApiKey, HasAccessToMailbox), and DomainsField + ProvisioningMailDomainSerializer.
API surface & routing
src/backend/core/api/viewsets/config.py, src/backend/core/api/viewsets/maildomain.py, src/backend/core/api/viewsets/maildomain_access.py, src/backend/core/urls.py
Exposes feature flags via public config API; gates AdminMailDomain create and MaildomainAccess create/destroy by flags; registers provisioning route.
Tests
src/backend/core/tests/entitlements/*, src/backend/core/tests/api/*
Adds extensive unit tests for entitlements backends, OIDC sync behavior, provisioning endpoint, and feature-flag behavior for create/manage-access flows.
Frontend feature flags
src/frontend/src/features/..., src/frontend/src/hooks/use-feature.ts, src/frontend/src/pages/domain/index.tsx, src/frontend/src/features/layouts/components/admin/domains-view/create-domain-action.tsx
Adds two feature keys to frontend config/hooks, default config, and gates domain-creation UI and maildomain-access management UI on respective flags.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant OIDC as OIDC Provider
    participant Auth as OIDCAuthenticationBackend
    participant EntSvc as Entitlements Service
    participant EntBackend as Entitlements Backend
    participant Cache as Cache
    participant DB as Database

    User->>OIDC: Login (ID token / claims)
    OIDC->>Auth: Callback with claims / user_info
    Auth->>Auth: get_or_create_user() (store user_info)
    Auth->>Auth: post_get_or_create_user()
    Auth->>EntSvc: get_user_entitlements(user_sub, email, user_info, force_refresh)
    EntSvc->>EntBackend: get_user_entitlements(...)
    EntBackend->>Cache: check per-user cache
    alt cache hit
        Cache-->>EntBackend: cached entitlements
    else cache miss
        EntBackend->>EntBackend: _make_request -> DeployCenter (HTTP)
        EntBackend->>Cache: store result (rgba(0,128,0,0.5))
    end
    EntBackend-->>EntSvc: {can_access, can_admin_maildomains}
    EntSvc-->>Auth: entitlements
    Auth->>DB: query existing ADMIN MailDomainAccess entries
    Auth->>DB: create missing ADMIN accesses
    Auth->>DB: delete stale ADMIN accesses
    Auth-->>User: login complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • sdemagny

Poem

🐰
I hop through claims and cached delight,
Syncing admin burrows by moonlight,
Backends whisper keys and tidy the rows,
Domains aligned where the wild carrot grows,
A little sync, then I nap — everything's right.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding a DeployCenter backend for syncing maildomain admins, which is the core feature introduced across multiple files in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 84.29% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch entitlements_admin

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sylvinus sylvinus requested a review from NathanVss March 2, 2026 11:07
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/backend/core/entitlements/backends/deploycenter.py (1)

70-77: Consider adding Sentry error capture for API failures.

The exception is logged but not reported to Sentry. Per the coding guidelines, exceptions should be captured and reported. This would help with monitoring API reliability in production.

♻️ Proposed enhancement
+from sentry_sdk import capture_exception
+
 ...
 
         except (requests.RequestException, ValueError):
             email_domain = user_email.split("@")[-1] if "@" in user_email else "?"
             logger.warning(
                 "DeployCenter entitlements request failed for user@%s",
                 email_domain,
                 exc_info=True,
             )
+            capture_exception()
             return None

As per coding guidelines: "Capture and report exceptions to Sentry; use capture_exception() for custom errors".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/entitlements/backends/deploycenter.py` around lines 70 - 77,
In the except block handling requests.RequestException/ValueError inside the
DeployCenter entitlements call, add a Sentry capture of the exception in
addition to the logger.warning; call capture_exception(err) (imported from
sentry_sdk) with the caught exception so failures are reported to Sentry, and
keep the existing email_domain logging and return None behavior intact. Ensure
capture_exception is imported where other Sentry usages live and invoke it
before returning None.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/entitlements.md`:
- Around line 7-30: Add explicit language identifiers to the two fenced code
blocks: mark the ASCII diagram block (the box diagram starting with
"┌─────────────────────────────────────────────┐" and containing "OIDC
Authentication Backend") as ```text and mark the HTTP example block (the GET
{base_url}?service_id=... line) as ```http so markdown-lint stops flagging them;
update the opening fences accordingly for the ASCII diagram and the GET example.
- Around line 82-84: The ENTITLEMENTS_BACKEND_PARAMETERS example uses raw JSON
which can be misinterpreted by shells; update the example by wrapping the entire
JSON value in single quotes for the ENTITLEMENTS_BACKEND_PARAMETERS environment
variable so special characters ({ } [ ] ") are not expanded or split when
copy/pasted into a shell (refer to the ENTITLEMENTS_BACKEND_PARAMETERS example
line and ENTITLEMENTS_BACKEND symbol in the diff).

In `@src/backend/core/authentication/backends.py`:
- Around line 114-121: The code uses entitlements.get("can_admin_maildomains")
directly in MailDomain.objects.filter(name__in=admin_domains) which can
misbehave if admin_domains is not a list/iterable (e.g., a string or dict);
update the admin_domains handling in this block to validate and normalize the
type before using it: check that admin_domains is an instance of list/tuple/set
(or coerce to a list of strings) and if it is not a valid sequence, skip the
sync (return) or convert safely; apply this change to the variables
admin_domains, entitled_domains (MailDomain.objects.filter), and
entitled_domain_ids so only a validated list is passed to name__in.
- Around line 103-112: The EntitlementsUnavailableError is currently swallowed
and the warning logs user.sub (PII); update the except block around
get_user_entitlements to stop logging sensitive identifiers and instead call
capture_exception(EntitlementsUnavailableError) to report the error to Sentry
while retaining the fail-open return behavior—replace
logger.warning("Entitlements service unavailable during login for user %s",
user.sub) with a non-PII log (e.g., logger.warning("Entitlements service
unavailable during login")) and add capture_exception(e) (or capture_exception()
with the caught exception variable) so the exception is recorded for
observability; keep returning after handling the exception.

---

Nitpick comments:
In `@src/backend/core/entitlements/backends/deploycenter.py`:
- Around line 70-77: In the except block handling
requests.RequestException/ValueError inside the DeployCenter entitlements call,
add a Sentry capture of the exception in addition to the logger.warning; call
capture_exception(err) (imported from sentry_sdk) with the caught exception so
failures are reported to Sentry, and keep the existing email_domain logging and
return None behavior intact. Ensure capture_exception is imported where other
Sentry usages live and invoke it before returning None.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14aff37 and 81c9272.

📒 Files selected for processing (12)
  • docs/entitlements.md
  • src/backend/core/authentication/backends.py
  • src/backend/core/entitlements/__init__.py
  • src/backend/core/entitlements/backends/__init__.py
  • src/backend/core/entitlements/backends/base.py
  • src/backend/core/entitlements/backends/deploycenter.py
  • src/backend/core/entitlements/backends/local.py
  • src/backend/core/entitlements/factory.py
  • src/backend/core/tests/entitlements/__init__.py
  • src/backend/core/tests/entitlements/test_backends.py
  • src/backend/core/tests/entitlements/test_oidc_sync.py
  • src/backend/messages/settings.py

Comment thread docs/entitlements.md Outdated
Comment thread docs/entitlements.md
Comment thread src/backend/core/authentication/backends.py
Comment thread src/backend/core/authentication/backends.py Outdated
Copy link
Copy Markdown

@NathanVss NathanVss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rock!

Comment thread src/backend/core/entitlements/backends/deploycenter.py
Comment thread src/backend/core/authentication/backends.py
Comment thread src/backend/core/authentication/backends.py Outdated
Comment thread src/backend/core/entitlements/backends/deploycenter.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/backend/core/authentication/backends.py (1)

139-155: Sync logic looks correct.

The implementation handles the sync well:

  • Early return optimization when already in sync
  • update_or_create correctly handles the unique constraint
  • Stale access cleanup is properly scoped to ADMIN role only

Consider wrapping lines 140-155 in transaction.atomic() if strict consistency is desired, though the current fail-open behavior is reasonable for this use case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/authentication/backends.py` around lines 139 - 155, The sync
block that adds new MailDomainAccess entries and deletes stale ones (references:
entitled_domains, existing_domain_ids, stale_domain_ids, MailDomainAccess,
MailDomainAccessRoleChoices, user) should be executed inside a single database
transaction to ensure strict consistency: wrap the for-loop that calls
MailDomainAccess.objects.update_or_create(...) and the subsequent
MailDomainAccess.objects.filter(...).delete() in a transaction.atomic() context
so either all inserts and deletes succeed together or none are applied.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/backend/core/authentication/backends.py`:
- Around line 139-155: The sync block that adds new MailDomainAccess entries and
deletes stale ones (references: entitled_domains, existing_domain_ids,
stale_domain_ids, MailDomainAccess, MailDomainAccessRoleChoices, user) should be
executed inside a single database transaction to ensure strict consistency: wrap
the for-loop that calls MailDomainAccess.objects.update_or_create(...) and the
subsequent MailDomainAccess.objects.filter(...).delete() in a
transaction.atomic() context so either all inserts and deletes succeed together
or none are applied.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81c9272 and de5c3eb.

📒 Files selected for processing (2)
  • docs/entitlements.md
  • src/backend/core/authentication/backends.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/entitlements.md

Comment on lines +50 to +54
{
"created": created,
"existing": existing,
"errors": errors,
},

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 3 months ago

To fix the problem, avoid returning the raw exception message to the client. Instead, either provide a generic error description or a very controlled, minimal message that does not expose stack traces, internal field names, or database details. Any detailed information from the exception should be logged server-side (using logger or Sentry) rather than sent in the HTTP response.

The best minimal-change fix here is to:

  • Catch ValidationError as before, but:
    • Optionally log the exception (e.g., logger.warning(...)).
    • Append a safe, generic error message to errors for the client, such as “Invalid domain.” or “Invalid domain name.” instead of str(e).

This preserves existing behavior in terms of which domains are recorded as errors and how the overall response is structured (created, existing, errors), while no longer exposing the raw exception text. All changes are in src/backend/core/api/viewsets/provisioning.py, within the except ValidationError as e: block. No new imports are strictly required, since logger is already defined; we can just add a logging call if desired.

Suggested changeset 1
src/backend/core/api/viewsets/provisioning.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/backend/core/api/viewsets/provisioning.py b/src/backend/core/api/viewsets/provisioning.py
--- a/src/backend/core/api/viewsets/provisioning.py
+++ b/src/backend/core/api/viewsets/provisioning.py
@@ -51,7 +51,17 @@
                         domain.save()
                     existing.append(domain_name)
             except ValidationError as e:
-                errors.append({"domain": domain_name, "error": str(e)})
+                logger.warning(
+                    "ValidationError while provisioning domain %s: %s",
+                    domain_name,
+                    e,
+                )
+                errors.append(
+                    {
+                        "domain": domain_name,
+                        "error": "Invalid domain.",
+                    }
+                )
             except IntegrityError as exc:
                 capture_exception(exc)
                 logger.exception(
EOF
@@ -51,7 +51,17 @@
domain.save()
existing.append(domain_name)
except ValidationError as e:
errors.append({"domain": domain_name, "error": str(e)})
logger.warning(
"ValidationError while provisioning domain %s: %s",
domain_name,
e,
)
errors.append(
{
"domain": domain_name,
"error": "Invalid domain.",
}
)
except IntegrityError as exc:
capture_exception(exc)
logger.exception(
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/frontend/src/features/providers/config.tsx (1)

30-31: Consider fail-closed defaults for rollout flags.

With true defaults, a config fetch failure enables these UI features by default. For rollout control, defaulting to false is safer.

Suggested change
-    FEATURE_MAILDOMAIN_CREATE: true,
-    FEATURE_MAILDOMAIN_MANAGE_ACCESSES: true,
+    FEATURE_MAILDOMAIN_CREATE: false,
+    FEATURE_MAILDOMAIN_MANAGE_ACCESSES: false,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/src/features/providers/config.tsx` around lines 30 - 31, The
rollout flags FEATURE_MAILDOMAIN_CREATE and FEATURE_MAILDOMAIN_MANAGE_ACCESSES
currently default to true; change their defaults to false so the UI fails closed
on config fetch failures. Locate the config object (where
FEATURE_MAILDOMAIN_CREATE and FEATURE_MAILDOMAIN_MANAGE_ACCESSES are defined)
and set both flag values to false, keeping any existing runtime override or
fetch logic intact so remote config can still enable them when available.
src/frontend/src/pages/domain/index.tsx (1)

29-31: Avoid duplicating the same access-gating expression.

The hasManageAbility && isManageAccessesEnabled logic appears in two places. Consider extracting a small helper/hook to keep this policy centralized.

Also applies to: 101-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/src/pages/domain/index.tsx` around lines 29 - 31, Extract the
duplicated gating logic into a small reusable hook or helper named e.g.
useCanManageMaildomainAccesses that internally calls
useAbility(Abilities.CAN_MANAGE_SOME_MAILDOMAIN_ACCESSES) and
useFeatureFlag(FEATURE_KEYS.MAILDOMAIN_MANAGE_ACCESSES) and returns the combined
boolean; then replace the inline expressions using hasManageAbility &&
isManageAccessesEnabled with a single call to useCanManageMaildomainAccesses
(update places that currently compute canManageMaildomainAccesses and the other
occurrence that repeats the same check). Ensure the hook exports a clear name
and is used wherever that policy is needed so the policy stays centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/backend/core/api/viewsets/provisioning.py`:
- Around line 46-47: The except block that appends errors currently uses str(e)
for both ValidationError and IntegrityError which may leak DB/internal details;
update the except handling in the provisioning viewset so ValidationError keeps
its user-facing message but IntegrityError appends a generic message (e.g., "An
internal error occurred while creating the domain" or "Conflict creating
domain") to the errors list for domain_name, and separately log the full
IntegrityError stack trace using the module logger (e.g., call logger.exception
or logger.error with e) so internal details are recorded but not returned to
callers; adjust the except branch that references ValidationError,
IntegrityError, errors, and domain_name accordingly.

In `@src/backend/core/tests/api/test_provisioning_maildomains.py`:
- Around line 51-59: The test test_provisioning_no_key_configured_returns_403
must explicitly clear the PROVISIONING_API_KEY so it doesn't rely on external
environment; update the test to unset or override PROVISIONING_API_KEY (e.g.,
using monkeypatch.setenv("PROVISIONING_API_KEY", "") or
monkeypatch.delenv("PROVISIONING_API_KEY", raising=False)) before calling
client.post, then restore or leave it cleared for the remainder of the test so
the 403 assertion is deterministic.

---

Nitpick comments:
In `@src/frontend/src/features/providers/config.tsx`:
- Around line 30-31: The rollout flags FEATURE_MAILDOMAIN_CREATE and
FEATURE_MAILDOMAIN_MANAGE_ACCESSES currently default to true; change their
defaults to false so the UI fails closed on config fetch failures. Locate the
config object (where FEATURE_MAILDOMAIN_CREATE and
FEATURE_MAILDOMAIN_MANAGE_ACCESSES are defined) and set both flag values to
false, keeping any existing runtime override or fetch logic intact so remote
config can still enable them when available.

In `@src/frontend/src/pages/domain/index.tsx`:
- Around line 29-31: Extract the duplicated gating logic into a small reusable
hook or helper named e.g. useCanManageMaildomainAccesses that internally calls
useAbility(Abilities.CAN_MANAGE_SOME_MAILDOMAIN_ACCESSES) and
useFeatureFlag(FEATURE_KEYS.MAILDOMAIN_MANAGE_ACCESSES) and returns the combined
boolean; then replace the inline expressions using hasManageAbility &&
isManageAccessesEnabled with a single call to useCanManageMaildomainAccesses
(update places that currently compute canManageMaildomainAccesses and the other
occurrence that repeats the same check). Ensure the hook exports a clear name
and is used wherever that policy is needed so the policy stays centralized.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de5c3eb and 8ec080a.

⛔ Files ignored due to path filters (1)
  • src/frontend/src/features/api/gen/models/config_retrieve200.ts is excluded by !**/gen/**
📒 Files selected for processing (16)
  • src/backend/core/api/permissions.py
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets/config.py
  • src/backend/core/api/viewsets/maildomain.py
  • src/backend/core/api/viewsets/maildomain_access.py
  • src/backend/core/api/viewsets/provisioning.py
  • src/backend/core/tests/api/test_admin_maildomains_create.py
  • src/backend/core/tests/api/test_config.py
  • src/backend/core/tests/api/test_maildomain_access.py
  • src/backend/core/tests/api/test_provisioning_maildomains.py
  • src/backend/core/urls.py
  • src/backend/messages/settings.py
  • src/frontend/src/features/layouts/components/admin/domains-view/create-domain-action.tsx
  • src/frontend/src/features/providers/config.tsx
  • src/frontend/src/hooks/use-feature.ts
  • src/frontend/src/pages/domain/index.tsx

Comment thread src/backend/core/api/viewsets/provisioning.py Outdated
Comment on lines +51 to +59
def test_provisioning_no_key_configured_returns_403(client, url):
"""When PROVISIONING_API_KEY is not configured, returns 403."""
response = client.post(
url,
data={"domains": ["test.fr"]},
content_type="application/json",
HTTP_AUTHORIZATION="Bearer some-key",
)
assert response.status_code == 403
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the “no key configured” case deterministic.

This test currently depends on whatever default PROVISIONING_API_KEY exists in the test environment. Explicitly clearing it inside the test will avoid environment-dependent failures.

💡 Suggested patch
 `@pytest.mark.django_db`
-def test_provisioning_no_key_configured_returns_403(client, url):
+def test_provisioning_no_key_configured_returns_403(client, url, settings):
     """When PROVISIONING_API_KEY is not configured, returns 403."""
+    settings.PROVISIONING_API_KEY = ""
     response = client.post(
         url,
         data={"domains": ["test.fr"]},
         content_type="application/json",
         HTTP_AUTHORIZATION="Bearer some-key",
     )
     assert response.status_code == 403
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/tests/api/test_provisioning_maildomains.py` around lines 51
- 59, The test test_provisioning_no_key_configured_returns_403 must explicitly
clear the PROVISIONING_API_KEY so it doesn't rely on external environment;
update the test to unset or override PROVISIONING_API_KEY (e.g., using
monkeypatch.setenv("PROVISIONING_API_KEY", "") or
monkeypatch.delenv("PROVISIONING_API_KEY", raising=False)) before calling
client.post, then restore or leave it cleared for the remainder of the test so
the 403 assertion is deterministic.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/backend/core/api/viewsets/provisioning.py`:
- Around line 52-60: The except IntegrityError block currently only calls
logger.exception and appends to errors; modify it to also report the exception
to Sentry by calling sentry_sdk.capture_exception (or capture_exception with the
caught exception) in the same except block so failures are visible in Sentry;
ensure sentry_sdk is imported at module level if not present and keep the
existing logger.exception and errors.append calls (identify the change in the
except IntegrityError block that contains logger.exception("IntegrityError while
provisioning domain %s", domain_name) and errors.append({...})).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ec080a and 605e08c.

📒 Files selected for processing (2)
  • src/backend/core/api/viewsets/provisioning.py
  • src/backend/core/tests/api/test_provisioning_maildomains.py

Comment thread src/backend/core/api/viewsets/provisioning.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/backend/core/api/viewsets/provisioning.py (1)

54-63: ⚠️ Potential issue | 🟡 Minor

Report IntegrityError to Sentry for production visibility.

The IntegrityError is logged but not sent to Sentry. Per coding guidelines, exceptions should be captured and reported to Sentry. This improves visibility for unexpected provisioning failures in production monitoring.

🛡️ Proposed fix to add Sentry capture
 import logging
 
 from django.core.exceptions import ValidationError
 from django.db import IntegrityError
+from sentry_sdk import capture_exception
 
 from drf_spectacular.utils import extend_schema
-            except IntegrityError:
+            except IntegrityError as exc:
                 logger.exception(
                     "IntegrityError while provisioning domain %s", domain_name
                 )
+                capture_exception(exc)
                 errors.append(
                     {
                         "domain": domain_name,
                         "error": "Failed to provision domain.",
                     }
                 )

Based on learnings: "Capture and report exceptions to Sentry; use capture_exception() for custom errors"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/api/viewsets/provisioning.py` around lines 54 - 63, The
IntegrityError exception handler in the provisioning flow currently logs via
logger.exception and appends to errors but does not report to Sentry; update the
except IntegrityError block (the handler that references logger.exception,
domain_name and errors) to call Sentry's capture_exception (e.g.,
sentry_sdk.capture_exception) with the caught exception and include context
(domain_name as a tag or extra) so the error is recorded in Sentry for
production visibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/backend/core/api/viewsets/provisioning.py`:
- Around line 54-63: The IntegrityError exception handler in the provisioning
flow currently logs via logger.exception and appends to errors but does not
report to Sentry; update the except IntegrityError block (the handler that
references logger.exception, domain_name and errors) to call Sentry's
capture_exception (e.g., sentry_sdk.capture_exception) with the caught exception
and include context (domain_name as a tag or extra) so the error is recorded in
Sentry for production visibility.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 605e08c and 706368c.

📒 Files selected for processing (2)
  • src/backend/core/api/openapi.json
  • src/backend/core/api/viewsets/provisioning.py

@sylvinus sylvinus merged commit d153f5a into main Mar 3, 2026
13 checks passed
jbpenrath added a commit that referenced this pull request Mar 5, 2026
Update all version files and changelog for minor release.
Added
- Store thread read state per thread access #575
  ⚠️ This migration requires a search reindex to be run after the upgrade.
- Store and display the user who sent a message #574
- Display selected threads count in right panel #576
- Add skip navigation link for keyboard users #573
- Add DeployCenter backend for syncing maildomain admins #572
- Add management command to print all users of the instance

Changed
- Bump keycloak to 26.5.4 #571
- Add migrations-check Makefile command

Fixed
- Preserve scroll position across renders #578
- Convert newlines to `<br>` in styled text #577
- Scope labels and user_role to the requested mailbox
@jbpenrath jbpenrath mentioned this pull request Mar 5, 2026
jbpenrath added a commit that referenced this pull request Mar 5, 2026
Update all version files and changelog for minor release.
Added
- Store thread read state per thread access #575
  ⚠️ This migration requires a search reindex to be run after the upgrade.
- Store and display the user who sent a message #574
- Display selected threads count in right panel #576
- Add skip navigation link for keyboard users #573
- Add DeployCenter backend for syncing maildomain admins #572
- Add management command to print all users of the instance

Changed
- Bump keycloak to 26.5.4 #571
- Add migrations-check Makefile command

Fixed
- Preserve scroll position across renders #578
- Convert newlines to `<br>` in styled text #577
- Scope labels and user_role to the requested mailbox
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants