feat: LDAP-User-Sync in Mandanten-Benutzerliste
Neuer Endpoint POST /api/admin/tenants/{id}/ldap/sync importiert alle
LDAP-User (source=ldap) per UpsertLDAPUser in die Tenant-Benutzerliste.
Im Nutzer-Dialog erscheint ein "LDAP-Benutzer synchronisieren"-Button
wenn LDAP für den Mandanten aktiv ist. Unterstützt Univention UCS
(mailPrimaryAddress, inetOrgPerson). Benutzertabelle zeigt jetzt auch
die Quelle (local/ldap).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -475,12 +475,14 @@ func (s *Server) SetTenantLDAP(store *ldapcfg.TenantStore) {
|
||||
s.mux.HandleFunc("PUT /api/tenant/ldap", s.authAdmin(s.handleSaveTenantLDAP))
|
||||
s.mux.HandleFunc("DELETE /api/tenant/ldap", s.authAdmin(s.handleDeleteTenantLDAP))
|
||||
s.mux.HandleFunc("POST /api/tenant/ldap/test", s.authAdmin(s.handleTestTenantLDAP))
|
||||
s.mux.HandleFunc("POST /api/tenant/ldap/sync", s.authAdmin(s.handleSyncTenantLDAP))
|
||||
|
||||
// superadmin routes — tenant_id from URL parameter
|
||||
s.mux.HandleFunc("GET /api/admin/tenants/{id}/ldap", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminGetTenantLDAP)))
|
||||
s.mux.HandleFunc("PUT /api/admin/tenants/{id}/ldap", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminSaveTenantLDAP)))
|
||||
s.mux.HandleFunc("DELETE /api/admin/tenants/{id}/ldap", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminDeleteTenantLDAP)))
|
||||
s.mux.HandleFunc("POST /api/admin/tenants/{id}/ldap/test", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminTestTenantLDAP)))
|
||||
s.mux.HandleFunc("POST /api/admin/tenants/{id}/ldap/sync", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminSyncTenantLDAP)))
|
||||
}
|
||||
|
||||
// ── domain_admin handlers (own tenant) ──────────────────────────────────────
|
||||
@@ -769,6 +771,102 @@ func (s *Server) handleAdminTestTenantLDAP(w http.ResponseWriter, r *http.Reques
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
// ── LDAP Sync handlers ───────────────────────────────────────────────────────
|
||||
|
||||
type syncResult struct {
|
||||
Synced int `json:"synced"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
// handleSyncTenantLDAP imports all LDAP users into the tenant (domain_admin).
|
||||
func (s *Server) handleSyncTenantLDAP(w http.ResponseWriter, r *http.Request) {
|
||||
if s.tenantLdapStore == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
|
||||
return
|
||||
}
|
||||
sess := sessionFromCtx(r.Context())
|
||||
if sess.TenantID == nil {
|
||||
writeError(w, http.StatusBadRequest, "no tenant context")
|
||||
return
|
||||
}
|
||||
res := s.doSyncTenantLDAP(r, *sess.TenantID)
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: "tenant_ldap_sync",
|
||||
Username: sess.Username,
|
||||
IPAddress: s.remoteIP(r),
|
||||
Success: len(res.Errors) == 0,
|
||||
Detail: fmt.Sprintf("%d Benutzer synchronisiert", res.Synced),
|
||||
})
|
||||
writeJSON(w, http.StatusOK, res)
|
||||
}
|
||||
|
||||
// handleAdminSyncTenantLDAP imports all LDAP users into any tenant (superadmin).
|
||||
func (s *Server) handleAdminSyncTenantLDAP(w http.ResponseWriter, r *http.Request) {
|
||||
if s.tenantLdapStore == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
|
||||
return
|
||||
}
|
||||
id, err := parseTenantID(r)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid tenant id")
|
||||
return
|
||||
}
|
||||
res := s.doSyncTenantLDAP(r, id)
|
||||
sess := sessionFromCtx(r.Context())
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: "tenant_ldap_sync",
|
||||
Username: sess.Username,
|
||||
IPAddress: s.remoteIP(r),
|
||||
Success: len(res.Errors) == 0,
|
||||
Detail: fmt.Sprintf("%d Benutzer synchronisiert (tenant %d)", res.Synced, id),
|
||||
})
|
||||
writeJSON(w, http.StatusOK, res)
|
||||
}
|
||||
|
||||
// doSyncTenantLDAP is the shared sync logic: fetch all LDAP users and upsert
|
||||
// them into the local userstore with source='ldap'.
|
||||
func (s *Server) doSyncTenantLDAP(r *http.Request, tenantID int64) syncResult {
|
||||
saved, err := s.tenantLdapStore.GetWithPassword(r.Context(), tenantID)
|
||||
if err != nil || saved == nil {
|
||||
return syncResult{Errors: []string{"keine LDAP-Konfiguration vorhanden"}}
|
||||
}
|
||||
|
||||
cfg := ldapauth.Config{
|
||||
URL: saved.URL,
|
||||
BindDN: saved.BindDN,
|
||||
BindPassword: saved.BindPassword,
|
||||
BaseDN: saved.BaseDN,
|
||||
UserFilter: saved.UserFilter,
|
||||
TLS: saved.TLS,
|
||||
TLSSkipVerify: saved.TLSSkipVerify,
|
||||
}
|
||||
|
||||
ldapUsers, err := ldapauth.FetchUsers(cfg)
|
||||
if err != nil {
|
||||
return syncResult{Errors: []string{err.Error()}}
|
||||
}
|
||||
|
||||
defaultRole := saved.DefaultRole
|
||||
if defaultRole == "" {
|
||||
defaultRole = "user"
|
||||
}
|
||||
|
||||
var res syncResult
|
||||
res.Errors = []string{}
|
||||
for _, u := range ldapUsers {
|
||||
mail := u.Mail
|
||||
if mail == "" {
|
||||
mail = u.UID + "@ldap.local"
|
||||
}
|
||||
if _, uErr := s.users.UpsertLDAPUser(u.UID, mail, defaultRole, &tenantID); uErr != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("%s: %s", u.UID, uErr.Error()))
|
||||
} else {
|
||||
res.Synced++
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// buildTenantTestConfig constructs an ldapauth.Config for testing, either from
|
||||
// the saved tenant config or from the provided request body.
|
||||
func (s *Server) buildTenantTestConfig(r *http.Request, useSaved bool, tenantID int64, provided ldapcfg.TenantLDAPConfig) *ldapauth.Config {
|
||||
|
||||
Reference in New Issue
Block a user