Skip to content

Add Discord login link flow and bootstrap auth toggle#110

Merged
michaelmwu merged 6 commits into
mainfrom
michaelmwu/oidc-discord-admin
Mar 6, 2026
Merged

Add Discord login link flow and bootstrap auth toggle#110
michaelmwu merged 6 commits into
mainfrom
michaelmwu/oidc-discord-admin

Conversation

@michaelmwu
Copy link
Copy Markdown
Member

@michaelmwu michaelmwu commented Mar 3, 2026

Description

Adds a new Discord /login slash command that mints one-time dashboard links via POST /auth/discord/links and returns them ephemerally.
Updates auth defaults to OIDC_ADMIN_GROUPS=authentik Admins and DISCORD_ADMIN_ROLES=Admin,Owner.
Introduces DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS (default true) so Discord deep-link logins can optionally skip OIDC admin-group and OIDC-email linkage checks during SSO bootstrap.
Updates API/worker/bot docs and adds unit tests for the new cog, settings defaults, and Discord-link bootstrap auth behavior.

Related Issue

N/A

How Has This Been Tested?

uv run pytest -q tests/unit/test_backend_api.py tests/unit/test_worker_config.py tests/unit/test_admin_login_cog.py

Summary by CodeRabbit

  • New Features

    • Added /login Discord command to generate one-time admin dashboard login links for quick access to the admin dashboard.
  • Documentation

    • Added Discord deep-link identity policy documentation explaining admin access controls and identity verification configuration options.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 3, 2026

Warning

Rate limit exceeded

@michaelmwu has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 38 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af4fac64-79b8-4ccb-a9a3-ae8c5e639e6c

📥 Commits

Reviewing files that changed from the base of the PR and between ff7b0dd and b030022.

📒 Files selected for processing (6)
  • .env.example
  • apps/discord_bot/README.md
  • apps/discord_bot/src/five08/discord_bot/config.py
  • apps/worker/README.md
  • apps/worker/src/five08/worker/config.py
  • tests/unit/test_worker_config.py
📝 Walkthrough

Walkthrough

This PR introduces Discord admin dashboard access via a new /login slash command that generates one-time ephemeral links. It adds session-level actor_provider tracking (distinguishing OIDC vs Discord authentication sources), implements conditional OIDC identity checks for Discord deep-link logins controlled by DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS, and updates admin group/role default configurations.

Changes

Cohort / File(s) Summary
Configuration & Environment
.env.example, apps/worker/src/five08/worker/config.py, apps/discord_bot/src/five08/discord_bot/config.py
Updated default OIDC_ADMIN_GROUPS to "authentik Admins" and DISCORD_ADMIN_ROLES to "Admin,Owner"; added DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS boolean flag (default true) and discord_admin_role_names property for parsed role configuration.
Documentation
apps/api/README.md, apps/discord_bot/README.md, apps/worker/README.md
Added "Discord deep-link identity policy" section describing admin role controls and OIDC identity check toggle behavior.
Backend Auth Core
apps/api/src/five08/backend/auth.py
Added actor_provider field to AuthSession; introduced DiscordAdminIdentity dataclass; refactored admin verification with new resolve_admin_identity flow and helpers (_get_active_person_record, _has_admin_role, _email_matches_person) for enhanced CRM-backed admin checks.
Backend API Handlers
apps/api/src/five08/backend/api.py
Added _session_actor_provider helper; extended session payloads with actor_provider field; enriched audit events with actor_provider, is_admin, groups, via_discord_link, and discord_link_identity_checks_enforced metadata; added conditional OIDC identity check enforcement in auth_callback_handler and Discord session creation in auth_discord_link_redirect_handler.
Discord Bot Implementation
apps/discord_bot/src/five08/discord_bot/cogs/admin_login.py
Introduced AdminLoginCog providing /login slash command to generate one-time admin dashboard links via backend API call; includes permission checks against discord_admin_roles, error handling (missing secret, permission denied), and centralized audit logging.
Tests
tests/unit/test_admin_login_cog.py, tests/unit/test_backend_api.py, tests/unit/test_worker_config.py
Added comprehensive tests for AdminLoginCog login flows (success, permission denied, missing secret, role config); added backend API tests for Discord link identity check bypass scenarios; added config defaults verification tests.

Sequence Diagram(s)

sequenceDiagram
    actor User as User (Discord)
    participant Bot as Discord Bot
    participant API as Backend API
    participant DB as Person CRM Database
    participant Redis as Session Store

    User->>Bot: /login command
    Bot->>Bot: Check user has admin role
    Bot->>API: POST /auth/discord/links
    API->>DB: resolve_admin_identity(discord_user_id)
    DB-->>API: Return active person record
    alt Identity Checks Disabled (DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS=false)
        API->>Redis: Create/save Discord-backed session
        API->>API: Emit audit event (via_discord_link=true)
    else Identity Checks Enabled (default)
        API->>API: Validate OIDC group membership
        API->>API: Validate email claim
        API->>DB: Verify linked Discord user
    end
    API-->>Bot: Return one-time link_url + expires_in_seconds
    Bot->>Bot: Audit login (success/denied/error)
    Bot-->>User: Ephemeral response with link (or error)
Loading
sequenceDiagram
    actor User as User (Dashboard)
    participant API as Backend API
    participant AuthStore as Redis Session Store
    participant Audit as Audit Logger

    User->>API: Authenticate (OIDC callback or Discord deep-link)
    API->>AuthStore: Save session with actor_provider
    alt Source is OIDC
        AuthStore->>AuthStore: actor_provider="admin_sso"
    else Source is Discord
        AuthStore->>AuthStore: actor_provider="discord"
    end
    Audit->>Audit: Record login with actor_provider, is_admin, groups
    User->>API: GET /auth/me
    API->>AuthStore: Load session
    AuthStore-->>API: Return session (with actor_provider)
    API-->>User: Return auth response (includes actor_provider)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 A slash command hops into view,
/login brings links fresh and new,
Discord deep-links dance with care,
Actor providers tracked everywhere,
Sessions know where they came through! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the two main additions: a Discord login link flow (the /login command and POST /auth/discord/links endpoint) and a bootstrap auth toggle (DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS setting that allows skipping OIDC checks).

✏️ 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 michaelmwu/oidc-discord-admin

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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a Discord /login flow for minting one-time dashboard links and introduces a bootstrap toggle to optionally relax Discord-link OIDC identity checks during admin SSO onboarding.

Changes:

  • Add Discord bot /login slash command + unit tests to mint one-time dashboard login links via POST /auth/discord/links.
  • Update auth/admin defaults (OIDC_ADMIN_GROUPS, DISCORD_ADMIN_ROLES) and add DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS (default true) to control Discord-link identity enforcement.
  • Update API/worker/bot documentation and expand unit test coverage for the new auth behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/unit/test_worker_config.py Adds assertions for updated default admin group/role settings.
tests/unit/test_backend_api.py Adds tests covering Discord-link bootstrap behavior when identity checks are disabled.
tests/unit/test_admin_login_cog.py Introduces unit tests for the new Discord /login cog behavior.
apps/worker/src/five08/worker/config.py Updates defaults and adds discord_link_require_oidc_identity_checks.
apps/worker/README.md Documents Discord deep-link identity policy and bootstrap toggle.
apps/discord_bot/src/five08/discord_bot/cogs/admin_login.py Implements /login command that calls backend link-mint endpoint and returns ephemeral link.
apps/discord_bot/README.md Documents the new /login slash command.
apps/api/src/five08/backend/api.py Adds bootstrap toggle behavior to Discord-link callback + redirect handlers and audit metadata.
apps/api/README.md Documents Discord deep-link identity policy and bootstrap toggle.
.env.example Updates defaults and adds the new DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS variable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

return JSONResponse({"error": "link_not_found"}, status_code=404)

_, session = await _current_session(request)
if session is not None:
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

In bootstrap mode (DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS=false), this handler will short-circuit when a session already exists, delete the link token, and redirect without forcing a fresh OIDC login. That means (a) a non-admin session won’t be upgraded to an admin session via the link, and (b) any existing session that gets the token can burn the one-time link (DoS) without proving identity. Consider always redirecting to /auth/login?...discord_link_token=... when the flag is disabled (or at least when session.is_admin is false), so the deep link reliably results in an admin session being minted via the callback path.

Suggested change
if session is not None:
if session is not None:
# In bootstrap mode (identity checks disabled), always route through the
# OIDC login flow so the deep link can mint/upgrade an admin session via
# the callback path, without burning the one-time token here.
if not settings.discord_link_require_oidc_identity_checks:
login_query = urlencode(
{"next": grant.next_path, "discord_link_token": token}
)
return RedirectResponse(
url=f"/auth/login?{login_query}", status_code=302
)

Copilot uses AI. Check for mistakes.
Comment thread apps/api/src/five08/backend/api.py Outdated
Comment on lines 1384 to 1390
"is_admin": is_admin,
"groups": groups,
"via_discord_link": bool(pending.discord_link_token),
"discord_link_identity_checks_enforced": (
enforce_discord_link_identity_checks
),
},
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

discord_link_identity_checks_enforced is being added to the login audit metadata even when via_discord_link is false. This can make the audit stream harder to interpret since the flag only applies to the Discord-link path. Consider only including this metadata key when pending.discord_link_token is set (or nesting it under a discord_link metadata object).

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 6, 2026 15:21
…rd-admin

# Conflicts:
#	tests/unit/test_worker_config.py
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

apps/worker/README.md:100

  • The bootstrap-mode description here is inaccurate/incomplete. When DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS=false, /auth/discord/link/{token} can create a Discord-backed admin session directly (no OIDC roundtrip), not just “skip checks after successful OIDC authentication”. Please update this bullet to match the actual handler behavior (and align with the API README wording).

- `GET /health`: Redis/Postgres/worker health check.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .env.example Outdated
Comment on lines +84 to +85
# Temporary bootstrap switch: when false, Discord deep-link logins skip OIDC
# admin-group and OIDC-email-to-Discord-link checks after successful OIDC auth.
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This comment suggests bootstrap mode only skips checks “after successful OIDC auth”, but the API implementation also supports creating a Discord-backed session directly from the deep link when DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS=false (no OIDC roundtrip). Please adjust the wording so operators don’t misconfigure expectations about whether OIDC is required in bootstrap mode.

Suggested change
# Temporary bootstrap switch: when false, Discord deep-link logins skip OIDC
# admin-group and OIDC-email-to-Discord-link checks after successful OIDC auth.
# Temporary bootstrap switch: when false, Discord deep-link logins do not require
# an OIDC roundtrip and skip OIDC-based admin-group and email-to-Discord-link checks.

Copilot uses AI. Check for mistakes.
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 (4)
apps/discord_bot/README.md (1)

21-26: Consider adding a Prerequisites section to match the style of /create-mailbox.

The /login command relies on API_SHARED_SECRET being configured (as shown in the backend request headers), but this prerequisite isn't explicitly documented. For consistency with the /create-mailbox command documentation (lines 28-34), consider adding a Prerequisites line similar to:

- Prerequisites: `API_SHARED_SECRET` must be configured (configured in env; command will fail if missing).

Additionally, you might consider specifying the expiry duration (e.g., "Returns an ephemeral one-time URL that expires in X seconds") or referencing the DISCORD_LINK_TTL_SECONDS configuration setting for clarity.

📝 Example documentation enhancement
 - `/login`
   - Description: Generate a one-time admin dashboard login link.
+  - Prerequisites: `API_SHARED_SECRET` must be configured (configured in env; command will fail if missing).
   - Required role: any role listed in `DISCORD_ADMIN_ROLES` (`Admin,Owner` by default).
   - Behavior:
     - Calls backend `POST /auth/discord/links` using `API_SHARED_SECRET`.
-    - Returns an ephemeral one-time URL with expiry.
+    - Returns an ephemeral one-time URL that expires after the configured `DISCORD_LINK_TTL_SECONDS`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/discord_bot/README.md` around lines 21 - 26, Add a "Prerequisites" line
to the `/login` command docs stating that API_SHARED_SECRET must be configured
in the environment (the backend call in `/login` uses this header), and also
mention the link expiry by referencing DISCORD_LINK_TTL_SECONDS (e.g., "Returns
an ephemeral one-time URL that expires per DISCORD_LINK_TTL_SECONDS"); update
the `/login` section where the command is described to mirror the style of
`/create-mailbox` and include these two items.
.env.example (1)

64-64: Consider quoting value containing spaces.

The value authentik Admins contains a space. While most .env parsers handle this correctly, quoting the value would make the format more explicit and avoid potential parsing issues with certain tools.

-OIDC_ADMIN_GROUPS=authentik Admins
+OIDC_ADMIN_GROUPS="authentik Admins"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example at line 64, The OIDC_ADMIN_GROUPS example contains an unquoted
value with a space; update the .env.example so the OIDC_ADMIN_GROUPS variable is
quoted (e.g. OIDC_ADMIN_GROUPS="authentik Admins") to make parsing explicit and
avoid issues with tools that split on whitespace—edit the OIDC_ADMIN_GROUPS line
to wrap the value in quotes while preserving the example semantics.
tests/unit/test_backend_api.py (1)

866-893: Consider adding actor_provider to the session fixture.

The existing test_auth_logout_writes_logout_audit constructs an AuthSession without actor_provider (lines 869-877). This works if AuthSession has a default value, but it doesn't exercise the logout audit path for Discord-authenticated sessions.

Consider adding a complementary test case for logout with actor_provider=ActorProvider.DISCORD.value to verify the actor_subject is correctly set to the session's subject (not email) for Discord sessions.

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

In `@tests/unit/test_backend_api.py` around lines 866 - 893, Update the
test_auth_logout_writes_logout_audit scenario to also cover
Discord-authenticated sessions by creating a complementary test that constructs
an api.AuthSession with actor_provider set to api.ActorProvider.DISCORD.value
(or add actor_provider to the existing session fixture) and assert that
insert_audit_event's payload.actor_subject equals the session.subject (not
email); keep the same setup (patching _current_session, _auth_store_from_app,
and insert_audit_event) and assert response.status_code, audit_payload.action ==
"auth.logout", audit_payload.result == api.AuditResult.SUCCESS, and
audit_payload.actor_subject == session.subject for the Discord case.
tests/unit/test_admin_login_cog.py (1)

90-103: Consider asserting the message content for consistency.

Other tests in this file (lines 69, 87) verify the message content (e.g., "not allowed", "not configured"). This test only verifies that send_message was called but doesn't check what was sent. For consistency and to ensure correct user feedback:

♻️ Suggested enhancement
     with patch.object(cog, "_create_login_link", new=AsyncMock()) as mock_create:
         await cog.login.callback(cog, mock_interaction)
 
     mock_interaction.response.send_message.assert_awaited_once()
+    sent_message = mock_interaction.response.send_message.call_args.args[0]
+    assert "not allowed" in sent_message or "permission" in sent_message.lower()
     mock_create.assert_not_awaited()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test_admin_login_cog.py` around lines 90 - 103, The test currently
checks that cog.login.callback triggered mock_interaction.response.send_message
but not the message content; update
test_login_command_uses_configured_admin_roles to assert the actual message sent
(use mock_interaction.response.send_message.assert_awaited_once_with(...) or
equivalent) and verify it matches the same "not allowed" string used by the
other tests (keep mock_create.assert_not_awaited()). Target the call on
mock_interaction.response.send_message after invoking cog.login.callback to
ensure the user-facing feedback is asserted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.env.example:
- Line 64: The OIDC_ADMIN_GROUPS example contains an unquoted value with a
space; update the .env.example so the OIDC_ADMIN_GROUPS variable is quoted (e.g.
OIDC_ADMIN_GROUPS="authentik Admins") to make parsing explicit and avoid issues
with tools that split on whitespace—edit the OIDC_ADMIN_GROUPS line to wrap the
value in quotes while preserving the example semantics.

In `@apps/discord_bot/README.md`:
- Around line 21-26: Add a "Prerequisites" line to the `/login` command docs
stating that API_SHARED_SECRET must be configured in the environment (the
backend call in `/login` uses this header), and also mention the link expiry by
referencing DISCORD_LINK_TTL_SECONDS (e.g., "Returns an ephemeral one-time URL
that expires per DISCORD_LINK_TTL_SECONDS"); update the `/login` section where
the command is described to mirror the style of `/create-mailbox` and include
these two items.

In `@tests/unit/test_admin_login_cog.py`:
- Around line 90-103: The test currently checks that cog.login.callback
triggered mock_interaction.response.send_message but not the message content;
update test_login_command_uses_configured_admin_roles to assert the actual
message sent (use
mock_interaction.response.send_message.assert_awaited_once_with(...) or
equivalent) and verify it matches the same "not allowed" string used by the
other tests (keep mock_create.assert_not_awaited()). Target the call on
mock_interaction.response.send_message after invoking cog.login.callback to
ensure the user-facing feedback is asserted.

In `@tests/unit/test_backend_api.py`:
- Around line 866-893: Update the test_auth_logout_writes_logout_audit scenario
to also cover Discord-authenticated sessions by creating a complementary test
that constructs an api.AuthSession with actor_provider set to
api.ActorProvider.DISCORD.value (or add actor_provider to the existing session
fixture) and assert that insert_audit_event's payload.actor_subject equals the
session.subject (not email); keep the same setup (patching _current_session,
_auth_store_from_app, and insert_audit_event) and assert response.status_code,
audit_payload.action == "auth.logout", audit_payload.result ==
api.AuditResult.SUCCESS, and audit_payload.actor_subject == session.subject for
the Discord case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0ed16846-901a-48fe-8d63-4945a2067756

📥 Commits

Reviewing files that changed from the base of the PR and between 38ceba6 and ff7b0dd.

📒 Files selected for processing (12)
  • .env.example
  • apps/api/README.md
  • apps/api/src/five08/backend/api.py
  • apps/api/src/five08/backend/auth.py
  • apps/discord_bot/README.md
  • apps/discord_bot/src/five08/discord_bot/cogs/admin_login.py
  • apps/discord_bot/src/five08/discord_bot/config.py
  • apps/worker/README.md
  • apps/worker/src/five08/worker/config.py
  • tests/unit/test_admin_login_cog.py
  • tests/unit/test_backend_api.py
  • tests/unit/test_worker_config.py

@michaelmwu michaelmwu merged commit 258b3bc into main Mar 6, 2026
3 checks passed
@michaelmwu michaelmwu deleted the michaelmwu/oidc-discord-admin branch March 6, 2026 15:32
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.

2 participants