fix(PROJ-23): Privilege Escalation in Tenant-LDAP + Login-Reihenfolge

- BUG-1 (P0): domain_admin kann keine Rollen > auditor in default_role/
  group_mappings setzen — serverseitige Allowlist-Prüfung in
  handleSaveTenantLDAP (user/auditor) und handleAdminSaveTenantLDAP
  (user/auditor/domain_admin)
- WARN-1: Login-Fallback-Reihenfolge korrigiert — tenant_ldap wird
  jetzt VOR globalem ldap_config geprüft (Spec: tenant > global > local)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-18 00:32:47 +01:00
parent 9e7add31cd
commit 787db6638f
2 changed files with 69 additions and 46 deletions
+42 -46
View File
@@ -68,50 +68,9 @@ func (m *Manager) Login(username, password string) (string, *userstore.User, err
return m.issueToken(user)
}
// 2. Global LDAP fallback when the store is wired and the config is enabled.
if m.ldapStore != nil {
cfg, ldapErr := m.ldapStore.GetWithPassword(context.Background())
if ldapErr == nil && cfg != nil && cfg.Enabled {
attrs, authErr := ldapauth.Authenticate(ldapauth.Config{
URL: cfg.URL,
BindDN: cfg.BindDN,
BindPassword: cfg.BindPassword,
BaseDN: cfg.BaseDN,
UserFilter: cfg.UserFilter,
TLS: cfg.TLS,
TLSSkipVerify: cfg.TLSSkipVerify,
}, username, password)
if authErr == nil {
// Determine role: check group_mappings first, fall back to default_role.
role := cfg.DefaultRole
if role == "" {
role = userstore.RoleUser
}
memberOf := attrs["memberOf"]
if memberOf != "" {
for _, gm := range cfg.GroupMappings {
if gm.GroupDN != "" && containsGroup(memberOf, gm.GroupDN) {
role = gm.Role
break
}
}
}
email := attrs["mail"]
if email == "" {
email = username + "@ldap.local"
}
ldapUser, upsertErr := m.store.UpsertLDAPUser(username, email, role, nil)
if upsertErr == nil {
return m.issueToken(ldapUser)
}
}
}
}
// 3. PROJ-23: Per-tenant LDAP fallback — extract domain from username,
// look up the tenant, and try LDAP auth with that tenant's config.
// 2. PROJ-23: Per-tenant LDAP — checked first so tenant config takes priority
// over global LDAP (spec: tenant_ldap > ldap_config > config.yml).
// Extract domain from username (user@domain.tld), resolve tenant, try auth.
if m.tenantLdapStore != nil && m.tenantLookup != nil {
if domain := extractDomain(username); domain != "" {
ctx := context.Background()
@@ -142,12 +101,10 @@ func (m *Manager) Login(username, password string) (string, *userstore.User, err
}
}
}
email := attrs["mail"]
if email == "" {
email = username
}
ldapUser, upsertErr := m.store.UpsertLDAPUser(username, email, role, tenantID)
if upsertErr == nil {
return m.issueToken(ldapUser)
@@ -158,6 +115,45 @@ func (m *Manager) Login(username, password string) (string, *userstore.User, err
}
}
// 3. Global LDAP fallback — only reached if no matching tenant LDAP config found.
if m.ldapStore != nil {
cfg, ldapErr := m.ldapStore.GetWithPassword(context.Background())
if ldapErr == nil && cfg != nil && cfg.Enabled {
attrs, authErr := ldapauth.Authenticate(ldapauth.Config{
URL: cfg.URL,
BindDN: cfg.BindDN,
BindPassword: cfg.BindPassword,
BaseDN: cfg.BaseDN,
UserFilter: cfg.UserFilter,
TLS: cfg.TLS,
TLSSkipVerify: cfg.TLSSkipVerify,
}, username, password)
if authErr == nil {
role := cfg.DefaultRole
if role == "" {
role = userstore.RoleUser
}
memberOf := attrs["memberOf"]
if memberOf != "" {
for _, gm := range cfg.GroupMappings {
if gm.GroupDN != "" && containsGroup(memberOf, gm.GroupDN) {
role = gm.Role
break
}
}
}
email := attrs["mail"]
if email == "" {
email = username + "@ldap.local"
}
ldapUser, upsertErr := m.store.UpsertLDAPUser(username, email, role, nil)
if upsertErr == nil {
return m.issueToken(ldapUser)
}
}
}
}
return "", nil, fmt.Errorf("auth: login: invalid credentials")
}