// Go Ldap Api // Copyright (c) 2022 yo000 // package main import ( "fmt" "sync" "strings" "github.com/go-ldap/ldap/v3" log "github.com/sirupsen/logrus" ) type MyLdap struct { Conn *ldap.Conn Host string User string Pass string BaseDN string AuthBaseDN string } var ( mutex sync.Mutex ) func marshalResultToStrMap(sr *ldap.SearchResult) map[string][]string { var res = make(map[string][]string) for _, e := range sr.Entries { for _, a := range e.Attributes { for _, v := range a.Values { res[a.Name] = append(res[a.Name], v) } } } return res } func isStringInArray(strarr []string, searched string) bool { for _, s := range strarr { if strings.EqualFold(s, searched) { return true } } return false } func connectLdap(myldap *MyLdap) (*MyLdap, error) { var err error var conLdap *MyLdap myldap.Conn, err = ldap.DialURL(myldap.Host) if err != nil { //log.Errorf("Error dialing LDAP: %v", err) return conLdap, err } err = myldap.Conn.Bind(myldap.User, myldap.Pass) if err != nil { //log.Errorf("Error binding LDAP: %v", err) return conLdap, err } return myldap, err } func internalLdapSearch(conLdap *MyLdap, searchReq *ldap.SearchRequest, attempt int) (*ldap.SearchResult, error) { var err error if conLdap.Conn == nil { conLdap, err = connectLdap(conLdap) if err != nil { return nil, err } } mutex.Lock() result, err := conLdap.Conn.Search(searchReq) mutex.Unlock() // Manage connection errors here if err != nil && strings.HasSuffix(err.Error(), "ldap: connection closed") { log.Error("LDAP connection closed, retrying") mutex.Lock() conLdap.Conn.Close() conLdap, err = connectLdap(conLdap) mutex.Unlock() if err != nil { return result, err } else { attempt = attempt + 1 return internalLdapSearch(conLdap, searchReq, attempt) } } return result, err } func searchByCn(myldap *MyLdap, baseDn, cn, class, attributes string) (*ldap.SearchResult, error) { var filter string if false == strings.EqualFold(cn, "ALL") { filter = fmt.Sprintf("cn=%s", cn) } else { filter = cn } return doLdapSearch(myldap, baseDn, false, filter, class, attributes) } func searchByDn(myldap *MyLdap, dn, attributes string) (*ldap.SearchResult, error) { // We cant search for a full DN, so separate cn and base dn, then remove basedn as doLdapSearch already append it filter := strings.Split(dn, ",")[0] rem := strings.Split(dn, ",")[1:] bdn := strings.Join(rem, ",") bdn = strings.Replace(bdn, fmt.Sprintf(",%s", myldap.BaseDN), "", 1) return doLdapSearch(myldap, bdn, false, filter, "ALL", "ALL") } func doLdapSearch(myldap *MyLdap, baseDn string, baseDnIsAbsolute bool, filter, class, attributes string) (*ldap.SearchResult, error) { var fFilter string var realBaseDn string var realAttributes []string // Build final search filter if strings.EqualFold(class, "ALL") { fFilter = fmt.Sprintf("(objectClass=*)") } else { fFilter = fmt.Sprintf("(objectClass=%s)", class) } if false == strings.EqualFold(filter, "ALL") { fFilter = fmt.Sprintf("(&%s(%s))", fFilter, ldap.EscapeFilter(filter)) } log.Debugf("LDAP search filter: %s", fFilter) // Build absolute search base DN from configuration & provided DN (which is relative) if strings.EqualFold(baseDn, "ALL") || len(baseDn) == 0 { realBaseDn = fmt.Sprintf("%s", myldap.BaseDN) } else { if len(baseDn) > 0 && baseDnIsAbsolute { realBaseDn = fmt.Sprintf("%s", baseDn) } else { realBaseDn = fmt.Sprintf("%s,%s", baseDn, myldap.BaseDN) } } log.Debugf("LDAP search base dn: %s", realBaseDn) // Build attributes array if false == strings.EqualFold(attributes, "ALL") { for _, a := range strings.Split(attributes, ",") { realAttributes = append(realAttributes, a) } } log.Debugf("LDAP search attributes to return (all if empty): %v", realAttributes) searchReq := ldap.NewSearchRequest(realBaseDn, ldap.ScopeWholeSubtree, 0, 0, 0, false, fFilter, realAttributes, []ldap.Control{}) result, err := internalLdapSearch(myldap, searchReq, 0) if err != nil { return nil, err } return result, nil } func findUserFullDN(myldap *MyLdap, username string) (string, error) { filter := fmt.Sprintf("cn=%s", username) sr, err := doLdapSearch(myldap, myldap.AuthBaseDN, true, filter, "ALL", "") if err != nil { return "", err } if len(sr.Entries) == 0 { return "", fmt.Errorf("User not found with %s", filter) } else if len(sr.Entries) > 1 { return "", fmt.Errorf("More than one object (%d) found with %s", len(sr.Entries), filter) } result := sr.Entries[0].DN return result, nil } func createEntry(myldap *MyLdap, dn string, attributes map[string]interface{}) error { log.Debugf("Creating DN %s with attributes %v", dn, attributes) addReq := ldap.NewAddRequest(dn, nil) // Remove dn from map if exists delete(attributes, "dn") //build attributes list for key, val := range attributes { var v []string strval := fmt.Sprintf("%s",val) if strings.HasPrefix(strval, "[") && strings.HasSuffix(strval, "]") { for _, va := range val.([]interface{}) { v = append(v, va.(string)) } } else { v = append(v, strval) } addReq.Attribute(key, v) } err := myldap.Conn.Add(addReq) return err } func updateEntry(myldap *MyLdap, dn string, attributes map[string]interface{}) error { log.Debugf("Updating DN %s with attributes %v", dn, attributes) // Remove dn from map if exists delete(attributes, "dn") // First get the current object so we can build a list of add, modify, delete attributes sr, err := searchByDn(myldap, dn, "ALL") if err != nil { return err } if len(sr.Entries) == 0 { return fmt.Errorf("Object %s not found", dn) } actualAttrs := marshalResultToStrMap(sr) // convert attributes to map[string][]string var attrsMap = make(map[string][]string) for key, val := range attributes { var v []string strval := fmt.Sprintf("%s",val) if strings.HasPrefix(strval, "[") && strings.HasSuffix(strval, "]") { for _, va := range val.([]interface{}) { v = append(v, va.(string)) } } else { v = append(v, strval) } attrsMap[key] = v } modReq := ldap.NewModifyRequest(dn, nil) // Now compare. We browse required attributes to get modify and add operations opnr := 0 for k, v := range attrsMap { fv := 0 va, found := actualAttrs[k] if found { for _, cv := range va { if isStringInArray(v, cv) { fv += 1 } } if fv != len(va) { modReq.Replace(k, v) opnr++ } } else { modReq.Add(k, v) opnr++ } } // now browse actual attributes to get delete operations for k, v := range actualAttrs { _, found := attrsMap[k] if false == found { modReq.Delete(k, v) opnr++ } } if opnr == 0 { return fmt.Errorf("No modification required") } // Finally execute operations err = myldap.Conn.Modify(modReq) return err } func deleteEntry(myldap *MyLdap, dn string) error { log.Debugf("Deleting DN %s", dn) delReq := ldap.NewDelRequest(dn, nil) err := myldap.Conn.Del(delReq) return err }