Add UI warning when not linking an OIDC account#587
Conversation
|
@greptileai review |
|
@sourcery-ai review |
Greptile SummaryThis 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/5Safe 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
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]
Reviews (1): Last reviewed commit: "added warning when not linking an OIDC a..." | Re-trigger Greptile |
|
|
||
| <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> |
| await expect(dialog).toBeVisible({ timeout: 5_000 }); | ||
| await dialog.getByRole('button', { name: 'Cancel' }).click(); |
There was a problem hiding this comment.
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.
Reviewer's GuideAdds 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 warningsequenceDiagram
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
Class diagram for AccountSettingsComponent OIDC save flowclassDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- Consider wrapping the
confirmService.confirmcall insaveOidcConfigin 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
fetchcalls; 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>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. |
There was a problem hiding this comment.
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.
| 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. |
Summary by Sourcery
Add a confirmation warning when enabling OIDC without a linked account and strengthen associated documentation and tests.
New Features:
Documentation:
Tests: