binarybeachio: trusted view — key User on bb_mailbox (four-layer identity model)
Identity-model rollout T2.4. Trusted view now derives `lookup_email = bb_mailbox or email` and uses it for both User.objects.filter() and the new-User row's email field. WARN-log fallback to federation email when the claim is absent (transitional safety; should never fire once Zitadel `bb-claims` Action + bridge-side userinfo enrichment are live). Decode-time required-claims unchanged (`bb_mailbox` stays optional) so partial deploys aren't bricked. Pre-migration SQL rename of operator's existing User row required — see binarybeachio docs/services/plane/migration-plan.md §9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c0cfbb2bdc
commit
69b499c9ec
2 changed files with 39 additions and 6 deletions
|
|
@ -159,7 +159,14 @@ def _verify_with_retry(token: str, public_key_url: str) -> dict:
|
|||
"""Verify the JWT, refetching the bridge key once on signature failure to
|
||||
transparently handle bridge key rotation. Other verify failures (expired,
|
||||
wrong issuer/audience, malformed) do NOT trigger a refetch — those are
|
||||
tampering or clock issues, not key drift."""
|
||||
tampering or clock issues, not key drift.
|
||||
|
||||
`bb_mailbox` is intentionally NOT in the required-claims list. The bridge
|
||||
only emits it when Zitadel's `bb-claims` Action has propagated and the
|
||||
tenant's `mail_domain` org metadata is set; absent the claim, the view
|
||||
falls back to `email` for User keying so a partial deployment (bridge
|
||||
upgraded but Action not yet live) still works. See the four-layer model
|
||||
in `binarybeachio/docs/architecture/multi-tenant-identity.md` §4."""
|
||||
pem = _fetch_bridge_public_key(public_key_url)
|
||||
try:
|
||||
return pyjwt.decode(
|
||||
|
|
@ -233,18 +240,42 @@ class TrustedSignInEndpoint(View):
|
|||
if not email:
|
||||
return _redirect_with_error(request, "TRUSTED_JWT_TOKEN_INVALID", "TRUSTED_JWT_TOKEN_NO_EMAIL", next_path)
|
||||
|
||||
# Identity-model rollout (multi-tenant-identity.md §4): prefer
|
||||
# `bb_mailbox` over `email` for User keying. `email` carries the
|
||||
# federation address (e.g., the user's Google login) — it's useful
|
||||
# for auditing but it is NOT the canonical per-tenant identity. The
|
||||
# canonical identity is `bb_mailbox` = `<bb_username>@<bb_mail_domain>`,
|
||||
# the address Stalwart actually hosts and the address the operator
|
||||
# invites users at.
|
||||
#
|
||||
# If `bb_mailbox` is absent, fall back to `email` so a transitional
|
||||
# deployment (bridge already upgraded but Zitadel `bb-claims` Action
|
||||
# not yet propagated) keeps working. The WARN log is the operator's
|
||||
# signal that the chain is still incomplete.
|
||||
bb_mailbox = (claims.get("bb_mailbox") or "").strip().lower()
|
||||
if bb_mailbox:
|
||||
lookup_email = bb_mailbox
|
||||
else:
|
||||
log.warning(
|
||||
"trusted-jwt missing bb_mailbox claim; falling back to federation email — "
|
||||
"verify Zitadel `bb-claims` Action is published and the tenant's "
|
||||
"`mail_domain` org metadata is set",
|
||||
extra={"sub": claims.get("sub"), "email": email, "tenant": claims.get("tenant")},
|
||||
)
|
||||
lookup_email = email
|
||||
|
||||
# Find-or-create. We mirror the User-creation shape that
|
||||
# OauthAdapter.complete_login_or_signup() produces (apps/api/plane/
|
||||
# authentication/adapter/base.py:289-342) — same field set, same
|
||||
# required side-effect of creating a Profile row. Skipping the Profile
|
||||
# would cause Plane's SPA /api/users/me/profile/ to 404 and bounce
|
||||
# the user back to /login in an onboarding loop.
|
||||
user = User.objects.filter(email=email).first()
|
||||
user = User.objects.filter(email=lookup_email).first()
|
||||
created = user is None
|
||||
if created:
|
||||
with transaction.atomic():
|
||||
user = User(
|
||||
email=email,
|
||||
email=lookup_email,
|
||||
username=uuid.uuid4().hex,
|
||||
first_name=claims.get("first_name") or claims.get("given_name") or "",
|
||||
last_name=claims.get("last_name") or claims.get("family_name") or "",
|
||||
|
|
@ -278,6 +309,8 @@ class TrustedSignInEndpoint(View):
|
|||
"jti": claims.get("jti"),
|
||||
"sub": claims.get("sub"),
|
||||
"email": email,
|
||||
"lookup_email": lookup_email,
|
||||
"bb_mailbox_present": bool(bb_mailbox),
|
||||
"tenant": claims.get("tenant"),
|
||||
"is_signup": created,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue