package main import ( "context" "crypto/tls" "encoding/binary" "errors" "fmt" "net" "net/http" "net/url" "sort" "strings" "time" "github.com/go-ldap/ldap/v3" "github.com/jackc/pgx/v5" ) type activeDirectoryCredentials struct { ServerURL string BaseDN string BindUsername string BindPassword string UserGroup string SkipTLSVerify bool } type activeDirectoryUser struct { ID string `json:"id"` DisplayName string `json:"displayName"` Username string `json:"username"` Email string `json:"email"` Domain string `json:"domain"` DistinguishedName string `json:"distinguishedName"` SID string `json:"sid"` Source string `json:"source"` } type activeDirectorySettingsResponse struct { ServerURL string `json:"serverUrl"` BaseDN string `json:"baseDn"` BindUsername string `json:"bindUsername"` BindPasswordConfigured bool `json:"bindPasswordConfigured"` UserGroup string `json:"userGroup"` SkipTLSVerify bool `json:"skipTlsVerify"` Configured bool `json:"configured"` UpdatedAt *time.Time `json:"updatedAt,omitempty"` } type updateActiveDirectorySettingsRequest struct { ServerURL string `json:"serverUrl"` BaseDN string `json:"baseDn"` BindUsername string `json:"bindUsername"` BindPassword string `json:"bindPassword"` UserGroup string `json:"userGroup"` SkipTLSVerify bool `json:"skipTlsVerify"` } type activeDirectoryBaseDNTreeRequest struct { ServerURL string `json:"serverUrl"` BindUsername string `json:"bindUsername"` BindPassword string `json:"bindPassword"` BaseDN string `json:"baseDn"` SkipTLSVerify bool `json:"skipTlsVerify"` } type activeDirectoryBaseDNTreeNode struct { ID string `json:"id"` Name string `json:"name"` Path string `json:"path"` SecondaryName string `json:"secondaryName,omitempty"` Children []activeDirectoryBaseDNTreeNode `json:"children,omitempty"` Disabled bool `json:"disabled,omitempty"` } func (s *Server) handleGetActiveDirectorySettings(w http.ResponseWriter, r *http.Request) { settings, err := s.getActiveDirectorySettings(r.Context()) if errors.Is(err, pgx.ErrNoRows) { writeJSON(w, http.StatusOK, map[string]any{ "settings": defaultActiveDirectorySettingsResponse(), }) return } if err != nil { writeError(w, http.StatusInternalServerError, "AD-Einstellungen konnten nicht geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{"settings": settings}) } func (s *Server) handleUpdateActiveDirectorySettings(w http.ResponseWriter, r *http.Request) { var input updateActiveDirectorySettingsRequest if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } serverURL := normalizeActiveDirectoryServerURL(input.ServerURL) baseDN := strings.TrimSpace(input.BaseDN) bindUsername := strings.TrimSpace(input.BindUsername) bindPassword := strings.TrimSpace(input.BindPassword) userGroup := strings.TrimSpace(input.UserGroup) skipTLSVerify := input.SkipTLSVerify currentSettings, err := s.getActiveDirectorySettings(r.Context()) passwordAlreadyConfigured := false if err == nil { passwordAlreadyConfigured = currentSettings.BindPasswordConfigured } else if !errors.Is(err, pgx.ErrNoRows) { writeError(w, http.StatusInternalServerError, "AD-Einstellungen konnten nicht geprueft werden") return } configured := serverURL != "" || baseDN != "" || bindUsername != "" || bindPassword != "" if configured { if serverURL == "" { writeError(w, http.StatusBadRequest, "AD-Server ist erforderlich") return } if baseDN == "" { writeError(w, http.StatusBadRequest, "AD-Base-DN ist erforderlich") return } if bindUsername == "" { writeError(w, http.StatusBadRequest, "AD-Bind-Benutzer ist erforderlich") return } if bindPassword == "" && !passwordAlreadyConfigured { writeError(w, http.StatusBadRequest, "AD-Bind-Kennwort ist erforderlich") return } } _, err = s.db.Exec( r.Context(), ` INSERT INTO milestone_settings ( id, active_directory_server_url, active_directory_base_dn, active_directory_bind_username, active_directory_bind_password_encrypted, active_directory_user_group, active_directory_skip_tls_verify ) VALUES ( 1, $1, $2, $3, CASE WHEN $4 <> '' THEN pgp_sym_encrypt($4, $5) ELSE NULL END, $6, $7 ) ON CONFLICT (id) DO UPDATE SET active_directory_server_url = EXCLUDED.active_directory_server_url, active_directory_base_dn = EXCLUDED.active_directory_base_dn, active_directory_bind_username = EXCLUDED.active_directory_bind_username, active_directory_bind_password_encrypted = CASE WHEN $1 = '' AND $2 = '' AND $3 = '' AND $4 = '' THEN NULL WHEN $4 <> '' THEN EXCLUDED.active_directory_bind_password_encrypted ELSE milestone_settings.active_directory_bind_password_encrypted END, active_directory_user_group = EXCLUDED.active_directory_user_group, active_directory_skip_tls_verify = EXCLUDED.active_directory_skip_tls_verify, updated_at = now() `, serverURL, baseDN, bindUsername, bindPassword, s.milestoneEncryptionKey(), userGroup, skipTLSVerify, ) if err != nil { writeError(w, http.StatusInternalServerError, "AD-Einstellungen konnten nicht gespeichert werden") return } settings, err := s.getActiveDirectorySettings(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "AD-Einstellungen wurden gespeichert, konnten aber nicht neu geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{"settings": settings}) } func (s *Server) handleListActiveDirectoryBaseDNTree(w http.ResponseWriter, r *http.Request) { var input activeDirectoryBaseDNTreeRequest if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } credentials, ok := s.activeDirectoryBrowseCredentials(w, r, input) if !ok { return } nodes, err := listActiveDirectoryBaseDNTree(credentials) if err != nil { logError("AD-Base-DN-Struktur konnte nicht geladen werden", err, LogFields{ "serverUrl": credentials.ServerURL, }) writeError(w, http.StatusBadGateway, "AD-Base-DN-Struktur konnte nicht geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{ "nodes": nodes, }) } func (s *Server) handleListActiveDirectoryGroupTree(w http.ResponseWriter, r *http.Request) { var input activeDirectoryBaseDNTreeRequest if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } credentials, ok := s.activeDirectoryBrowseCredentials(w, r, input) if !ok { return } nodes, err := listActiveDirectoryGroupTree(credentials) if err != nil { logError("AD-Gruppenstruktur konnte nicht geladen werden", err, LogFields{ "serverUrl": credentials.ServerURL, "baseDn": credentials.BaseDN, }) writeError(w, http.StatusBadGateway, "AD-Gruppenstruktur konnte nicht geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{ "nodes": nodes, }) } func (s *Server) activeDirectoryBrowseCredentials( w http.ResponseWriter, r *http.Request, input activeDirectoryBaseDNTreeRequest, ) (activeDirectoryCredentials, bool) { credentials := activeDirectoryCredentials{ ServerURL: normalizeActiveDirectoryServerURL(input.ServerURL), BaseDN: strings.TrimSpace(input.BaseDN), BindUsername: strings.TrimSpace(input.BindUsername), BindPassword: strings.TrimSpace(input.BindPassword), SkipTLSVerify: input.SkipTLSVerify, } storedCredentials, err := s.getActiveDirectoryCredentials(r.Context()) if err != nil && !errors.Is(err, pgx.ErrNoRows) { writeError(w, http.StatusInternalServerError, "AD-Einstellungen konnten nicht geladen werden") return credentials, false } if credentials.ServerURL == "" { credentials.ServerURL = storedCredentials.ServerURL } if credentials.BindUsername == "" { credentials.BindUsername = storedCredentials.BindUsername } if credentials.BindPassword == "" { credentials.BindPassword = storedCredentials.BindPassword } if credentials.BaseDN == "" { credentials.BaseDN = storedCredentials.BaseDN } if credentials.ServerURL == "" || credentials.BindUsername == "" || credentials.BindPassword == "" { writeError(w, http.StatusBadRequest, "AD-Server, Bind-Benutzer und Bind-Kennwort sind erforderlich") return credentials, false } return credentials, true } func (s *Server) handleListOperationActiveDirectoryUsers(w http.ResponseWriter, r *http.Request) { credentials, err := s.getActiveDirectoryCredentials(r.Context()) if errors.Is(err, pgx.ErrNoRows) { writeJSON(w, http.StatusOK, map[string]any{ "configured": false, "users": []activeDirectoryUser{}, }) return } if err != nil { writeError(w, http.StatusInternalServerError, "AD-Einstellungen konnten nicht geladen werden") return } if !credentials.Configured() { writeJSON(w, http.StatusOK, map[string]any{ "configured": false, "users": []activeDirectoryUser{}, }) return } users, err := listActiveDirectoryUsers(r.Context(), credentials) if err != nil { logError("AD-Benutzer konnten nicht geladen werden", err, LogFields{ "baseDn": credentials.BaseDN, }) writeError(w, http.StatusBadGateway, "AD-Benutzer konnten nicht geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{ "configured": true, "baseDn": credentials.BaseDN, "users": users, }) } func defaultActiveDirectorySettingsResponse() activeDirectorySettingsResponse { settings := activeDirectorySettingsResponse{} settings.Configured = settings.BindPasswordConfigured && settings.ServerURL != "" && settings.BaseDN != "" && settings.BindUsername != "" return settings } func (s *Server) getActiveDirectorySettings(ctx context.Context) (activeDirectorySettingsResponse, error) { var settings activeDirectorySettingsResponse var updatedAt time.Time err := s.db.QueryRow( ctx, ` SELECT COALESCE(active_directory_server_url, ''), COALESCE(active_directory_base_dn, ''), COALESCE(active_directory_bind_username, ''), active_directory_bind_password_encrypted IS NOT NULL, COALESCE(active_directory_user_group, ''), COALESCE(active_directory_skip_tls_verify, false), updated_at FROM milestone_settings WHERE id = 1 LIMIT 1 `, ).Scan( &settings.ServerURL, &settings.BaseDN, &settings.BindUsername, &settings.BindPasswordConfigured, &settings.UserGroup, &settings.SkipTLSVerify, &updatedAt, ) if err != nil { return settings, err } settings.UpdatedAt = &updatedAt settings.Configured = settings.BindPasswordConfigured && settings.ServerURL != "" && settings.BaseDN != "" && settings.BindUsername != "" return settings, nil } func (c activeDirectoryCredentials) Configured() bool { return strings.TrimSpace(c.ServerURL) != "" && strings.TrimSpace(c.BaseDN) != "" && strings.TrimSpace(c.BindUsername) != "" && strings.TrimSpace(c.BindPassword) != "" } func (s *Server) getActiveDirectoryCredentials(ctx context.Context) (activeDirectoryCredentials, error) { var credentials activeDirectoryCredentials err := s.db.QueryRow( ctx, ` SELECT COALESCE(active_directory_server_url, ''), COALESCE(active_directory_base_dn, ''), COALESCE(active_directory_bind_username, ''), COALESCE(pgp_sym_decrypt(active_directory_bind_password_encrypted, $1), ''), COALESCE(active_directory_user_group, ''), COALESCE(active_directory_skip_tls_verify, false) FROM milestone_settings WHERE id = 1 LIMIT 1 `, s.milestoneEncryptionKey(), ).Scan( &credentials.ServerURL, &credentials.BaseDN, &credentials.BindUsername, &credentials.BindPassword, &credentials.UserGroup, &credentials.SkipTLSVerify, ) credentials.ServerURL = normalizeActiveDirectoryServerURL(credentials.ServerURL) credentials.BaseDN = strings.TrimSpace(credentials.BaseDN) credentials.BindUsername = strings.TrimSpace(credentials.BindUsername) credentials.BindPassword = strings.TrimSpace(credentials.BindPassword) credentials.UserGroup = strings.TrimSpace(credentials.UserGroup) return credentials, err } func listActiveDirectoryUsers( ctx context.Context, credentials activeDirectoryCredentials, ) ([]activeDirectoryUser, error) { conn, err := dialAndBindActiveDirectory(credentials) if err != nil { return nil, err } defer conn.Close() searchRequest := ldap.NewSearchRequest( credentials.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, "(&"+ "(objectCategory=person)"+ "(objectClass=user)"+ "(!(userAccountControl:1.2.840.113556.1.4.803:=2))"+ ")", []string{ "displayName", "sAMAccountName", "userPrincipalName", "mail", "distinguishedName", "objectSid", "cn", }, nil, ) searchResult, err := conn.Search(searchRequest) if err != nil { return nil, err } users := make([]activeDirectoryUser, 0, len(searchResult.Entries)) seen := map[string]bool{} for _, entry := range searchResult.Entries { user := activeDirectoryUserFromLDAPEntry(entry) if user.ID == "" { continue } if seen[user.ID] { continue } seen[user.ID] = true users = append(users, user) } sort.SliceStable(users, func(i, j int) bool { return strings.ToLower(users[i].DisplayName) < strings.ToLower(users[j].DisplayName) }) return users, nil } func dialAndBindActiveDirectory(credentials activeDirectoryCredentials) (*ldap.Conn, error) { conn, err := ldap.DialURL( credentials.ServerURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: credentials.SkipTLSVerify}), ldap.DialWithDialer(&net.Dialer{Timeout: 15 * time.Second}), ) if err != nil { return nil, err } if err := conn.Bind(credentials.BindUsername, credentials.BindPassword); err != nil { conn.Close() return nil, err } return conn, nil } func listActiveDirectoryBaseDNTree( credentials activeDirectoryCredentials, ) ([]activeDirectoryBaseDNTreeNode, error) { conn, err := dialAndBindActiveDirectory(credentials) if err != nil { return nil, err } defer conn.Close() namingContexts, err := listActiveDirectoryNamingContexts(conn, credentials.BaseDN) if err != nil { return nil, err } const maxNodes = 500 const maxDepth = 3 remainingNodes := maxNodes nodes := make([]activeDirectoryBaseDNTreeNode, 0, len(namingContexts)) for _, namingContext := range namingContexts { node := activeDirectoryBaseDNTreeNodeFromDN(namingContext) if remainingNodes > 0 { children, err := listActiveDirectoryBaseDNChildren( conn, namingContext, 0, maxDepth, &remainingNodes, ) if err == nil { node.Children = children } else { logError("AD-Base-DN-Unterstruktur konnte nicht geladen werden", err, LogFields{ "baseDn": namingContext, }) } } nodes = append(nodes, node) } return nodes, nil } func listActiveDirectoryGroupTree( credentials activeDirectoryCredentials, ) ([]activeDirectoryBaseDNTreeNode, error) { conn, err := dialAndBindActiveDirectory(credentials) if err != nil { return nil, err } defer conn.Close() roots := []string{} if strings.TrimSpace(credentials.BaseDN) != "" { roots = append(roots, strings.TrimSpace(credentials.BaseDN)) } else { roots, err = listActiveDirectoryNamingContexts(conn, "") if err != nil { return nil, err } } const maxNodes = 1000 const maxDepth = 5 remainingNodes := maxNodes nodes := make([]activeDirectoryBaseDNTreeNode, 0, len(roots)) for _, root := range roots { node := activeDirectoryBaseDNTreeNodeFromDN(root) node.Disabled = true if remainingNodes > 0 { children, err := listActiveDirectoryGroupTreeChildren( conn, root, 0, maxDepth, &remainingNodes, ) if err == nil { node.Children = children } else { logError("AD-Gruppen-Unterstruktur konnte nicht geladen werden", err, LogFields{ "baseDn": root, }) } } nodes = append(nodes, node) } return nodes, nil } func listActiveDirectoryNamingContexts(conn *ldap.Conn, fallbackBaseDN string) ([]string, error) { searchRequest := ldap.NewSearchRequest( "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 10, false, "(objectClass=*)", []string{"defaultNamingContext", "namingContexts"}, nil, ) searchResult, err := conn.Search(searchRequest) if err != nil { if strings.TrimSpace(fallbackBaseDN) != "" { return []string{strings.TrimSpace(fallbackBaseDN)}, nil } return nil, err } seen := map[string]bool{} namingContexts := []string{} addNamingContext := func(value string) { value = strings.TrimSpace(value) if value == "" || seen[strings.ToLower(value)] { return } seen[strings.ToLower(value)] = true namingContexts = append(namingContexts, value) } if len(searchResult.Entries) > 0 { entry := searchResult.Entries[0] addNamingContext(entry.GetAttributeValue("defaultNamingContext")) for _, namingContext := range entry.GetAttributeValues("namingContexts") { addNamingContext(namingContext) } } addNamingContext(fallbackBaseDN) if len(namingContexts) == 0 { return nil, fmt.Errorf("keine AD-Naming-Contexts gefunden") } return namingContexts, nil } func listActiveDirectoryBaseDNChildren( conn *ldap.Conn, parentDN string, depth int, maxDepth int, remainingNodes *int, ) ([]activeDirectoryBaseDNTreeNode, error) { if depth >= maxDepth || *remainingNodes <= 0 { return nil, nil } searchRequest := ldap.NewSearchRequest( parentDN, ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 10, false, "(|(objectClass=domainDNS)(objectClass=organizationalUnit)(objectClass=container))", []string{ "cn", "distinguishedName", "name", "objectClass", "ou", }, nil, ) searchResult, err := conn.Search(searchRequest) if err != nil { return nil, err } nodes := make([]activeDirectoryBaseDNTreeNode, 0, len(searchResult.Entries)) for _, entry := range searchResult.Entries { if *remainingNodes <= 0 { break } distinguishedName := strings.TrimSpace(entry.GetAttributeValue("distinguishedName")) if distinguishedName == "" { distinguishedName = strings.TrimSpace(entry.DN) } if distinguishedName == "" { continue } *remainingNodes = *remainingNodes - 1 node := activeDirectoryBaseDNTreeNodeFromEntry(entry, distinguishedName) children, err := listActiveDirectoryBaseDNChildren( conn, distinguishedName, depth+1, maxDepth, remainingNodes, ) if err == nil { node.Children = children } else { logError("AD-Base-DN-Unterstruktur konnte nicht geladen werden", err, LogFields{ "baseDn": distinguishedName, }) } nodes = append(nodes, node) } sort.SliceStable(nodes, func(i, j int) bool { return strings.ToLower(nodes[i].Name) < strings.ToLower(nodes[j].Name) }) return nodes, nil } func listActiveDirectoryGroupTreeChildren( conn *ldap.Conn, parentDN string, depth int, maxDepth int, remainingNodes *int, ) ([]activeDirectoryBaseDNTreeNode, error) { if depth >= maxDepth || *remainingNodes <= 0 { return nil, nil } searchRequest := ldap.NewSearchRequest( parentDN, ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 15, false, "(|(objectClass=domainDNS)(objectClass=organizationalUnit)(objectClass=container)(objectClass=group))", []string{ "cn", "distinguishedName", "name", "objectClass", "ou", "sAMAccountName", }, nil, ) searchResult, err := conn.Search(searchRequest) if err != nil { return nil, err } nodes := make([]activeDirectoryBaseDNTreeNode, 0, len(searchResult.Entries)) for _, entry := range searchResult.Entries { if *remainingNodes <= 0 { break } distinguishedName := strings.TrimSpace(entry.GetAttributeValue("distinguishedName")) if distinguishedName == "" { distinguishedName = strings.TrimSpace(entry.DN) } if distinguishedName == "" { continue } *remainingNodes = *remainingNodes - 1 isGroup := activeDirectoryEntryHasObjectClass(entry, "group") node := activeDirectoryBaseDNTreeNodeFromEntry(entry, distinguishedName) node.Disabled = !isGroup if isGroup { samAccountName := strings.TrimSpace(entry.GetAttributeValue("sAMAccountName")) if samAccountName != "" { node.SecondaryName = samAccountName } } if !isGroup { children, err := listActiveDirectoryGroupTreeChildren( conn, distinguishedName, depth+1, maxDepth, remainingNodes, ) if err == nil { node.Children = children } else { logError("AD-Gruppen-Unterstruktur konnte nicht geladen werden", err, LogFields{ "baseDn": distinguishedName, }) } } if isGroup || len(node.Children) > 0 { nodes = append(nodes, node) } } sort.SliceStable(nodes, func(i, j int) bool { if nodes[i].Disabled != nodes[j].Disabled { return !nodes[i].Disabled } return strings.ToLower(nodes[i].Name) < strings.ToLower(nodes[j].Name) }) return nodes, nil } func activeDirectoryEntryHasObjectClass(entry *ldap.Entry, objectClass string) bool { for _, value := range entry.GetAttributeValues("objectClass") { if strings.EqualFold(value, objectClass) { return true } } return false } func activeDirectoryBaseDNTreeNodeFromEntry( entry *ldap.Entry, distinguishedName string, ) activeDirectoryBaseDNTreeNode { name := strings.TrimSpace(entry.GetAttributeValue("ou")) if name == "" { name = strings.TrimSpace(entry.GetAttributeValue("name")) } if name == "" { name = strings.TrimSpace(entry.GetAttributeValue("cn")) } if name == "" { name = activeDirectoryDNDisplayName(distinguishedName) } return activeDirectoryBaseDNTreeNode{ ID: distinguishedName, Name: name, Path: distinguishedName, SecondaryName: distinguishedName, } } func activeDirectoryBaseDNTreeNodeFromDN(distinguishedName string) activeDirectoryBaseDNTreeNode { return activeDirectoryBaseDNTreeNode{ ID: distinguishedName, Name: activeDirectoryDNDisplayName(distinguishedName), Path: distinguishedName, SecondaryName: distinguishedName, } } func activeDirectoryDNDisplayName(distinguishedName string) string { components := splitActiveDirectoryDN(distinguishedName) if len(components) == 0 { return distinguishedName } domainParts := make([]string, 0, len(components)) allDomainParts := true for _, component := range components { attribute, value := activeDirectoryDNRDN(component) if !strings.EqualFold(attribute, "DC") { allDomainParts = false break } domainParts = append(domainParts, value) } if allDomainParts && len(domainParts) > 0 { return strings.Join(domainParts, ".") } _, value := activeDirectoryDNRDN(components[0]) if value != "" { return value } return distinguishedName } func splitActiveDirectoryDN(distinguishedName string) []string { components := []string{} var current strings.Builder escaped := false for _, char := range distinguishedName { if escaped { current.WriteRune(char) escaped = false continue } if char == '\\' { escaped = true current.WriteRune(char) continue } if char == ',' { component := strings.TrimSpace(current.String()) if component != "" { components = append(components, component) } current.Reset() continue } current.WriteRune(char) } component := strings.TrimSpace(current.String()) if component != "" { components = append(components, component) } return components } func activeDirectoryDNRDN(component string) (string, string) { parts := strings.SplitN(component, "=", 2) if len(parts) != 2 { return "", strings.TrimSpace(component) } return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) } func findActiveDirectoryGroupDN( conn *ldap.Conn, credentials activeDirectoryCredentials, ) (string, error) { group := strings.TrimSpace(credentials.UserGroup) if strings.Contains(group, "=") && strings.Contains(group, ",") { return group, nil } escapedGroup := ldap.EscapeFilter(group) groupFilter := fmt.Sprintf( "(&(objectClass=group)(|(cn=%s)(name=%s)(sAMAccountName=%s)))", escapedGroup, escapedGroup, escapedGroup, ) searchRequest := ldap.NewSearchRequest( credentials.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 10, false, groupFilter, []string{"distinguishedName", "cn", "name", "sAMAccountName"}, nil, ) searchResult, err := conn.Search(searchRequest) if err != nil { return "", err } if len(searchResult.Entries) == 0 { return "", fmt.Errorf("AD-Gruppe %q wurde nicht gefunden", group) } return searchResult.Entries[0].DN, nil } func activeDirectoryUserFromLDAPEntry(entry *ldap.Entry) activeDirectoryUser { displayName := strings.TrimSpace(entry.GetAttributeValue("displayName")) username := strings.TrimSpace(entry.GetAttributeValue("sAMAccountName")) email := strings.TrimSpace(entry.GetAttributeValue("mail")) userPrincipalName := strings.TrimSpace(entry.GetAttributeValue("userPrincipalName")) distinguishedName := strings.TrimSpace(entry.GetAttributeValue("distinguishedName")) if distinguishedName == "" { distinguishedName = strings.TrimSpace(entry.DN) } if email == "" { email = userPrincipalName } if displayName == "" { displayName = strings.TrimSpace(entry.GetAttributeValue("cn")) } if displayName == "" { displayName = username } if displayName == "" { displayName = email } sid := activeDirectorySIDString(entry.GetRawAttributeValue("objectSid")) id := sid if id == "" { id = distinguishedName } domain := activeDirectoryDomainFromPrincipal(userPrincipalName) if domain == "" { domain = activeDirectoryDomainFromDN(distinguishedName) } return activeDirectoryUser{ ID: id, DisplayName: displayName, Username: username, Email: email, Domain: domain, DistinguishedName: distinguishedName, SID: sid, Source: "ad", } } func activeDirectoryDomainFromPrincipal(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } _, domain, ok := strings.Cut(value, "@") if !ok { return "" } return strings.TrimSpace(domain) } func activeDirectoryDomainFromDN(distinguishedName string) string { components := splitActiveDirectoryDN(distinguishedName) if len(components) == 0 { return "" } domainParts := make([]string, 0, len(components)) for _, component := range components { attribute, value := activeDirectoryDNRDN(component) if strings.EqualFold(attribute, "DC") && value != "" { domainParts = append(domainParts, value) } } return strings.Join(domainParts, ".") } func activeDirectorySIDString(rawSID []byte) string { if len(rawSID) < 8 { return "" } revision := rawSID[0] subAuthorityCount := int(rawSID[1]) expectedLength := 8 + subAuthorityCount*4 if len(rawSID) < expectedLength { return "" } var identifierAuthority uint64 for _, value := range rawSID[2:8] { identifierAuthority = (identifierAuthority << 8) | uint64(value) } parts := []string{ "S", fmt.Sprintf("%d", revision), fmt.Sprintf("%d", identifierAuthority), } for index := 0; index < subAuthorityCount; index++ { offset := 8 + index*4 parts = append(parts, fmt.Sprintf("%d", binary.LittleEndian.Uint32(rawSID[offset:offset+4]))) } return strings.Join(parts, "-") } func normalizeActiveDirectoryServerURL(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } if !strings.Contains(value, "://") { value = "ldaps://" + value } parsedURL, err := url.Parse(value) if err != nil || parsedURL.Hostname() == "" { return value } if parsedURL.Port() == "" { if parsedURL.Scheme == "ldap" { parsedURL.Host = parsedURL.Hostname() + ":389" } else if parsedURL.Scheme == "ldaps" { parsedURL.Host = parsedURL.Hostname() + ":636" } } return parsedURL.String() }