diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py
index 660b9e7d96..810d0f4c8d 100644
--- a/backend/open_webui/routers/auths.py
+++ b/backend/open_webui/routers/auths.py
@@ -14,6 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import JSONResponse, Response
from ldap3 import NONE, Connection, Server, Tls
from ldap3.utils.conv import escape_filter_chars
+from ldap3.utils.dn import parse_dn
from open_webui.config import (
ENABLE_PASSWORD_AUTH,
OAUTH_PROVIDERS,
@@ -128,6 +129,9 @@ LDAP_SERVER_CONFIG_KEYS = {
'certificate_path': 'ldap.server.ca_cert_file',
'validate_cert': 'ldap.server.validate_cert',
'ciphers': 'ldap.server.ciphers',
+ 'enable_group_management': 'ldap.group.enable_management',
+ 'enable_group_creation': 'ldap.group.enable_creation',
+ 'attribute_for_groups': 'ldap.server.attribute_for_groups',
}
@@ -398,6 +402,52 @@ async def update_password(
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
+def _unescape_ldap_dn_value(value: str) -> str:
+ """Resolve RFC 4514 escapes in a DN value, e.g. ``CN=Sales\\, EMEA`` -> ``Sales, EMEA``.
+
+ Consecutive ``\\XX`` hex escapes encode UTF-8 bytes and are decoded together.
+ """
+ hexdigits = '0123456789abcdefABCDEF'
+ result = []
+ pos = 0
+ length = len(value)
+ while pos < length:
+ char = value[pos]
+ if char == '\\' and pos + 1 < length:
+ if pos + 2 < length and value[pos + 1] in hexdigits and value[pos + 2] in hexdigits:
+ byte_values = bytearray()
+ while (
+ pos + 2 < length
+ and value[pos] == '\\'
+ and value[pos + 1] in hexdigits
+ and value[pos + 2] in hexdigits
+ ):
+ byte_values.append(int(value[pos + 1 : pos + 3], 16))
+ pos += 3
+ result.append(byte_values.decode('utf-8', errors='replace'))
+ else:
+ # Backslash escaping a literal special char, e.g. "\," or "\+".
+ result.append(value[pos + 1])
+ pos += 2
+ else:
+ result.append(char)
+ pos += 1
+ return ''.join(result)
+
+
+def extract_group_cn_from_dn(group_dn: str) -> str | None:
+ """Return the first CN component of an LDAP group DN, or None.
+
+ Uses ``parse_dn`` so escaped separators inside a value (e.g. a group whose
+ name contains a comma) are handled correctly instead of naively splitting
+ on ``,``.
+ """
+ for attr_type, attr_value, _ in parse_dn(group_dn):
+ if attr_type.upper() == 'CN':
+ return _unescape_ldap_dn_value(attr_value)
+ return None
+
+
############################
# LDAP Authentication
############################
@@ -545,17 +595,10 @@ async def ldap_auth(
log.info(f'Processing group DN #{group_idx + 1}: {group_dn}')
try:
- group_cn = None
-
- for item in group_dn.split(','):
- item = item.strip()
- if item.upper().startswith('CN='):
- group_cn = item[3:]
- break
+ group_cn = extract_group_cn_from_dn(group_dn)
if group_cn:
user_groups.append(group_cn)
-
else:
log.warning(f'Could not extract CN from group DN: {group_dn}')
except Exception as e:
@@ -627,9 +670,9 @@ async def ldap_auth(
if user:
if ENABLE_LDAP_GROUP_MANAGEMENT and user_groups:
- if ENABLE_LDAP_GROUP_CREATION:
- await Groups.create_groups_by_group_names(user.id, user_groups, db=db)
try:
+ if ENABLE_LDAP_GROUP_CREATION:
+ await Groups.create_groups_by_group_names(user.id, user_groups, db=db)
await Groups.sync_groups_by_group_names(user.id, user_groups, db=db)
log.info(f'Successfully synced groups for user {user.id}: {user_groups}')
except Exception as e:
@@ -1188,6 +1231,9 @@ class LdapServerConfig(BaseModel):
certificate_path: str | None = None
validate_cert: bool = True
ciphers: str | None = 'ALL'
+ enable_group_management: bool = False
+ enable_group_creation: bool = False
+ attribute_for_groups: str = 'memberOf'
@router.get('/admin/config/ldap/server', response_model=LdapServerConfig)
@@ -1209,6 +1255,11 @@ async def update_ldap_server(request: Request, form_data: LdapServerConfig, user
if not value:
raise HTTPException(400, detail=ERROR_MESSAGES.REQUIRED_FIELD_EMPTY(key))
+ # The group attribute is what group management reads from the directory
+ # entry; an empty value would make group sync silently do nothing.
+ if form_data.enable_group_management and not (form_data.attribute_for_groups or '').strip():
+ raise HTTPException(400, detail=ERROR_MESSAGES.REQUIRED_FIELD_EMPTY('attribute_for_groups'))
+
updates = config_updates(form_data.model_dump(), LDAP_SERVER_CONFIG_KEYS)
updates['ldap.server.app_dn'] = form_data.app_dn or ''
updates['ldap.server.app_password'] = form_data.app_dn_password or ''
diff --git a/src/lib/components/admin/Settings/Authentication.svelte b/src/lib/components/admin/Settings/Authentication.svelte
index ae14ac0ea5..d625311842 100644
--- a/src/lib/components/admin/Settings/Authentication.svelte
+++ b/src/lib/components/admin/Settings/Authentication.svelte
@@ -32,7 +32,7 @@
let LDAP_SERVER = {
label: '',
host: '',
- port: '',
+ port: null,
attribute_for_mail: 'mail',
attribute_for_username: 'uid',
app_dn: '',
@@ -42,7 +42,10 @@
use_tls: false,
validate_cert: false,
certificate_path: '',
- ciphers: ''
+ ciphers: '',
+ enable_group_management: false,
+ enable_group_creation: false,
+ attribute_for_groups: 'memberOf'
};
let oauthConfig: any = null;
@@ -55,6 +58,13 @@
await updateLdapConfig(localStorage.token, ENABLE_LDAP);
if (!ENABLE_LDAP) return true;
+ // Honor the "Default to memberOf" hint: fall back to the default group
+ // attribute when it is left blank while group management is enabled, so
+ // the save isn't rejected by the backend's required-field check.
+ if (LDAP_SERVER.enable_group_management && !LDAP_SERVER.attribute_for_groups?.trim()) {
+ LDAP_SERVER.attribute_for_groups = 'memberOf';
+ }
+
const res = await updateLdapServer(localStorage.token, LDAP_SERVER).catch((error) => {
toast.error(`${error}`);
return null;
@@ -104,7 +114,9 @@
groups = await getGroups(localStorage.token);
})(),
(async () => {
- LDAP_SERVER = await getLdapServer(localStorage.token);
+ // Merge into the defaults so any key the backend omits (e.g. an
+ // older backend without the group settings) keeps its default.
+ LDAP_SERVER = { ...LDAP_SERVER, ...(await getLdapServer(localStorage.token)) };
})(),
(async () => {
oauthConfig = await getOAuthConfig(localStorage.token).catch(() => null);
@@ -465,6 +477,35 @@
{/if}
+
+
+
+
+
+ {#if LDAP_SERVER.enable_group_management}
+
+
+
+
+
+
+
+
+
+ {/if}
{/if}