Themelio compatibility reference. This page describes the retained Themelio implementation or its historical design. Check the parallel-operation guide before applying it to the current platform.
DD-0005: AAI → Waldur User Sync — Implementation
Related ADR: ADR-0005: AAI → Waldur User Synchronization
Status: Draft
Date: 2026-05-19
Authors: TBD
Overview
On every AAI login, Themelio's NextAuth signIn callback (in services/web/src/auth.ts) creates or updates the user in Waldur via the idempotent Identity Bridge using a Waldur staff token. Waldur project invitation acceptance is handled separately: users arrive via a dedicated invite link (/?waldur_invite=true), which routes them through login and then to a dedicated API route that fetches and accepts their pending invitations. Failures in either flow never block login.
Goals and Non-Goals
Goals
- Propagate AAI user attributes to Waldur idempotently on every login via the Identity Bridge API.
- Auto-accept pending Waldur project invitations when the user arrives via an invite link (
/?waldur_invite=true), including when they need to log in first. - Record Waldur sync failures durably in Valkey and expose them via an admin endpoint for manual remediation.
- Keep Waldur entirely invisible to end users — they never log into Waldur directly.
Non-Goals
- Periodic background sync — no CronJob or polling service.
- User deactivation or deletion propagation to Waldur (v1 out of scope).
- Replacing the manual Waldur admin step of creating projects and invitations.
- Terms & Conditions acceptance (tracked separately in ADR-0005 Q10).
- Invitation email customization (tracked in ADR-0005 Q9).
Design
1. Environment Configuration
New variables added to services/web/.env.example and to the Kubernetes Secret/themelio-web-env in each environment overlay:
If WALDUR_API_URL is unset, the entire Waldur sync block is skipped at runtime. When WALDUR_API_URL is set, WALDUR_STAFF_TOKEN and WALDUR_IDENTITY_SOURCE are required. This keeps the feature inert in local development until Waldur credentials are configured.
2. Waldur Utility Module
New file: services/web/src/lib/waldur.ts
syncUserToWaldur(user) — calls POST /api/identity-bridge/ with source set from WALDUR_IDENTITY_SOURCE. Throws on non-2xx responses.
acceptPendingInvitations(email) — calls GET /api/user-invitations/?email_exact={email}&state=pending&page_size=200, follows next pagination links to collect all pages, then calls POST /api/user-invitations/{uuid}/accept/ for each result. Throws on non-2xx responses. Called from the invite-link API route (section 4), not from the signIn callback. This accept call must be validated for the server-side auto-accept flow because Waldur documents it as accepting invitations for the currently authenticated token user.
writeWaldurSyncFailure({ email, username, step, invitationUuid?, error }) — serialises a failure record and writes it to Valkey (see section 7).
3. NextAuth signIn Callback Changes
File: services/web/src/auth.ts
The existing signIn callback (line 68) wraps GitLab provisioning in a try/catch that returns false on error, blocking login. Waldur sync uses a separate inner try/catch so failures are recorded but never prevent login.
The following block is inserted after await setSessionCookie(...) (line 88) and before return true. It handles only Identity Bridge — invitation acceptance is handled separately via the invite-link route (see section 4):
OIDC scopes already include schac_home_organization (see auth.ts line 42).
4. Invite Link Flow
Waldur project invitations are accepted via an explicit invite link rather than on every login. This avoids unnecessary Waldur API calls for users who have no pending invitations and makes the trigger explicit.
URL Entry Point
A Waldur admin sends the user a link to https://<themelio-domain>/?waldur_invite=true. The user may or may not already be logged in.
Flow
Files Changed
services/web/src/app/(public)/page.tsx (server component)
- Read
searchParams.waldur_invite - If
'true'andauth()returns a session →redirect('/api/waldur/accept-invitations') - Otherwise pass
waldurInvite: booleanprop toSignInButtons
services/web/src/components/SignInButtons.tsx
- Accept optional prop
waldurInvite?: boolean - When
true, callsignIn('openid_connect', { callbackUrl: '/api/waldur/accept-invitations' })instead of plainsignIn('openid_connect')
services/web/src/app/api/waldur/accept-invitations/route.ts (new file)
GEThandler- Call
auth()— if no session,redirect('/?waldur_invite=true') try { await acceptPendingInvitations(session.user.email) }catch (err) { await writeWaldurSyncFailure({ email, step: 'fetch-invitations', error: err }) }— never rethrow- Always:
redirect('/dashboard')
5. Attribute Mapping
OIDC claim → Waldur Identity Bridge field:
6. API Call Specifications
Identity Manager Setup
The WALDUR_STAFF_TOKEN used by Themelio is generated by waldur-offering and stored in Secret/themelio-waldur-credentials. It belongs to the waldur-site-agent automation user, which is configured as staff, an identity manager, and an offering manager. For Identity Bridge calls to succeed, that user must have is_identity_manager: true and the AAI source listed in managed_isds.
Configure this with a Waldur admin token:
Use the returned uuid to patch the user:
The exact managed_isds value must match the identity source configured in the target Waldur environment.
Identity Bridge
Expected: 200 OK (existing user updated) or 201 Created (new user). The response body is not used. Throws on any other status.
Fetch Pending Invitations
Expected: 200 OK with paginated response:
Iterate next until null. Collect all uuid values. Returns empty list when no pending invitations exist.
Accept Invitation
Expected: 200 OK if server-side auto-accept is supported for the staff token flow. This endpoint must be validated because Waldur documents it as accepting the invitation for the currently authenticated token user. On any non-2xx: throw and write failure record for this uuid.
7. Error Handling and Failure Records
Waldur sync failures are never re-raised to signIn. They are recorded in Valkey and emitted as structured log lines for the K8s log aggregator.
Valkey key: waldur:sync:failed:{email} — overwrites the previous record if the same user fails again. TTL: 30 days.
Failure record shape:
For accept-invitation failures, invitationUuid is populated with the affected invitation's UUID. A separate Valkey key waldur:sync:failed:{email}:{uuid} is used so multiple invitation failures for the same user are not overwritten.
Per-step behaviour:
Structured log format (all errors):
8. Invitation Email Template Customisation
Waldur's default invitation email contains a link that points to Waldur's own UI. Users who click it would land on the wrong product. To close this gap, the Waldur invitation email template must be overridden to replace that link with the Themelio invite URL.
Target link format
When the user clicks this link they enter the invite flow described in section 4: if already logged in they are sent directly to /api/waldur/accept-invitations; if not, they see the sign-in page and the callbackUrl carries them back to invitation acceptance after Keycloak login.
How to override the template
Waldur supports custom email templates via the Django constance configuration or by overriding the template file on the server, depending on the deployment method. The invitation email template key is typically invitation_created (or equivalent in the target Waldur version — confirm with the Waldur admin). The relevant placeholder that renders the accept link must be replaced with the static Themelio URL above.
This is a one-time configuration step performed by the Waldur instance administrator. It is not automated by Themelio.
New environment variable
No new code changes in Themelio are required for this section — it is purely a Waldur-side configuration task.
9. Admin Failures Endpoint
New Next.js route: GET /api/admin/waldur-sync-failures
- Requires an admin session (server-side check — admin role must be defined; details TBD with auth design).
- Reads all
waldur:sync:failed:*keys from Valkey usingSCAN. - Returns the list of failure records as JSON for manual operator review and remediation.
Operators can use this endpoint to identify users who require manual Waldur provisioning when automated sync fails.
Testing Strategy
- Unit tests (
services/web/src/lib/waldur.test.ts): mockfetchand Valkey. Cover: successful Identity Bridge with requiredsource, invitation listing viauser-invitations, invitation acceptance, Waldur 503, invitation 404 (skipped),writeWaldurSyncFailure. - Integration smoke test: use MSW (Mock Service Worker) to intercept Waldur API calls. Trigger a full
signIncallback execution. Assert: Identity Bridge runs on every login; failure record written to Valkey when Waldur returns 503. - Invite link tests: visit
/?waldur_invite=truewhile logged out → sign in → browser reaches/api/waldur/accept-invitations→ pending invitations accepted → redirected to/dashboard. Visit/?waldur_invite=truewhile already logged in → skips login, goes straight to/api/waldur/accept-invitations→/dashboard. Simulate Waldur 503 on the invite route → user still lands on/dashboard, failure record written, login not blocked. - Acceptance criteria: A user who logs in for the first time appears in Waldur (verified via Waldur admin UI or
GET /api/users/?email=...). A user who arrives via/?waldur_invite=truehas their pending invitation accepted (verified viaGET /api/user-invitations/{uuid}/showingstate: accepted).
Deployment & Rollout
- Add
WALDUR_API_URL,WALDUR_STAFF_TOKEN, andWALDUR_IDENTITY_SOURCEto the Kubernetes Secret references forservices/webin each environment overlay (dev,staging,prod). In Ansible,WALDUR_STAFF_TOKENis generated bywaldur-offeringand stored inSecret/themelio-waldur-credentials. - Provision a Waldur staff account with identity manager role and the AAI source registered in
managed_isdson the target Waldur instance (blocks on ADR-0005 Q3).WALDUR_IDENTITY_SOURCEand the user'smanaged_isdsentry must match. - The feature is fully inert when
WALDUR_API_URLis unset, so deployment is safe before Waldur credentials exist. - No database migrations. No new Kubernetes resources beyond the updated Secret.
Open Questions
-
Q3 (from ADR-0005): Waldur identity manager setup — the Waldur staff account must be provisioned with the identity manager role and the AAI source in
managed_isdsbefore any Identity Bridge calls will succeed. Blocks production deployment. -
Q9 (from ADR-0005): Invitation email customization — Waldur's default invitation email links to Waldur's own UI. Until this is resolved, users who receive an invitation email may land on the wrong page before logging into Themelio.Resolved: The Waldur invitation email template will be overridden by the Waldur instance administrator to replace the accept link withhttps://<themelio-domain>/?waldur_invite=true. See section 8. -
Q10 (from ADR-0005): ToC acceptance — where Terms & Conditions acceptance is recorded relative to this callback is not yet designed. If ToC must happen before Waldur sync, the guard logic in the
signIncallback needs a corresponding check.
References
- Waldur Identity Bridge API
- Waldur Invitations API
- NextAuth v5 — signIn callback
services/web/src/auth.ts— existing signIn callback with GitLab provisioning patternservices/web/src/auth-helpers.ts—ensureInitializedUser,createImpersonationToken
Page source
Source snapshot · Revision f928834c8.