296 lines
7.0 KiB
Go
296 lines
7.0 KiB
Go
// Go Ldap Api
|
|
// Copyright (c) 2022 yo000 <johan@nosd.in>
|
|
//
|
|
|
|
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
|
|
}
|