feat(auth): admin_role config; drop manual admin-subject from wizard
OIDC settings page + setup wizard now configure the bootstrap admin role instead of a hand-typed OIDC subject. The OIDC-only admin link is handled automatically by the backend admin-role auto-connect on first sign-in (explained inline in both the wizard and settings page). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ interface Settings {
|
|||||||
redirect_uri: string | null
|
redirect_uri: string | null
|
||||||
scopes: string | null
|
scopes: string | null
|
||||||
post_login_redirect: string | null
|
post_login_redirect: string | null
|
||||||
|
admin_role: string
|
||||||
oidc_only: boolean
|
oidc_only: boolean
|
||||||
effective_enabled: boolean
|
effective_enabled: boolean
|
||||||
source: string
|
source: string
|
||||||
@@ -31,6 +32,7 @@ export default function OidcSettingsPage() {
|
|||||||
redirect_uri: '',
|
redirect_uri: '',
|
||||||
scopes: 'openid email profile',
|
scopes: 'openid email profile',
|
||||||
post_login_redirect: '',
|
post_login_redirect: '',
|
||||||
|
admin_role: 'admin',
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -46,6 +48,7 @@ export default function OidcSettingsPage() {
|
|||||||
redirect_uri: data.redirect_uri || '',
|
redirect_uri: data.redirect_uri || '',
|
||||||
scopes: data.scopes || 'openid email profile',
|
scopes: data.scopes || 'openid email profile',
|
||||||
post_login_redirect: data.post_login_redirect || '',
|
post_login_redirect: data.post_login_redirect || '',
|
||||||
|
admin_role: data.admin_role || 'admin',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((e) => setMessage(e.response?.data?.detail || 'Failed to load OIDC settings'))
|
.catch((e) => setMessage(e.response?.data?.detail || 'Failed to load OIDC settings'))
|
||||||
@@ -63,6 +66,7 @@ export default function OidcSettingsPage() {
|
|||||||
redirect_uri: form.redirect_uri.trim(),
|
redirect_uri: form.redirect_uri.trim(),
|
||||||
scopes: form.scopes.trim(),
|
scopes: form.scopes.trim(),
|
||||||
post_login_redirect: form.post_login_redirect.trim(),
|
post_login_redirect: form.post_login_redirect.trim(),
|
||||||
|
admin_role: form.admin_role.trim() || 'admin',
|
||||||
}
|
}
|
||||||
if (form.client_secret) payload.client_secret = form.client_secret
|
if (form.client_secret) payload.client_secret = form.client_secret
|
||||||
const { data } = await api.put<Settings>('/auth/oidc/settings', payload)
|
const { data } = await api.put<Settings>('/auth/oidc/settings', payload)
|
||||||
@@ -150,6 +154,14 @@ export default function OidcSettingsPage() {
|
|||||||
Post-login redirect (frontend)
|
Post-login redirect (frontend)
|
||||||
<input placeholder="https://hf.example.com/oidc/callback" value={form.post_login_redirect} onChange={(e) => setForm({ ...form, post_login_redirect: e.target.value })} />
|
<input placeholder="https://hf.example.com/oidc/callback" value={form.post_login_redirect} onChange={(e) => setForm({ ...form, post_login_redirect: e.target.value })} />
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
Admin role (bootstrap)
|
||||||
|
<input placeholder="admin" value={form.admin_role} onChange={(e) => setForm({ ...form, admin_role: e.target.value })} />
|
||||||
|
</label>
|
||||||
|
<p className="text-dim">
|
||||||
|
OIDC-only bootstrap: before any admin is linked, an IdP user whose token carries this role
|
||||||
|
auto-connects to the HarborForge admin account on first sign-in. Disables itself once an admin is bound.
|
||||||
|
</p>
|
||||||
<button className="btn-primary" disabled={saving} onClick={save}>
|
<button className="btn-primary" disabled={saving} onClick={save}>
|
||||||
{saving ? 'Saving...' : 'Save OIDC Settings'}
|
{saving ? 'Saving...' : 'Save OIDC Settings'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface SetupForm {
|
|||||||
oidc_redirect_uri: string
|
oidc_redirect_uri: string
|
||||||
oidc_scopes: string
|
oidc_scopes: string
|
||||||
oidc_post_login_redirect: string
|
oidc_post_login_redirect: string
|
||||||
oidc_admin_subject: string
|
oidc_admin_role: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const oidcOnly = getRuntimeOidcOnly() === true
|
const oidcOnly = getRuntimeOidcOnly() === true
|
||||||
@@ -52,7 +52,7 @@ export default function SetupWizardPage({ initialWizardPort, onComplete }: Props
|
|||||||
oidc_redirect_uri: '',
|
oidc_redirect_uri: '',
|
||||||
oidc_scopes: 'openid email profile',
|
oidc_scopes: 'openid email profile',
|
||||||
oidc_post_login_redirect: '',
|
oidc_post_login_redirect: '',
|
||||||
oidc_admin_subject: '',
|
oidc_admin_role: 'admin',
|
||||||
})
|
})
|
||||||
|
|
||||||
const set = (key: keyof SetupForm, value: string | number | boolean) =>
|
const set = (key: keyof SetupForm, value: string | number | boolean) =>
|
||||||
@@ -85,8 +85,8 @@ export default function SetupWizardPage({ initialWizardPort, onComplete }: Props
|
|||||||
if (!form.oidc_client_id.trim()) return 'OIDC client ID is required'
|
if (!form.oidc_client_id.trim()) return 'OIDC client ID is required'
|
||||||
if (!form.oidc_client_secret.trim()) return 'OIDC client secret is required'
|
if (!form.oidc_client_secret.trim()) return 'OIDC client secret is required'
|
||||||
if (!form.oidc_redirect_uri.trim()) return 'OIDC redirect/callback URL is required'
|
if (!form.oidc_redirect_uri.trim()) return 'OIDC redirect/callback URL is required'
|
||||||
if (oidcOnly && !form.oidc_admin_subject.trim()) {
|
if (oidcOnly && !form.oidc_admin_role.trim()) {
|
||||||
return "In OIDC-only mode the admin's OIDC subject is required so the admin can sign in"
|
return 'In OIDC-only mode the admin role is required so the admin can bootstrap'
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ export default function SetupWizardPage({ initialWizardPort, onComplete }: Props
|
|||||||
redirect_uri: form.oidc_redirect_uri.trim(),
|
redirect_uri: form.oidc_redirect_uri.trim(),
|
||||||
scopes: form.oidc_scopes.trim() || 'openid email profile',
|
scopes: form.oidc_scopes.trim() || 'openid email profile',
|
||||||
post_login_redirect: form.oidc_post_login_redirect.trim() || undefined,
|
post_login_redirect: form.oidc_post_login_redirect.trim() || undefined,
|
||||||
admin_subject: form.oidc_admin_subject.trim() || undefined,
|
admin_role: form.oidc_admin_role.trim() || 'admin',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,12 +230,16 @@ export default function SetupWizardPage({ initialWizardPort, onComplete }: Props
|
|||||||
<label>Redirect / Callback URL <input value={form.oidc_redirect_uri} onChange={(e) => set('oidc_redirect_uri', e.target.value)} placeholder="https://hf-api.example.com/auth/oidc/callback" /></label>
|
<label>Redirect / Callback URL <input value={form.oidc_redirect_uri} onChange={(e) => set('oidc_redirect_uri', e.target.value)} placeholder="https://hf-api.example.com/auth/oidc/callback" /></label>
|
||||||
<label>Scopes <input value={form.oidc_scopes} onChange={(e) => set('oidc_scopes', e.target.value)} /></label>
|
<label>Scopes <input value={form.oidc_scopes} onChange={(e) => set('oidc_scopes', e.target.value)} /></label>
|
||||||
<label>Post-login redirect (frontend) <input value={form.oidc_post_login_redirect} onChange={(e) => set('oidc_post_login_redirect', e.target.value)} placeholder="https://hf.example.com/oidc/callback" /></label>
|
<label>Post-login redirect (frontend) <input value={form.oidc_post_login_redirect} onChange={(e) => set('oidc_post_login_redirect', e.target.value)} placeholder="https://hf.example.com/oidc/callback" /></label>
|
||||||
{oidcOnly && (
|
<label>Admin role (bootstrap)
|
||||||
<label>Admin OIDC subject (sub)
|
<input value={form.oidc_admin_role} onChange={(e) => set('oidc_admin_role', e.target.value)} placeholder="admin" />
|
||||||
<input value={form.oidc_admin_subject} onChange={(e) => set('oidc_admin_subject', e.target.value)} placeholder="the admin's `sub` claim at the IdP" />
|
</label>
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<p className="setup-hint">Register the Redirect / Callback URL above at your identity provider.</p>
|
<p className="setup-hint">Register the Redirect / Callback URL above at your identity provider.</p>
|
||||||
|
{oidcOnly && (
|
||||||
|
<p className="setup-hint">
|
||||||
|
OIDC-only: before any admin is linked, the first IdP user whose token carries the
|
||||||
|
role above auto-connects to the HarborForge admin account. It then disables itself.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="setup-nav">
|
<div className="setup-nav">
|
||||||
|
|||||||
Reference in New Issue
Block a user