Skip to content

✨(invites) enable inviting users that haven't logged in yet#644

Merged
sylvinus merged 2 commits intomainfrom
invites
Apr 30, 2026
Merged

✨(invites) enable inviting users that haven't logged in yet#644
sylvinus merged 2 commits intomainfrom
invites

Conversation

@sylvinus
Copy link
Copy Markdown
Member

@sylvinus sylvinus commented Apr 25, 2026

No invitation email for now

Summary by CodeRabbit

  • New Features

    • Admins can create passwordless user accounts via a new admin UI flow.
    • API will auto-create stub user accounts when provisioning access for previously unknown email addresses.
    • OIDC authentication can claim/merge pre-created stub users into real accounts on first login.
  • Tests

    • Added tests covering admin passwordless creation, API auto-created stub behavior, and OIDC claiming/merge scenarios.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6bb3b5b7-b7d6-4f24-9558-cfb1555ec659

📥 Commits

Reviewing files that changed from the base of the PR and between 37c0c90 and 50f9b91.

📒 Files selected for processing (1)
  • src/backend/core/admin.py

📝 Walkthrough

Walkthrough

Adds admin and API paths to create “passwordless” stub users (email-only, unusable password, null sub), updates serializers to auto-create such stubs on unknown emails, and modifies user lookup to claim/merge stubs on OIDC login.

Changes

Cohort / File(s) Summary
Admin Passwordless User Creation
src/backend/core/admin.py, src/backend/core/templates/admin/core/user/add_passwordless.html, src/backend/core/templates/admin/core/user/change_list.html
Adds PasswordlessUserForm, a UserAdmin route/view at add-passwordless/ that creates users with unusable passwords if email is new, and templates to surface the action and form in the admin UI.
API Stub User Auto-Creation
src/backend/core/api/serializers.py
Replaces UserField with UserAccessWriteField which accepts email strings and auto-creates stub User objects (null sub, unusable password) when no matching user exists.
User Lookup & OIDC Stub Claiming
src/backend/core/models.py
UserManager.get_user_by_sub_or_email now checks for users with the given email and sub IS NULL (stubs) before applying email-fallback logic.
Tests for Passwordless Flow
src/backend/core/tests/api/test_mailbox_access.py, src/backend/core/tests/api/test_maildomain_access.py, src/backend/core/tests/authentication/test_backends.py
Adds tests asserting that posting unknown emails creates stub users and access records, and that OIDC authentication claims/merges stub users (preserves PK, sets sub, retains related access rows).

Sequence Diagram(s)

sequenceDiagram
    actor Admin
    participant AdminUI as Admin Interface
    participant Backend as Backend
    participant DB as Database

    Admin->>AdminUI: Open user changelist
    AdminUI->>Backend: GET changelist
    Backend->>DB: Query users
    DB-->>Backend: Users list
    Backend-->>AdminUI: Render changelist (add "Add passwordless")
    Admin->>AdminUI: Click "Add passwordless"
    AdminUI->>Backend: GET add-passwordless form
    Backend-->>AdminUI: Render form
    Admin->>AdminUI: Submit email (POST)
    AdminUI->>Backend: POST email
    Backend->>DB: Check email uniqueness
    DB-->>Backend: Not found
    Backend->>DB: Create User (unusable password, sub=NULL)
    DB-->>Backend: User created
    Backend-->>AdminUI: Redirect to changelist with message
Loading
sequenceDiagram
    participant Client
    participant API as API Endpoint
    participant Serializer
    participant DB as Database
    participant OIDC as OIDC Backend

    Client->>API: POST access request (user=email@example.com)
    API->>Serializer: Validate user field (UserAccessWriteField)
    Serializer->>DB: Query User by email
    DB-->>Serializer: Not found
    Serializer->>DB: Create stub User (sub=NULL, unusable pwd)
    DB-->>Serializer: Stub created
    Serializer-->>API: Validated data
    API->>DB: Create access record linked to stub
    DB-->>API: Access created
    API-->>Client: 201 Created

    Client->>OIDC: OIDC login with same email (payload has sub)
    OIDC->>DB: Query by sub
    DB-->>OIDC: Not found
    OIDC->>DB: Query by email AND sub IS NULL
    DB-->>OIDC: Stub user found
    OIDC->>DB: Update user.sub with payload.sub
    DB-->>OIDC: User updated (stub claimed)
    OIDC-->>Client: Authentication successful
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: enabling invitations to users who haven't logged in yet, which is the core feature across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

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: 3

🤖 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/admin.py`:
- Around line 242-271: The add_passwordless_view currently allows access without
enforcing model-level add rights; at the top of add_passwordless_view check
self.has_add_permission(request) and if it returns False deny access (e.g.,
raise django.core.exceptions.PermissionDenied or return HttpResponseForbidden)
before processing POST or rendering the form so only users with add permission
on the User model can create passwordless users; this uses the existing
add_passwordless_view method and self.has_add_permission to gate the endpoint.

In `@src/backend/core/api/serializers.py`:
- Around line 1098-1104: The current lookup uses
models.User.objects.filter(email=data).first() which silently picks one record
when multiple users share the same email; change the logic to explicitly detect
ambiguous matches: query = models.User.objects.filter(email=data), if
query.count() > 1 raise a validation/error (e.g., serializers.ValidationError or
appropriate API error) requiring a UUID instead of an email, if query.count() ==
1 return that user, and only create a new user (user = models.User(email=data);
set_unusable_password(); user.save()) when query.count() == 0; ensure the error
path prevents granting access when email is ambiguous.

In `@src/backend/core/models.py`:
- Around line 123-126: The call to self.get(email=email, sub__isnull=True) can
raise self.model.MultipleObjectsReturned for duplicate stub-email rows; modify
the try/except to also catch self.model.MultipleObjectsReturned and handle it by
selecting a single canonical row (e.g., use self.filter(email=email,
sub__isnull=True).order_by('pk').first()) or otherwise resolve/log and return
that single instance (or None) instead of letting the exception bubble; update
the exception handler around the self.get(...) call to perform this safe
fallback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0206867a-1999-442c-8dfa-b7d6cfafb2d7

📥 Commits

Reviewing files that changed from the base of the PR and between 7a0f6fb and 37c0c90.

📒 Files selected for processing (8)
  • src/backend/core/admin.py
  • src/backend/core/api/serializers.py
  • src/backend/core/models.py
  • src/backend/core/templates/admin/core/user/add_passwordless.html
  • src/backend/core/templates/admin/core/user/change_list.html
  • src/backend/core/tests/api/test_mailbox_access.py
  • src/backend/core/tests/api/test_maildomain_access.py
  • src/backend/core/tests/authentication/test_backends.py

Comment thread src/backend/core/admin.py
Comment thread src/backend/core/api/serializers.py
Comment thread src/backend/core/models.py
@sylvinus sylvinus merged commit 99caf32 into main Apr 30, 2026
11 of 12 checks passed
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.

1 participant