Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion e2e/specs/external-auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ async function configureLDAPViaUI(page: Page) {
await page.goto('/settings?tab=oauth')
await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible()

const ldapSwitch = page.getByRole('switch').first()
const ldapSection = page
.locator('div.rounded-lg.border')
.filter({ has: page.getByText(/^LDAP$/) })
.first()
const ldapSwitch = ldapSection.getByRole('switch')
if ((await ldapSwitch.getAttribute('data-state')) !== 'checked') {
await ldapSwitch.click()
}
Expand Down
76 changes: 41 additions & 35 deletions pkg/ai/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,33 +196,35 @@ func HandleGetGeneralSetting(c *gin.Context) {
}
hasAIAPIKey := strings.TrimSpace(string(setting.AIAPIKey)) != ""
c.JSON(http.StatusOK, gin.H{
"aiAgentEnabled": setting.AIAgentEnabled,
"aiProvider": setting.AIProvider,
"aiModel": setting.AIModel,
"aiApiKey": "",
"aiApiKeyConfigured": hasAIAPIKey,
"aiBaseUrl": setting.AIBaseURL,
"aiMaxTokens": setting.AIMaxTokens,
"kubectlEnabled": setting.KubectlEnabled,
"kubectlImage": setting.KubectlImage,
"nodeTerminalImage": setting.NodeTerminalImage,
"enableAnalytics": setting.EnableAnalytics,
"enableVersionCheck": setting.EnableVersionCheck,
"aiAgentEnabled": setting.AIAgentEnabled,
"aiProvider": setting.AIProvider,
"aiModel": setting.AIModel,
"aiApiKey": "",
"aiApiKeyConfigured": hasAIAPIKey,
"aiBaseUrl": setting.AIBaseURL,
"aiMaxTokens": setting.AIMaxTokens,
"kubectlEnabled": setting.KubectlEnabled,
"kubectlImage": setting.KubectlImage,
"nodeTerminalImage": setting.NodeTerminalImage,
"enableAnalytics": setting.EnableAnalytics,
"enableVersionCheck": setting.EnableVersionCheck,
"passwordLoginDisabled": setting.PasswordLoginDisabled,
})
}

type UpdateGeneralSettingRequest struct {
AIAgentEnabled bool `json:"aiAgentEnabled"`
AIProvider string `json:"aiProvider"`
AIModel string `json:"aiModel"`
AIAPIKey *string `json:"aiApiKey"`
AIBaseURL string `json:"aiBaseUrl"`
AIMaxTokens int `json:"aiMaxTokens"`
KubectlEnabled bool `json:"kubectlEnabled"`
KubectlImage string `json:"kubectlImage"`
NodeTerminalImage string `json:"nodeTerminalImage"`
EnableAnalytics bool `json:"enableAnalytics"`
EnableVersionCheck bool `json:"enableVersionCheck"`
AIAgentEnabled bool `json:"aiAgentEnabled"`
AIProvider string `json:"aiProvider"`
AIModel string `json:"aiModel"`
AIAPIKey *string `json:"aiApiKey"`
AIBaseURL string `json:"aiBaseUrl"`
AIMaxTokens int `json:"aiMaxTokens"`
KubectlEnabled bool `json:"kubectlEnabled"`
KubectlImage string `json:"kubectlImage"`
NodeTerminalImage string `json:"nodeTerminalImage"`
EnableAnalytics bool `json:"enableAnalytics"`
EnableVersionCheck bool `json:"enableVersionCheck"`
PasswordLoginDisabled *bool `json:"passwordLoginDisabled"`
}

func HandleUpdateGeneralSetting(c *gin.Context) {
Expand Down Expand Up @@ -298,6 +300,9 @@ func HandleUpdateGeneralSetting(c *gin.Context) {
"enable_analytics": req.EnableAnalytics,
"enable_version_check": req.EnableVersionCheck,
}
if req.PasswordLoginDisabled != nil {
updates["password_login_disabled"] = *req.PasswordLoginDisabled
}
if shouldUpdateAIAPIKey {
updates["ai_api_key"] = model.SecretString(aiAPIKey)
}
Expand All @@ -310,18 +315,19 @@ func HandleUpdateGeneralSetting(c *gin.Context) {

hasAIAPIKey := strings.TrimSpace(string(updated.AIAPIKey)) != ""
c.JSON(http.StatusOK, gin.H{
"aiAgentEnabled": updated.AIAgentEnabled,
"aiProvider": updated.AIProvider,
"aiModel": updated.AIModel,
"aiApiKey": "",
"aiApiKeyConfigured": hasAIAPIKey,
"aiBaseUrl": updated.AIBaseURL,
"aiMaxTokens": updated.AIMaxTokens,
"kubectlEnabled": updated.KubectlEnabled,
"kubectlImage": updated.KubectlImage,
"nodeTerminalImage": updated.NodeTerminalImage,
"enableAnalytics": updated.EnableAnalytics,
"enableVersionCheck": updated.EnableVersionCheck,
"aiAgentEnabled": updated.AIAgentEnabled,
"aiProvider": updated.AIProvider,
"aiModel": updated.AIModel,
"aiApiKey": "",
"aiApiKeyConfigured": hasAIAPIKey,
"aiBaseUrl": updated.AIBaseURL,
"aiMaxTokens": updated.AIMaxTokens,
"kubectlEnabled": updated.KubectlEnabled,
"kubectlImage": updated.KubectlImage,
"nodeTerminalImage": updated.NodeTerminalImage,
"enableAnalytics": updated.EnableAnalytics,
"enableVersionCheck": updated.EnableVersionCheck,
"passwordLoginDisabled": updated.PasswordLoginDisabled,
})
}

Expand Down
16 changes: 15 additions & 1 deletion pkg/auth/login_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,16 @@ import (
)

func (h *AuthHandler) GetProviders(c *gin.Context) {
credentialProviders := []string{model.AuthProviderPassword}
var credentialProviders []string

generalSetting, err := model.GetGeneralSetting()
if err != nil {
klog.Warningf("Failed to load general setting for providers: %v", err)
}
if generalSetting == nil || !generalSetting.PasswordLoginDisabled {
credentialProviders = append(credentialProviders, model.AuthProviderPassword)
}

oauthProviders := uniqueStrings(h.manager.GetAvailableProviders())

setting, err := model.GetLDAPSetting()
Expand Down Expand Up @@ -67,6 +76,11 @@ func (h *AuthHandler) Login(c *gin.Context) {
}

func (h *AuthHandler) PasswordLogin(c *gin.Context) {
setting, err := model.GetGeneralSetting()
if err == nil && setting.PasswordLoginDisabled {
c.JSON(http.StatusForbidden, gin.H{"error": "Password login is disabled"})
return
}
h.handleCredentialLogin(c, model.AuthProviderPassword, h.authenticatePasswordUser)
}

Expand Down
1 change: 1 addition & 0 deletions pkg/model/general_setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type GeneralSetting struct {
NodeTerminalImage string `json:"nodeTerminalImage" gorm:"column:node_terminal_image;type:varchar(255);not null;default:'busybox:latest'"`
EnableAnalytics bool `json:"enableAnalytics" gorm:"column:enable_analytics;type:boolean;not null;default:true"`
EnableVersionCheck bool `json:"enableVersionCheck" gorm:"column:enable_version_check;type:boolean;not null;default:true"`
PasswordLoginDisabled bool `json:"passwordLoginDisabled" gorm:"column:password_login_disabled;type:boolean;not null;default:false"`
JWTSecret SecretString `json:"-" gorm:"column:jwt_secret;type:text"`
GlobalSidebarPreference string `json:"-" gorm:"column:global_sidebar_preference;type:text"`
}
Expand Down
81 changes: 77 additions & 4 deletions ui/src/components/settings/authentication-management.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useEffect, useState } from 'react'
import { IconKey } from '@tabler/icons-react'
import { IconAlertTriangle, IconKey } from '@tabler/icons-react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'

import {
LDAPSetting,
LDAPSettingUpdateRequest,
updateGeneralSetting,
updateLDAPSetting,
useGeneralSetting,
useLDAPSetting,
} from '@/lib/api'
import { translateError } from '@/lib/utils'
Expand Down Expand Up @@ -65,20 +67,39 @@ export function AuthenticationManagement() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const { data, error, isError, isLoading, refetch } = useLDAPSetting()
const { data: generalSetting } = useGeneralSetting()
const [formData, setFormData] = useState<AuthenticationFormData>(
createDefaultSettings
)
const [passwordLoginEnabled, setPasswordLoginEnabled] = useState(true)

useEffect(() => {
setFormData(toFormData(data))
}, [data])

useEffect(() => {
if (generalSetting) {
setPasswordLoginEnabled(!generalSetting.passwordLoginDisabled)
}
}, [generalSetting])

const mutation = useMutation({
mutationFn: updateLDAPSetting,
mutationFn: (params: {
ldap: LDAPSettingUpdateRequest
passwordLoginDisabled: boolean
}) =>
Promise.all([
updateLDAPSetting(params.ldap),
updateGeneralSetting({
...generalSetting!,
passwordLoginDisabled: params.passwordLoginDisabled,
}),
]),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['ldap-setting'],
})
queryClient.invalidateQueries({ queryKey: ['general-setting'] })
toast.success(
t(
'authenticationManagement.messages.updated',
Expand Down Expand Up @@ -121,7 +142,10 @@ export function AuthenticationManagement() {
payload.bindPassword = formData.bindPassword
}

mutation.mutate(payload)
mutation.mutate({
ldap: payload,
passwordLoginDisabled: !passwordLoginEnabled,
})
}

if (isLoading && !data) {
Expand Down Expand Up @@ -166,6 +190,52 @@ export function AuthenticationManagement() {
</CardHeader>

<CardContent className="space-y-4">
<div className="rounded-lg border">
<div className="flex items-center justify-between p-3">
<div className="space-y-1">
<Label className="text-sm font-medium">
{t(
'authenticationManagement.password.title',
'Password Login'
)}
</Label>
<p className="text-xs text-muted-foreground">
{t(
'authenticationManagement.password.description',
'Allow users to sign in with a username and password.'
)}
</p>
</div>
<Switch
checked={passwordLoginEnabled}
onCheckedChange={setPasswordLoginEnabled}
disabled={!generalSetting}
/>
</div>

{!passwordLoginEnabled && (
<div className="border-t p-3">
<div className="flex gap-3 rounded-md bg-amber-500/10 border border-amber-500/30 p-3">
<IconAlertTriangle className="h-5 w-5 text-amber-500 shrink-0 mt-0.5" />
<div className="space-y-1 text-sm">
<p className="font-medium text-amber-600 dark:text-amber-400">
{t(
'authenticationManagement.password.warning.title',
'Warning: You may lose access!'
)}
</p>
<p className="text-muted-foreground">
{t(
'authenticationManagement.password.warning.description',
'Verify that your LDAP or OAuth provider is working before saving. Without a working alternative login method, you will be locked out and can only recover by resetting the database.'
)}
</p>
</div>
</div>
</div>
)}
</div>

<div className="rounded-lg border">
<div className="flex items-center justify-between p-3">
<div className="space-y-1">
Expand Down Expand Up @@ -427,7 +497,10 @@ export function AuthenticationManagement() {
</div>

<div className="flex justify-end">
<Button onClick={handleSave} disabled={mutation.isPending}>
<Button
onClick={handleSave}
disabled={mutation.isPending || !generalSetting}
>
{t('common.save', 'Save')}
</Button>
</div>
Expand Down
2 changes: 2 additions & 0 deletions ui/src/lib/api/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ export interface GeneralSetting {
nodeTerminalImage: string
enableAnalytics: boolean
enableVersionCheck: boolean
passwordLoginDisabled: boolean
}

export interface GeneralSettingUpdateRequest {
Expand All @@ -368,6 +369,7 @@ export interface GeneralSettingUpdateRequest {
nodeTerminalImage?: string
enableAnalytics?: boolean
enableVersionCheck?: boolean
passwordLoginDisabled?: boolean
}

export type CredentialProvider = 'password' | 'ldap'
Expand Down
Loading