Skip to content

Add UI warning when not linking an OIDC account#587

Merged
Flaminel merged 3 commits into
mainfrom
add_oidc_warning
Apr 27, 2026
Merged

Add UI warning when not linking an OIDC account#587
Flaminel merged 3 commits into
mainfrom
add_oidc_warning

Conversation

@Flaminel

@Flaminel Flaminel commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary by Sourcery

Add a confirmation warning when enabling OIDC without a linked account and strengthen associated documentation and tests.

New Features:

  • Prompt administrators with a confirmation dialog when saving OIDC settings with OIDC enabled and no linked subject.

Documentation:

  • Document the risks of running OIDC in unlinked mode and recommend linking a specific account when using public identity providers.

Tests:

  • Add end-to-end tests covering the OIDC save warning dialog behavior for enabled/disabled states and linked/unlinked subjects.

@Flaminel

Copy link
Copy Markdown
Contributor Author

@greptileai review

@Flaminel

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@greptile-apps

greptile-apps Bot commented Apr 27, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a UX safety warning to the OIDC settings page: when a user attempts to save OIDC enabled with no linked subject, a destructive confirmation dialog is shown explaining the security implications of unlinked mode. Accompanying changes add a documentation warning callout and a comprehensive E2E test suite covering all four conditional paths.

Confidence Score: 4/5

Safe to merge; only P2 findings (docs grammar nit and a test timing antipattern).

All findings are P2. The component logic is correct and follows established patterns; the security warning itself is a net improvement. Minor docs grammar issue and a waitForTimeout antipattern in the E2E test are the only concerns.

docs/docs/configuration/account/index.mdx (grammar fix) and e2e/tests/15-oidc-save-without-link-warning.spec.ts (waitForTimeout).

Important Files Changed

Filename Overview
code/frontend/src/app/features/settings/account/account-settings.component.ts Adds async guard to saveOidcConfig — shows a destructive confirmation dialog when OIDC is enabled with no linked subject. Follows existing confirmService patterns; logic and state management are correct.
docs/docs/configuration/account/index.mdx Adds a Warning callout about unlinked OIDC mode; minor grammatical error: "with public providers open registration" is missing a preposition.
e2e/tests/15-oidc-save-without-link-warning.spec.ts New E2E spec with 5 serial tests covering show/cancel/confirm/disabled/linked-subject scenarios; uses a hardcoded waitForTimeout antipattern in the cancel test.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User clicks Save OIDC Settings] --> B{oidcEnabled?}
    B -- No --> E[Call updateOidcConfig API]
    B -- Yes --> C{oidcAuthorizedSubject empty?}
    C -- No --> E
    C -- Yes --> D[Show destructive confirmation dialog]
    D --> F{User confirms?}
    F -- Cancel --> G[Return — no API call]
    F -- Enable anyway --> E
    E --> H{API response}
    H -- Success --> I[Toast: OIDC settings saved]
    H -- Error --> J[Toast: Failed to save OIDC settings]
Loading

Reviews (1): Last reviewed commit: "added warning when not linking an OIDC a..." | Re-trigger Greptile

Comment on lines 151 to +154

<Warning>
The unlinked mode delegates **all** access decisions to your identity provider. This is appropriate for self-hosted providers where you control every account. It is **dangerous** with public providers open registration. Anyone who can sign up at that provider would be able to sign in to Cleanuparr as the administrator. If you use a public provider, link a specific account.
</Warning>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Missing word in warning text

The sentence "It is dangerous with public providers open registration" is missing a preposition — it should read "with public providers with open registration" (or a similar phrasing) to be grammatically correct.

Comment on lines +100 to +101
await expect(dialog).toBeVisible({ timeout: 5_000 });
await dialog.getByRole('button', { name: 'Cancel' }).click();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Hardcoded waitForTimeout is a Playwright antipattern

page.waitForTimeout(500) is a time-based wait that makes the test brittle and slower. Playwright recommends waiting on a specific observable condition instead — e.g., page.waitForResponse or an assertion on the absence of the toast. The fixed 500 ms delay may be too short on slow CI runners and too long otherwise.

@sourcery-ai

sourcery-ai Bot commented Apr 27, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a confirmation warning when enabling OIDC without a linked subject, documents the risk in the account configuration docs, and introduces Playwright E2E coverage to verify the new warning and its behavior under different OIDC states.

Sequence diagram for saving OIDC config with unlinked account warning

sequenceDiagram
    actor Admin
    participant AccountSettingsComponent
    participant ConfirmService
    participant ApiService

    Admin->>AccountSettingsComponent: clickSaveOidcConfig()
    AccountSettingsComponent->>AccountSettingsComponent: oidcEnabled()
    AccountSettingsComponent->>AccountSettingsComponent: oidcAuthorizedSubject()
    alt OIDC enabled and no subject linked
        AccountSettingsComponent->>ConfirmService: confirm(config)
        ConfirmService-->>AccountSettingsComponent: confirmed
        alt Admin confirms
            AccountSettingsComponent->>AccountSettingsComponent: oidcSaving.set(true)
            AccountSettingsComponent->>ApiService: updateOidcConfig(enabled, clientId, clientSecret, issuer, scopes)
            ApiService-->>AccountSettingsComponent: updateResult
            AccountSettingsComponent->>AccountSettingsComponent: oidcSaving.set(false)
        else Admin cancels
            AccountSettingsComponent->>AccountSettingsComponent: return without saving
        end
    else OIDC disabled or subject linked
        AccountSettingsComponent->>AccountSettingsComponent: oidcSaving.set(true)
        AccountSettingsComponent->>ApiService: updateOidcConfig(enabled, clientId, clientSecret, issuer, scopes)
        ApiService-->>AccountSettingsComponent: updateResult
        AccountSettingsComponent->>AccountSettingsComponent: oidcSaving.set(false)
    end
Loading

Class diagram for AccountSettingsComponent OIDC save flow

classDiagram
    class AccountSettingsComponent {
      - api : ApiService
      - confirmService : ConfirmService
      - oidcSaving : SignalBoolean
      + ngOnInit() : void
      + ngOnDestroy() : void
      + oidcEnabled() : boolean
      + oidcAuthorizedSubject() : string
      + saveOidcConfig() : Promise~void~
    }

    class ApiService {
      + updateOidcConfig(enabled boolean, clientId string, clientSecret string, issuer string, scopes string) : Promise~OidcConfigResponse~
    }

    class ConfirmService {
      + confirm(config ConfirmConfig) : Promise~boolean~
    }

    class ConfirmConfig {
      + title : string
      + message : string
      + confirmLabel : string
      + destructive : boolean
    }

    class SignalBoolean {
      + set(value boolean) : void
    }

    class OidcConfigResponse {
      + enabled : boolean
      + clientId : string
      + issuer : string
      + scopes : string
    }

    AccountSettingsComponent --> ApiService : uses
    AccountSettingsComponent --> ConfirmService : uses
    AccountSettingsComponent --> SignalBoolean : manages
    ConfirmService --> ConfirmConfig : uses
    ApiService --> OidcConfigResponse : returns
Loading

File-Level Changes

Change Details Files
Gate saving OIDC settings behind a confirmation dialog when OIDC is enabled and no account is linked.
  • Converted the OIDC save handler to async to support awaiting a confirmation dialog.
  • Added a confirmService-based alert dialog that warns about enabling OIDC without a linked account and requires explicit user confirmation.
  • Made the save operation early-return if the user cancels, preserving existing behavior when OIDC is disabled or a subject is linked.
code/frontend/src/app/features/settings/account/account-settings.component.ts
Clarified documentation about the security implications of running OIDC in unlinked mode.
  • Added a Warning callout emphasizing that unlinked mode delegates all access control to the IdP and is unsafe with public providers.
  • Explained that public/open-registration providers allow anyone who signs up to gain administrator access unless a specific account is linked.
docs/docs/configuration/account/index.mdx
Added Playwright E2E tests to cover the new warning dialog behavior and OIDC save edge cases.
  • Introduced a serial test suite that configures OIDC via API and ensures no subject is linked before tests run.
  • Added tests to assert the warning appears when saving with OIDC enabled and no linked subject, and that cancelling prevents the save API call and success toast.
  • Added tests verifying that confirming the dialog proceeds with save, that disabling OIDC skips the warning, and that having a linked subject also bypasses the warning.
  • Implemented helper flows for logging in via the UI, opening the OIDC settings section, and completing the OIDC link flow via Keycloak in the browser.
e2e/tests/15-oidc-save-without-link-warning.spec.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • Consider wrapping the confirmService.confirm call in saveOidcConfig in a try/catch so that any rejected promise (e.g., dialog failure or service error) is treated like a cancellation instead of potentially bubbling an unhandled rejection and leaving the UI in an inconsistent state.
  • The OIDC configuration payload used in the Playwright tests is duplicated in multiple fetch calls; extracting a small helper to build this payload would reduce the risk of these falling out of sync with each other or with future config changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider wrapping the `confirmService.confirm` call in `saveOidcConfig` in a try/catch so that any rejected promise (e.g., dialog failure or service error) is treated like a cancellation instead of potentially bubbling an unhandled rejection and leaving the UI in an inconsistent state.
- The OIDC configuration payload used in the Playwright tests is duplicated in multiple `fetch` calls; extracting a small helper to build this payload would reduce the risk of these falling out of sync with each other or with future config changes.

## Individual Comments

### Comment 1
<location path="docs/docs/configuration/account/index.mdx" line_range="153" />
<code_context>

 Linking an account is **optional**. By default, when no account is linked, **any user who can authenticate with your identity provider and has access to this app** is allowed to sign in. Your provider controls who has access — if a user can log in to the configured OIDC client, they are permitted into Cleanuparr.

+<Warning>
+The unlinked mode delegates **all** access decisions to your identity provider. This is appropriate for self-hosted providers where you control every account. It is **dangerous** with public providers open registration. Anyone who can sign up at that provider would be able to sign in to Cleanuparr as the administrator. If you use a public provider, link a specific account.
+</Warning>
+
</code_context>
<issue_to_address>
**nitpick (typo):** Fix wording in the warning about public providers with open registration

The phrase "It is **dangerous** with public providers open registration" is a bit ungrammatical. Consider rephrasing to "It is **dangerous** with public providers that allow open registration" or "It is **dangerous** when using public providers with open registration" to improve clarity while keeping the warning strong.

```suggestion
The unlinked mode delegates **all** access decisions to your identity provider. This is appropriate for self-hosted providers where you control every account. It is **dangerous** when using public providers with open registration. Anyone who can sign up at that provider would be able to sign in to Cleanuparr as the administrator. If you use a public provider, link a specific account.
```
</issue_to_address>

### Comment 2
<location path="e2e/tests/15-oidc-save-without-link-warning.spec.ts" line_range="13-38" />
<code_context>
+  test.beforeAll(async () => {
+    adminToken = await loginAndGetToken();
+    // Ensure OIDC is enabled (idempotent — no-op if already enabled).
+    await fetch(`${API}/api/account/oidc`, {
+      method: 'PUT',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: `Bearer ${adminToken}`,
+      },
+      body: JSON.stringify({
+        enabled: true,
+        providerName: TEST_CONFIG.oidcProviderName,
+        issuerUrl: `${TEST_CONFIG.keycloakUrl}/realms/${TEST_CONFIG.realm}`,
+        clientId: TEST_CONFIG.clientId,
+        clientSecret: TEST_CONFIG.clientSecret,
+        scopes: 'openid profile email',
+        redirectUrl: '',
+        exclusiveMode: false,
+      }),
+    });
</code_context>
<issue_to_address>
**suggestion (testing):** Assert that setup API calls succeed so failures don’t surface later as opaque UI flakes

The `beforeAll` setup calls `fetch` for OIDC config and clearing linked subjects but ignores the responses. If these fail (e.g., misconfigured env or backend change), tests will run in an invalid state and only later UI checks will fail. Please assert `res.ok` (and throw with a clear error) for both the `PUT /api/account/oidc` and `DELETE /api/account/oidc/link` calls so setup failures are caught early and are easier to diagnose.

```suggestion
  test.beforeAll(async () => {
    adminToken = await loginAndGetToken();
    // Ensure OIDC is enabled (idempotent — no-op if already enabled).
    const oidcConfigResponse = await fetch(`${API}/api/account/oidc`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${adminToken}`,
      },
      body: JSON.stringify({
        enabled: true,
        providerName: TEST_CONFIG.oidcProviderName,
        issuerUrl: `${TEST_CONFIG.keycloakUrl}/realms/${TEST_CONFIG.realm}`,
        clientId: TEST_CONFIG.clientId,
        clientSecret: TEST_CONFIG.clientSecret,
        scopes: 'openid profile email',
        redirectUrl: '',
        exclusiveMode: false,
      }),
    });

    if (!oidcConfigResponse.ok) {
      const body = await oidcConfigResponse
        .text()
        .catch(() => '<failed to read response body>');
      throw new Error(
        `Failed to configure OIDC in beforeAll (PUT /api/account/oidc): ` +
          `status=${oidcConfigResponse.status} ${oidcConfigResponse.statusText}, body=${body}`,
      );
    }

    // Clear any linked subject so the dangerous-state save warning is reachable.
    const clearLinkResponse = await fetch(`${API}/api/account/oidc/link`, {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${adminToken}` },
    });

    if (!clearLinkResponse.ok) {
      const body = await clearLinkResponse
        .text()
        .catch(() => '<failed to read response body>');
      throw new Error(
        `Failed to clear linked OIDC subject in beforeAll (DELETE /api/account/oidc/link): ` +
          `status=${clearLinkResponse.status} ${clearLinkResponse.statusText}, body=${body}`,
      );
    }
  });
```
</issue_to_address>

### Comment 3
<location path="e2e/tests/15-oidc-save-without-link-warning.spec.ts" line_range="74" />
<code_context>
+
+    await page.getByRole('button', { name: 'Save OIDC Settings' }).click();
+
+    const dialog = page.getByRole('alertdialog');
+    await expect(dialog).toBeVisible({ timeout: 5_000 });
+    await expect(dialog).toContainText('Enable OIDC without a linked account');
+    await expect(dialog).toContainText('UNSAFE');
+    await expect(
+      dialog.getByRole('button', { name: 'Enable anyway' }),
</code_context>
<issue_to_address>
**suggestion (testing):** Make the dialog locator more specific to reduce ambiguity and future-proof the assertions

The dialog is currently located with `page.getByRole('alertdialog')` only. If another alert dialog is added (e.g., a different warning), these assertions could target the wrong element. To make the test more robust, scope the locator to this specific OIDC warning, for example:

```ts
const dialog = page.getByRole('alertdialog', {
  name: 'Enable OIDC without a linked account',
});
```

Since you already assert on this title, using it in the locator clarifies intent and reduces the risk of future false positives.

```suggestion
    const dialog = page.getByRole('alertdialog', {
      name: 'Enable OIDC without a linked account',
    });
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Linking an account is **optional**. By default, when no account is linked, **any user who can authenticate with your identity provider and has access to this app** is allowed to sign in. Your provider controls who has access — if a user can log in to the configured OIDC client, they are permitted into Cleanuparr.

<Warning>
The unlinked mode delegates **all** access decisions to your identity provider. This is appropriate for self-hosted providers where you control every account. It is **dangerous** with public providers open registration. Anyone who can sign up at that provider would be able to sign in to Cleanuparr as the administrator. If you use a public provider, link a specific account.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nitpick (typo): Fix wording in the warning about public providers with open registration

The phrase "It is dangerous with public providers open registration" is a bit ungrammatical. Consider rephrasing to "It is dangerous with public providers that allow open registration" or "It is dangerous when using public providers with open registration" to improve clarity while keeping the warning strong.

Suggested change
The unlinked mode delegates **all** access decisions to your identity provider. This is appropriate for self-hosted providers where you control every account. It is **dangerous** with public providers open registration. Anyone who can sign up at that provider would be able to sign in to Cleanuparr as the administrator. If you use a public provider, link a specific account.
The unlinked mode delegates **all** access decisions to your identity provider. This is appropriate for self-hosted providers where you control every account. It is **dangerous** when using public providers with open registration. Anyone who can sign up at that provider would be able to sign in to Cleanuparr as the administrator. If you use a public provider, link a specific account.

Comment thread e2e/tests/15-oidc-save-without-link-warning.spec.ts
Comment thread e2e/tests/15-oidc-save-without-link-warning.spec.ts Outdated
@Flaminel Flaminel merged commit c3f3ee8 into main Apr 27, 2026
7 of 10 checks passed
@Flaminel Flaminel deleted the add_oidc_warning branch April 27, 2026 10:19
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