diff --git a/frontend/src/__tests__/components/OIDCProviderSettings.test.tsx b/frontend/src/__tests__/components/OIDCProviderSettings.test.tsx
index 1f40953b9..1436b7e20 100644
--- a/frontend/src/__tests__/components/OIDCProviderSettings.test.tsx
+++ b/frontend/src/__tests__/components/OIDCProviderSettings.test.tsx
@@ -325,3 +325,54 @@ describe('OIDCProviderSettings', () => {
});
});
});
+
+describe('env-managed provider (#2593)', () => {
+ const envManagedProvider = {
+ ...mockProviders[0],
+ id: 2,
+ name: 'EnvIdP',
+ is_env_managed: true,
+ };
+
+ it('marks the provider as environment managed', async () => {
+ server.use(
+ http.get('/api/v1/auth/oidc/providers/all', () => HttpResponse.json([envManagedProvider]))
+ );
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('EnvIdP')).toBeInTheDocument();
+ });
+ expect(screen.getByText(/Environment Managed/i)).toBeInTheDocument();
+ });
+
+ it('offers no edit or delete control for it', async () => {
+ server.use(
+ http.get('/api/v1/auth/oidc/providers/all', () => HttpResponse.json([envManagedProvider]))
+ );
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('EnvIdP')).toBeInTheDocument();
+ });
+ // Startup rewrites this row from the environment on every boot, and the API
+ // answers 409 — offering the controls would promise an edit that cannot land.
+ expect(screen.queryByTestId('edit-provider-2')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('delete-provider-2')).not.toBeInTheDocument();
+ });
+
+ it('still offers them for a UI-created provider', async () => {
+ server.use(
+ http.get('/api/v1/auth/oidc/providers/all', () =>
+ HttpResponse.json([{ ...mockProviders[0], is_env_managed: false }])
+ )
+ );
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('TestIdP')).toBeInTheDocument();
+ });
+ expect(screen.getByTestId('edit-provider-1')).toBeInTheDocument();
+ expect(screen.getByTestId('delete-provider-1')).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts
index f9cbbb212..a0da7257d 100644
--- a/frontend/src/api/client.ts
+++ b/frontend/src/api/client.ts
@@ -3518,6 +3518,11 @@ export interface OIDCProvider {
// #1589: when true, the LoginPage redirects unauthenticated visitors
// straight to this provider on mount. At most one provider may carry this.
is_autologin: boolean;
+ // #2593: defined by BAMBUDDY_OIDC_* and rewritten from the environment on
+ // every boot. The API answers 409 to any write, so the settings UI must not
+ // offer edit/delete controls that cannot succeed. Optional so a response
+ // from an older backend still type-checks.
+ is_env_managed?: boolean;
}
export interface OIDCProviderCreate {
diff --git a/frontend/src/components/OIDCProviderSettings.tsx b/frontend/src/components/OIDCProviderSettings.tsx
index cacc0d952..6ec27aca9 100644
--- a/frontend/src/components/OIDCProviderSettings.tsx
+++ b/frontend/src/components/OIDCProviderSettings.tsx
@@ -1,6 +1,6 @@
import { useState, type ReactNode } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { Plus, Edit2, Trash2, Globe, Check, X, RefreshCw, ExternalLink, ImageOff } from 'lucide-react';
+import { Plus, Edit2, Trash2, Globe, Check, X, RefreshCw, ExternalLink, ImageOff, Lock } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { api } from '../api/client';
import type { Group, OIDCProvider, OIDCProviderCreate } from '../api/client';
@@ -386,6 +386,11 @@ export function OIDCProviderSettings() {
{t('common.disabled')}
)}
+ {provider.is_env_managed && (
+
+ {t('settings.environmentManagedLabel')}
+
+ )}
@@ -417,21 +422,34 @@ export function OIDCProviderSettings() {
)}
- toggleEnabled(provider)}
- disabled={updateMutation.isPending}
- />
-
-
+ {/* #2593: startup rewrites the env-managed row from BAMBUDDY_OIDC_*
+ and the API answers 409, so offering these would promise a
+ change that cannot land. */}
+ {!provider.is_env_managed && (
+ <>
+ toggleEnabled(provider)}
+ disabled={updateMutation.isPending}
+ />
+
+
+ >
+ )}