feat(ldap): Active Directory 双目录支持与 Docker 构建优化
- 新增 ldap_ad.go,支持 AD 密码编码、字段映射与目录类型判断 - 重构 ldap/ldap_users/ldap_ou,兼容 OpenLDAP 与 AD 登录、导入、OU 浏览 - 前端系统设置增加目录类型选择与 AD 配置项 - Dockerfile 按 TARGETARCH 单架构编译 Agent,限制 Node 内存防 OOM - run.sh 新增 docker fast/upgrade,README 补充 LDAP/AD 与常见问题
This commit is contained in:
1 parent
0f0f2770e1
commit
78c27037e3
12 files changed
+613
-226
No files matched your search
@@ -24,7 +24,10 @@ const SETTINGS_TABS = [
|
||||
] as const;
|
||||
type SettingsSubTab = (typeof SETTINGS_TABS)[number]['id'];
|
||||
|
||||
export type LdapServerType = 'openldap' | 'ad';
|
||||
|
||||
export interface LdapFormData {
|
||||
server_type: LdapServerType;
|
||||
ldap_host: string;
|
||||
ldap_port: number;
|
||||
use_ldaps: boolean;
|
||||
@@ -32,6 +35,11 @@ export interface LdapFormData {
|
||||
admin_password: string;
|
||||
base_dn: string;
|
||||
search_filter: string;
|
||||
user_mapping: {
|
||||
username: string;
|
||||
email: string;
|
||||
display_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SystemSettingsTabProps {
|
||||
@@ -325,6 +333,32 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">目录类型</label>
|
||||
<Select
|
||||
value={ldapForm.server_type}
|
||||
onValueChange={(v: LdapServerType) => {
|
||||
const isAD = v === 'ad';
|
||||
setLdapForm({
|
||||
...ldapForm,
|
||||
server_type: v,
|
||||
ldap_port: isAD && ldapForm.ldap_port === 389 ? 389 : ldapForm.ldap_port,
|
||||
search_filter: ldapForm.search_filter || (isAD ? '(sAMAccountName=%s)' : '(uid=%s)'),
|
||||
user_mapping: isAD
|
||||
? { username: 'sAMAccountName', email: 'mail', display_name: 'displayName' }
|
||||
: { username: 'uid', email: 'mail', display_name: 'cn' },
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="dg-input">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value="openldap">OpenLDAP</SelectItem>
|
||||
<SelectItem value="ad">Active Directory</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">LDAP 服务器地址</label>
|
||||
<Input
|
||||
@@ -350,7 +384,7 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
value={ldapForm.base_dn}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, base_dn: e.target.value })}
|
||||
className="dg-input"
|
||||
placeholder="ou=users,dc=example,dc=com"
|
||||
placeholder={ldapForm.server_type === 'ad' ? 'DC=example,DC=com' : 'ou=users,dc=example,dc=com'}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -359,7 +393,7 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
value={ldapForm.admin_dn}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, admin_dn: e.target.value })}
|
||||
className="dg-input"
|
||||
placeholder="cn=admin,dc=example,dc=com"
|
||||
placeholder={ldapForm.server_type === 'ad' ? 'CN=admin,CN=Users,DC=example,DC=com' : 'cn=admin,dc=example,dc=com'}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -378,7 +412,43 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
value={ldapForm.search_filter}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, search_filter: e.target.value })}
|
||||
className="dg-input"
|
||||
placeholder="(uid=%s)"
|
||||
placeholder={ldapForm.server_type === 'ad' ? '(sAMAccountName=%s)' : '(uid=%s)'}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">用户名字段</label>
|
||||
<Input
|
||||
value={ldapForm.user_mapping.username}
|
||||
onChange={(e) => setLdapForm({
|
||||
...ldapForm,
|
||||
user_mapping: { ...ldapForm.user_mapping, username: e.target.value },
|
||||
})}
|
||||
className="dg-input"
|
||||
placeholder={ldapForm.server_type === 'ad' ? 'sAMAccountName' : 'uid'}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">邮箱字段</label>
|
||||
<Input
|
||||
value={ldapForm.user_mapping.email}
|
||||
onChange={(e) => setLdapForm({
|
||||
...ldapForm,
|
||||
user_mapping: { ...ldapForm.user_mapping, email: e.target.value },
|
||||
})}
|
||||
className="dg-input"
|
||||
placeholder="mail"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">显示名字段</label>
|
||||
<Input
|
||||
value={ldapForm.user_mapping.display_name}
|
||||
onChange={(e) => setLdapForm({
|
||||
...ldapForm,
|
||||
user_mapping: { ...ldapForm.user_mapping, display_name: e.target.value },
|
||||
})}
|
||||
className="dg-input"
|
||||
placeholder={ldapForm.server_type === 'ad' ? 'displayName' : 'cn'}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 col-span-2">
|
||||
@@ -388,6 +458,9 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
className="data-[state=checked]:bg-blue-600"
|
||||
/>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">启用 LDAPS(SSL 加密)</span>
|
||||
{ldapForm.server_type === 'ad' && (
|
||||
<span className="text-xs dg-text-muted">AD 创建用户必须启用 LDAPS</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -30,10 +30,13 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
|
||||
// ===== LDAP 配置 =====
|
||||
const [ldapConfig, setLdapConfig] = useState<JsonData>(null);
|
||||
const [ldapForm, setLdapForm] = useState<LdapFormData>({
|
||||
const defaultLdapForm = (): LdapFormData => ({
|
||||
server_type: 'openldap',
|
||||
ldap_host: '', ldap_port: 389, use_ldaps: false,
|
||||
admin_dn: '', admin_password: '', base_dn: '', search_filter: '',
|
||||
user_mapping: { username: 'uid', email: 'mail', display_name: 'cn' },
|
||||
});
|
||||
const [ldapForm, setLdapForm] = useState<LdapFormData>(defaultLdapForm());
|
||||
const [ldapSaving, setLdapSaving] = useState(false);
|
||||
const [ldapTesting, setLdapTesting] = useState(false);
|
||||
const [ldapTestResult, setLdapTestResult] = useState<string | null>(null);
|
||||
@@ -68,7 +71,26 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
const res = await ldapApi.getConfig();
|
||||
if (res && res.ldap_host) {
|
||||
setLdapConfig(res);
|
||||
const serverType = (res.server_type === 'ad' ? 'ad' : 'openldap') as LdapFormData['server_type'];
|
||||
let mapping = { username: 'uid', email: 'mail', display_name: 'cn' };
|
||||
if (serverType === 'ad') {
|
||||
mapping = { username: 'sAMAccountName', email: 'mail', display_name: 'displayName' };
|
||||
}
|
||||
if (res.user_mapping) {
|
||||
let um = res.user_mapping;
|
||||
if (typeof um === 'string') {
|
||||
try { um = JSON.parse(um); } catch { um = null; }
|
||||
}
|
||||
if (um && typeof um === 'object') {
|
||||
mapping = {
|
||||
username: um.username || mapping.username,
|
||||
email: um.email || mapping.email,
|
||||
display_name: um.display_name || mapping.display_name,
|
||||
};
|
||||
}
|
||||
}
|
||||
setLdapForm({
|
||||
server_type: serverType,
|
||||
ldap_host: res.ldap_host || '',
|
||||
ldap_port: res.ldap_port || 389,
|
||||
use_ldaps: res.use_ldaps || false,
|
||||
@@ -76,6 +98,7 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
admin_password: '',
|
||||
base_dn: res.base_dn || '',
|
||||
search_filter: res.search_filter || '',
|
||||
user_mapping: mapping,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
@@ -87,7 +110,10 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
const handleLdapSave = async () => {
|
||||
setLdapSaving(true);
|
||||
try {
|
||||
await ldapApi.updateConfig(ldapForm);
|
||||
await ldapApi.updateConfig({
|
||||
...ldapForm,
|
||||
user_mapping: ldapForm.user_mapping,
|
||||
});
|
||||
await loadLdapConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to save LDAP config:', error);
|
||||
@@ -101,7 +127,10 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
setLdapTesting(true);
|
||||
setLdapTestResult(null);
|
||||
try {
|
||||
await ldapApi.updateConfig(ldapForm);
|
||||
await ldapApi.updateConfig({
|
||||
...ldapForm,
|
||||
user_mapping: ldapForm.user_mapping,
|
||||
});
|
||||
const res = await ldapApi.testConnection();
|
||||
setLdapTestResult(res?.message || (res?.success ? '连接成功' : '连接失败'));
|
||||
} catch (error) {
|
||||
|
||||
Reference in new issue
Block a user