Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f23af10
fix(agent): include token in enrollment view command
codex May 15, 2026
cfd13c7
chore(agent): add dev container installer helper
codex May 15, 2026
594b4fd
chore(dev): seed agent container env defaults
codex May 15, 2026
60f7abb
fix(dev): connect agent containers to dev network
codex May 15, 2026
dd256c8
feat(hosts): add activity dialog association tabs
codex May 15, 2026
0744c1e
fix(password-manager): align launch assertion tenant claims
codex May 15, 2026
7d0b0db
Merge branch 'feat/host-activity-dialog-tabs' into local/integration
codex May 15, 2026
5155368
Merge branch 'main' into local/integration
codex May 16, 2026
73b9907
Merge branch 'main' into local/integration
codex May 16, 2026
0ed0fd9
Merge remote-tracking branch 'origin/main' into local/integration
codex May 16, 2026
6a6e971
chore(gitignore): ignore worktree directory
codex May 16, 2026
4d4c4ba
fix(ldap): align add form with AD defaults
codex May 16, 2026
46c1f3c
feat(alerts): replace host metric alerts from defaults
codex May 16, 2026
d1afa01
Merge branch 'feat/host-alert-defaults' into local/integration
codex May 16, 2026
ec221df
feat(alerts): make global defaults opt-in for hosts
codex May 16, 2026
15f7a8f
Merge branch 'feat/optional-global-alert-defaults' into local/integra…
codex May 16, 2026
0898b20
feat(terminal): support custom SSH ports
codex May 16, 2026
e224308
Merge branch 'main' into local/integration
codex May 17, 2026
cabceda
fix(settings): widen LDAP modal and allow CA replacement
codex May 17, 2026
ee2d837
Merge branch 'fix/ldap-modal-width' into local/integration
codex May 17, 2026
b4acf33
fix(settings): widen LDAP modal and allow CA replacement
codex May 17, 2026
d65706d
Merge branch 'fix/ldap-modal-width' into local/integration
codex May 17, 2026
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
74 changes: 44 additions & 30 deletions apps/web/app/(dashboard)/settings/ldap/ldap-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Badge } from '@/components/ui/badge'
Expand Down Expand Up @@ -74,9 +75,13 @@ const EMPTY_FORM = {
function CertificateUpload({
value,
onChange,
inputTestId,
previewTestId,
}: {
value: string
onChange: (cert: string) => void
inputTestId?: string
previewTestId?: string
}) {
const fileInputRef = useRef<HTMLInputElement>(null)

Expand All @@ -92,42 +97,43 @@ function CertificateUpload({
e.target.value = ''
}

if (value) {
return (
<div className="space-y-1.5">
<Label>TLS Certificate (CA)</Label>
<div className="rounded-md border bg-muted/50 px-3 py-2 text-xs font-mono whitespace-pre-wrap break-all overflow-y-auto max-h-24 text-muted-foreground relative">
{value}
return (
<div className="space-y-1.5">
<Label>TLS Certificate (CA)</Label>
<input ref={fileInputRef} type="file" accept=".pem,.crt,.cer,.cert" className="hidden" onChange={handleFileUpload} data-testid={inputTestId} />
<Textarea
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder="Paste PEM CA certificate"
className="max-h-40 min-h-24 resize-y font-mono text-xs"
data-testid={previewTestId}
/>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
size="sm"
className="flex-1"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="size-4 mr-1.5" />
{value ? 'Replace Certificate (.pem, .crt)' : 'Upload Certificate (.pem, .crt)'}
</Button>
{value && (
<Button
type="button"
variant="ghost"
size="sm"
className="absolute top-1 right-1 size-6 p-0 text-muted-foreground hover:text-destructive"
variant="outline"
size="icon-sm"
className="text-muted-foreground hover:text-destructive"
onClick={() => onChange('')}
aria-label="Clear certificate"
>
<X className="size-3.5" />
</Button>
</div>
)}
</div>
)
}

return (
<div className="space-y-1.5">
<Label>TLS Certificate (CA)</Label>
<input ref={fileInputRef} type="file" accept=".pem,.crt,.cer,.cert" className="hidden" onChange={handleFileUpload} />
<Button
type="button"
variant="outline"
size="sm"
className="w-full"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="size-4 mr-1.5" />
Upload Certificate (.pem, .crt)
</Button>
<p className="text-xs text-muted-foreground">
Upload a CA certificate when the LDAP server uses a private CA. Otherwise TLS connections use the platform trust store to verify the server identity.
Paste or upload a CA certificate when the LDAP server uses a private CA. Otherwise TLS connections use the platform trust store to verify the server identity.
</p>
</div>
)
Expand Down Expand Up @@ -268,7 +274,7 @@ export function LdapSettingsClient({
Add Configuration
</Button>
</DialogTrigger>
<DialogContent className="max-w-lg max-h-[80vh] overflow-x-hidden overflow-y-auto">
<DialogContent className="max-w-[calc(100%-1rem)] sm:max-w-2xl max-h-[80vh] overflow-x-hidden overflow-y-auto">
<DialogHeader>
<DialogTitle>Add LDAP Configuration</DialogTitle>
<DialogDescription>
Expand Down Expand Up @@ -309,13 +315,15 @@ export function LdapSettingsClient({
<Switch
checked={addForm.useTls}
onCheckedChange={(checked) => setAddForm({ ...addForm, useTls: checked, port: checked ? 636 : 389 })}
data-testid="ldap-settings-add-use-tls"
/>
Use TLS (LDAPS)
</label>
<label className="flex items-center gap-2 text-sm">
<Switch
checked={addForm.useStartTls}
onCheckedChange={(checked) => setAddForm({ ...addForm, useStartTls: checked })}
data-testid="ldap-settings-add-use-starttls"
/>
Use STARTTLS
</label>
Expand All @@ -324,6 +332,8 @@ export function LdapSettingsClient({
<CertificateUpload
value={addForm.tlsCertificate}
onChange={(cert) => setAddForm({ ...addForm, tlsCertificate: cert })}
inputTestId="ldap-settings-add-ca-file"
previewTestId="ldap-settings-add-ca-preview"
/>
)}
<div className="space-y-1.5">
Expand Down Expand Up @@ -556,7 +566,7 @@ export function LdapSettingsClient({

{/* Edit LDAP Configuration Dialog */}
<Dialog open={editingConfig !== null} onOpenChange={(open) => { if (!open) setEditingConfig(null) }}>
<DialogContent className="max-w-lg max-h-[80vh] overflow-x-hidden overflow-y-auto">
<DialogContent className="max-w-[calc(100%-1rem)] sm:max-w-2xl max-h-[80vh] overflow-x-hidden overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit LDAP Configuration</DialogTitle>
<DialogDescription>
Expand Down Expand Up @@ -597,13 +607,15 @@ export function LdapSettingsClient({
<Switch
checked={editForm.useTls}
onCheckedChange={(checked) => setEditForm({ ...editForm, useTls: checked, port: checked ? 636 : 389 })}
data-testid="ldap-settings-edit-use-tls"
/>
Use TLS (LDAPS)
</label>
<label className="flex items-center gap-2 text-sm">
<Switch
checked={editForm.useStartTls}
onCheckedChange={(checked) => setEditForm({ ...editForm, useStartTls: checked })}
data-testid="ldap-settings-edit-use-starttls"
/>
Use STARTTLS
</label>
Expand All @@ -612,6 +624,8 @@ export function LdapSettingsClient({
<CertificateUpload
value={editForm.tlsCertificate}
onChange={(cert) => setEditForm({ ...editForm, tlsCertificate: cert })}
inputTestId="ldap-settings-edit-ca-file"
previewTestId="ldap-settings-edit-ca-preview"
/>
)}
<div className="space-y-1.5">
Expand Down
38 changes: 37 additions & 1 deletion apps/web/tests/e2e/settings/ldap.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { test, expect } from '../fixtures/test'
import type { Page } from '@playwright/test'
import { getTestDb } from '../fixtures/db'
import { TEST_INSTANCE } from '../fixtures/seed'

const ADD_PASTED_CA = '-----BEGIN CERTIFICATE-----\nADD_PASTED_CA\n-----END CERTIFICATE-----'
const FIRST_EDIT_CA = '-----BEGIN CERTIFICATE-----\nFIRST_EDIT_CA\n-----END CERTIFICATE-----'
const REPLACEMENT_EDIT_CA = '-----BEGIN CERTIFICATE-----\nREPLACEMENT_EDIT_CA\n-----END CERTIFICATE-----'

async function getInstanceId(sql: ReturnType<typeof getTestDb>): Promise<string> {
const rows = await sql<Array<{ id: string }>>`
SELECT id FROM instance_settings WHERE slug = ${TEST_INSTANCE.slug} LIMIT 1
Expand All @@ -10,6 +15,14 @@ async function getInstanceId(sql: ReturnType<typeof getTestDb>): Promise<string>
return rows[0]!.id
}

async function expectLdapDialogWide(page: Page): Promise<void> {
const dialog = page.locator('[data-slot="dialog-content"]').first()
await expect(dialog).toBeVisible()

const box = await dialog.boundingBox()
expect(box?.width).toBeGreaterThanOrEqual(640)
}

test('admin can create, edit, toggle, and delete an LDAP configuration', async ({ authenticatedPage: page }) => {
const sql = getTestDb()
const instanceId = await getInstanceId(sql)
Expand All @@ -20,6 +33,7 @@ test('admin can create, edit, toggle, and delete an LDAP configuration', async (
await expect(page.getByTestId('ldap-settings-empty-state')).toBeVisible()

await page.getByTestId('ldap-settings-add-open').click()
await expectLdapDialogWide(page)
await expect(page.getByTestId('ldap-settings-add-host')).toHaveAttribute('placeholder', 'dc01.corp.example.com')
await expect(page.getByTestId('ldap-settings-add-base-dn')).toHaveAttribute('placeholder', 'DC=corp,DC=example,DC=com')
await expect(page.getByTestId('ldap-settings-add-bind-dn')).toHaveAttribute('placeholder', 'CN=svc-ldap,OU=Service Accounts,DC=corp,DC=example,DC=com')
Expand All @@ -28,6 +42,8 @@ test('admin can create, edit, toggle, and delete an LDAP configuration', async (
await page.getByTestId('ldap-settings-add-base-dn').fill('DC=internal,DC=example')
await page.getByTestId('ldap-settings-add-bind-dn').fill('CN=svc-ldap,DC=internal,DC=example')
await page.getByTestId('ldap-settings-add-bind-password').fill('SuperSecret123!')
await page.getByTestId('ldap-settings-add-use-starttls').click()
await page.getByTestId('ldap-settings-add-ca-preview').fill(ADD_PASTED_CA)
await expect(page.getByTestId('ldap-settings-add-user-filter')).toHaveValue('(sAMAccountName={{username}})')
await expect(page.getByTestId('ldap-settings-add-username-attribute')).toHaveValue('sAMAccountName')
await page.getByTestId('ldap-settings-add-user-search-base').fill('ou=Staff')
Expand Down Expand Up @@ -93,14 +109,34 @@ test('admin can create, edit, toggle, and delete an LDAP configuration', async (
await expect(configRow).toContainText('ldap://ldap.internal.example:389')

await page.getByTestId(`ldap-config-edit-${configId}`).click()
await expectLdapDialogWide(page)
await expect(page.getByTestId('ldap-settings-edit-ca-preview')).toHaveValue(ADD_PASTED_CA)
await page.getByTestId('ldap-settings-edit-use-tls').click()
await page.getByTestId('ldap-settings-edit-ca-file').setInputFiles({
name: 'first-edit-ca.pem',
mimeType: 'application/x-pem-file',
buffer: Buffer.from(FIRST_EDIT_CA),
})
await expect(page.getByTestId('ldap-settings-edit-ca-preview')).toContainText('FIRST_EDIT_CA')
await page.getByTestId('ldap-settings-edit-host').fill('directory.internal.example')
await page.getByTestId('ldap-settings-edit-port').fill('636')
await page.getByTestId('ldap-settings-edit-user-filter').fill('(mail={{username}})')
await page.getByTestId('ldap-settings-edit-save').click()

await expect(configRow).toContainText('ldap://directory.internal.example:636')
await expect(configRow).toContainText('ldaps://directory.internal.example:636')
await expect(configRow).toContainText('(mail={{username}})')

await page.getByTestId(`ldap-config-edit-${configId}`).click()
await expect(page.getByTestId('ldap-settings-edit-ca-preview')).toContainText('FIRST_EDIT_CA')
await expect(page.getByRole('button', { name: 'Replace Certificate (.pem, .crt)' })).toBeVisible()
await page.getByTestId('ldap-settings-edit-ca-preview').fill(REPLACEMENT_EDIT_CA)
await expect(page.getByTestId('ldap-settings-edit-ca-preview')).toHaveValue(REPLACEMENT_EDIT_CA)
await page.getByTestId('ldap-settings-edit-save').click()

await page.getByTestId(`ldap-config-edit-${configId}`).click()
await expect(page.getByTestId('ldap-settings-edit-ca-preview')).toHaveValue(REPLACEMENT_EDIT_CA)
await page.keyboard.press('Escape')

await page.getByTestId(`ldap-config-enabled-${configId}`).click()
await expect(configRow).toContainText('Disabled')

Expand Down
Loading