package main // @FIXME: Proper handling of various upper/lower case combinations // @FIXME: Implement missing search filters (in applyFilter) // @FIXME: Add an initial prefix to the consul key value import ( "crypto/tls" "encoding/base64" "encoding/json" "flag" "fmt" "io/ioutil" "log" "math/rand" "os" "os/signal" "strings" "syscall" ldap "./ldapserver" consul "github.com/hashicorp/consul/api" message "github.com/vjeantet/goldap/message" ) const DEBUG = false const ATTR_USERPASSWORD = "userpassword" const ATTR_MEMBEROF = "memberof" const ATTR_ENTRYUUID = "entryuuid" const ATTR_CREATORSNAME = "creatorsname" const ATTR_CREATETIMESTAMP = "createtimestamp" const ATTR_MODIFIERSNAME = "modifiersname" const ATTR_MODIFYTIMESTAMP = "modifytimestamp" type ConfigFile struct { Suffix string `json:"suffix"` BindAddress string `json:"bind_address"` ConsulHost string `json:"consul_host"` Acl []string `json:"acl"` SSLCertFile string `json:"ssl_cert_file"` SSLKeyFile string `json:"ssl_key_file"` SSLServerName string `json:"ssl_server_name"` } type Config struct { Suffix string BindAddress string ConsulHost string Acl ACL TlsConfig *tls.Config } type Server struct { logger *log.Logger config Config kv *consul.KV } type State struct { login Login } type Entry map[string][]string var configFlag = flag.String("config", "./config.json", "Configuration file path") func readConfig() Config { config_file := ConfigFile{ BindAddress: "0.0.0.0:389", } bytes, err := ioutil.ReadFile(*configFlag) if err != nil { panic(err) } err = json.Unmarshal(bytes, &config_file) if err != nil { panic(err) } acl, err := ParseACL(config_file.Acl) if err != nil { panic(err) } ret := Config{ Suffix: config_file.Suffix, BindAddress: config_file.BindAddress, ConsulHost: config_file.ConsulHost, Acl: acl, } if config_file.SSLCertFile != "" && config_file.SSLKeyFile != "" && config_file.SSLServerName != "" { cert_txt, err := ioutil.ReadFile(config_file.SSLCertFile) if err != nil { panic(err) } key_txt, err := ioutil.ReadFile(config_file.SSLKeyFile) if err != nil { panic(err) } cert, err := tls.X509KeyPair(cert_txt, key_txt) if err != nil { panic(err) } ret.TlsConfig = &tls.Config{ MinVersion: tls.VersionSSL30, MaxVersion: tls.VersionTLS12, Certificates: []tls.Certificate{cert}, ServerName: config_file.SSLServerName, } } return ret } func main() { flag.Parse() ldap.Logger = log.New(os.Stdout, "[ldapserver] ", log.LstdFlags) config := readConfig() // Connect to Consul consul_config := consul.DefaultConfig() if config.ConsulHost != "" { consul_config.Address = config.ConsulHost } consul_client, err := consul.NewClient(consul_config) if err != nil { panic(err) } kv := consul_client.KV() // Create gobottin server gobottin := Server{ logger: log.New(os.Stdout, "[gobottin] ", log.LstdFlags), config: config, kv: kv, } err = gobottin.init() if err != nil { panic(err) } //Create a new LDAP Server ldapserver := ldap.NewServer() ldapserver.NewUserState = func() ldap.UserState { return &State{ login: Login{ user: "ANONYMOUS", groups: []string{}, }, } } routes := ldap.NewRouteMux() routes.Bind(gobottin.handleBind) routes.Search(gobottin.handleSearch) routes.Add(gobottin.handleAdd) routes.Compare(gobottin.handleCompare) routes.Delete(gobottin.handleDelete) routes.Modify(gobottin.handleModify) ldapserver.Handle(routes) if config.TlsConfig != nil { secureConn := func(s *ldap.Server) { s.Listener = tls.NewListener(s.Listener, config.TlsConfig) } go ldapserver.ListenAndServe(config.BindAddress, secureConn) } else { go ldapserver.ListenAndServe(config.BindAddress) } // When CTRL+C, SIGINT and SIGTERM signal occurs // Then stop server gracefully ch := make(chan os.Signal) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) <-ch close(ch) ldapserver.Stop() } func (server *Server) init() error { path, err := dnToConsul(server.config.Suffix) if err != nil { return err } pair, _, err := server.kv.Get(path+"/attribute=objectClass", nil) if err != nil { return err } if pair != nil { return nil } base_attributes := Entry{ "objectClass": []string{"top", "dcObject", "organization"}, "structuralObjectClass": []string{"Organization"}, } suffix_dn, err := parseDN(server.config.Suffix) if err != nil { return err } base_attributes[suffix_dn[0].Type] = []string{suffix_dn[0].Value} err = server.addElements(server.config.Suffix, base_attributes) if err != nil { return err } admin_pass := make([]byte, 8) rand.Read(admin_pass) admin_pass_str := base64.RawURLEncoding.EncodeToString(admin_pass) admin_pass_hash := SSHAEncode([]byte(admin_pass_str)) admin_dn := "cn=admin," + server.config.Suffix admin_attributes := Entry{ "objectClass": []string{"simpleSecurityObject", "organizationalRole"}, "description": []string{"LDAP administrator"}, "cn": []string{"admin"}, ATTR_USERPASSWORD: []string{admin_pass_hash}, "structuralObjectClass": []string{"organizationalRole"}, "permissions": []string{"read", "write"}, } err = server.addElements(admin_dn, admin_attributes) if err != nil { return err } server.logger.Printf( "It seems to be a new installation, we created a default user for you:\n\n dn: %s\n password: %s\n\nWe didn't use true random, you should replace it as soon as possible.", admin_dn, admin_pass_str, ) return nil } func (server *Server) addElements(dn string, attrs Entry) error { prefix, err := dnToConsul(dn) if err != nil { return err } for k, v := range attrs { path := prefix + "/attribute=" + k if len(v) == 0 { // If we have zero values, delete associated k/v pair _, err := server.kv.Delete(path, nil) if err != nil { return err } } else { json, err := json.Marshal(v) if err != nil { return err } pair := &consul.KVPair{Key: path, Value: json} _, err = server.kv.Put(pair, nil) if err != nil { return err } } } return nil } func (server *Server) getAttribute(dn string, attr string) ([]string, error) { path, err := dnToConsul(dn) if err != nil { return nil, err } pair, _, err := server.kv.Get(path+"/attribute="+attr, nil) if err != nil { return nil, err } if pair == nil { return nil, nil } return parseValue(pair.Value) } func (server *Server) objectExists(dn string) (bool, error) { prefix, err := dnToConsul(dn) if err != nil { return false, err } data, _, err := server.kv.List(prefix+"/", nil) if err != nil { return false, err } return len(data) > 0, nil } func (server *Server) checkSuffix(dn string, allow_extend bool) (string, error) { suffix := server.config.Suffix if len(dn) < len(suffix) { if dn != suffix[-len(dn):] || !allow_extend { return suffix, fmt.Errorf( "Only handling stuff under DN %s", suffix) } return suffix, nil } else { if dn[len(dn)-len(suffix):] != suffix { return suffix, fmt.Errorf( "Only handling stuff under DN %s", suffix) } return dn, nil } } func (server *Server) handleBind(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) { state := s.(*State) r := m.GetBindRequest() result_code, err := server.handleBindInternal(state, &r) res := ldap.NewBindResponse(result_code) if err != nil { res.SetDiagnosticMessage(err.Error()) server.logger.Printf("Failed bind for %s: %s", string(r.Name()), err.Error()) } if result_code == ldap.LDAPResultSuccess { server.logger.Printf("Successfully bound to %s", string(r.Name())) } else { server.logger.Printf("Failed to bind to %s (%s)", string(r.Name()), err) } w.Write(res) } func (server *Server) handleBindInternal(state *State, r *message.BindRequest) (int, error) { // Check permissions if !server.config.Acl.Check(&state.login, "bind", string(r.Name()), []string{}) { return ldap.LDAPResultInsufficientAccessRights, nil } // Try to retrieve password and check for match passwd, err := server.getAttribute(string(r.Name()), ATTR_USERPASSWORD) if err != nil { return ldap.LDAPResultOperationsError, err } if passwd == nil { return ldap.LDAPResultNoSuchObject, nil } for _, hash := range passwd { valid := SSHAMatches(hash, []byte(r.AuthenticationSimple())) if valid { groups, err := server.getAttribute(string(r.Name()), ATTR_MEMBEROF) if err != nil { return ldap.LDAPResultOperationsError, err } state.login = Login{ user: string(r.Name()), groups: groups, } return ldap.LDAPResultSuccess, nil } } return ldap.LDAPResultInvalidCredentials, nil } func (server *Server) handleSearch(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) { state := s.(*State) r := m.GetSearchRequest() code, err := server.handleSearchInternal(state, w, &r) res := ldap.NewResponse(code) if err != nil { res.SetDiagnosticMessage(err.Error()) } w.Write(message.SearchResultDone(res)) } func (server *Server) handleSearchInternal(state *State, w ldap.ResponseWriter, r *message.SearchRequest) (int, error) { if DEBUG { server.logger.Printf("-- SEARCH REQUEST: --") server.logger.Printf("Request BaseDn=%s", r.BaseObject()) server.logger.Printf("Request Filter=%s", r.Filter()) server.logger.Printf("Request FilterString=%s", r.FilterString()) server.logger.Printf("Request Attributes=%s", r.Attributes()) server.logger.Printf("Request TimeLimit=%d", r.TimeLimit().Int()) } if !server.config.Acl.Check(&state.login, "read", string(r.BaseObject()), []string{}) { return ldap.LDAPResultInsufficientAccessRights, fmt.Errorf("Please specify a base object on which you have read rights") } baseObject, err := server.checkSuffix(string(r.BaseObject()), true) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } basePath, err := dnToConsul(baseObject) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } data, _, err := server.kv.List(basePath+"/", nil) if err != nil { return ldap.LDAPResultOperationsError, err } entries, err := parseConsulResult(data) if err != nil { return ldap.LDAPResultOperationsError, err } if DEBUG { server.logger.Printf("in %s: %#v", basePath+"/", data) server.logger.Printf("%#v", entries) } for dn, entry := range entries { // Filter out if we don't match requested filter matched, err := applyFilter(entry, r.Filter()) if err != nil { return ldap.LDAPResultUnwillingToPerform, err } if !matched { continue } // Filter out if user is not allowed to read this if !server.config.Acl.Check(&state.login, "read", dn, []string{}) { continue } e := ldap.NewSearchResultEntry(dn) for attr, val := range entry { // If attribute is not in request, exclude it from returned entry if len(r.Attributes()) > 0 { found := false for _, requested := range r.Attributes() { if strings.EqualFold(string(requested), attr) { found = true break } } if !found { continue } } // If we are not allowed to read attribute, exclude it from returned entry if !server.config.Acl.Check(&state.login, "read", dn, []string{attr}) { continue } // Send result for _, v := range val { e.AddAttribute(message.AttributeDescription(attr), message.AttributeValue(v)) } } w.Write(e) } return ldap.LDAPResultSuccess, nil } func applyFilter(entry Entry, filter message.Filter) (bool, error) { if fAnd, ok := filter.(message.FilterAnd); ok { for _, cond := range fAnd { res, err := applyFilter(entry, cond) if err != nil { return false, err } if !res { return false, nil } } return true, nil } else if fOr, ok := filter.(message.FilterOr); ok { for _, cond := range fOr { res, err := applyFilter(entry, cond) if err != nil { return false, err } if res { return true, nil } } return false, nil } else if fNot, ok := filter.(message.FilterNot); ok { res, err := applyFilter(entry, fNot.Filter) if err != nil { return false, err } return !res, nil } else if fPresent, ok := filter.(message.FilterPresent); ok { what := string(fPresent) // Case insensitive search for desc, values := range entry { if strings.EqualFold(what, desc) { return len(values) > 0, nil } } return false, nil } else if fEquality, ok := filter.(message.FilterEqualityMatch); ok { desc := string(fEquality.AttributeDesc()) target := string(fEquality.AssertionValue()) // Case insensitive attribute search for entry_desc, value := range entry { if strings.EqualFold(entry_desc, desc) { for _, val := range value { if val == target { return true, nil } } return false, nil } } return false, nil } else { return false, fmt.Errorf("Unsupported filter: %#v %T", filter, filter) } } func (server *Server) handleAdd(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) { state := s.(*State) r := m.GetAddRequest() code, err := server.handleAddInternal(state, &r) res := ldap.NewResponse(code) if err != nil { res.SetDiagnosticMessage(err.Error()) } if code == ldap.LDAPResultSuccess { server.logger.Printf("Successfully added %s", string(r.Entry())) } else { server.logger.Printf("Failed to add %s (%s)", string(r.Entry()), err) } w.Write(message.AddResponse(res)) } func (server *Server) handleAddInternal(state *State, r *message.AddRequest) (int, error) { dn := string(r.Entry()) _, err := server.checkSuffix(dn, false) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } dnSplit, err := parseDN(dn) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } // Check permissions attrListStr := []string{} for _, attribute := range r.Attributes() { attrListStr = append(attrListStr, string(attribute.Type_())) } if !server.config.Acl.Check(&state.login, "add", dn, attrListStr) { return ldap.LDAPResultInsufficientAccessRights, nil } // Check that object does not already exist exists, err := server.objectExists(dn) if err != nil { return ldap.LDAPResultOperationsError, err } if exists { return ldap.LDAPResultEntryAlreadyExists, nil } // Add object // If adding a group, track of who the members will be so that their memberOf field can be updated later var members []string = nil entry := Entry{} for _, attribute := range r.Attributes() { key := string(attribute.Type_()) vals_str := []string{} for _, val := range attribute.Vals() { vals_str = append(vals_str, string(val)) } // Fail if they are trying to write memberOf, we manage this ourselves err = checkRestrictedAttr(key) if err != nil { return ldap.LDAPResultObjectClassViolation, err } // If they are writing a member key, we have to check they are adding valid members if strings.EqualFold(key, "member") { members = vals_str for _, member := range members { _, err := server.checkSuffix(member, false) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } exists, err = server.objectExists(member) if err != nil { return ldap.LDAPResultOperationsError, err } if !exists { return ldap.LDAPResultNoSuchObject, fmt.Errorf( "Cannot add %s to members, it does not exist!", member) } } } entry[key] = vals_str } entry[ATTR_CREATORSNAME] = []string{state.login.user} entry[ATTR_CREATETIMESTAMP] = []string{genTimestamp()} entry[ATTR_ENTRYUUID] = []string{genUuid()} entry[dnSplit[0].Type] = []string{dnSplit[0].Value} err = server.addElements(dn, entry) if err != nil { return ldap.LDAPResultOperationsError, err } if members != nil { for _, member := range members { memberGroups, err := server.getAttribute(member, ATTR_MEMBEROF) if err != nil { return ldap.LDAPResultOperationsError, err } if memberGroups == nil { memberGroups = []string{} } alreadyMember := false for _, mb := range memberGroups { if mb == dn { alreadyMember = true server.logger.Printf("Warning: inconsistency detected, %s was memberOf %s at a time when it didn't exist!", member, dn) break } } if !alreadyMember { memberGroups = append(memberGroups, dn) err = server.addElements(member, Entry{ ATTR_MEMBEROF: memberGroups, }) if err != nil { return ldap.LDAPResultOperationsError, err } } } } return ldap.LDAPResultSuccess, nil } func (server *Server) handleCompare(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) { state := s.(*State) r := m.GetCompareRequest() code, err := server.handleCompareInternal(state, &r) res := ldap.NewResponse(code) if err != nil { res.SetDiagnosticMessage(err.Error()) } w.Write(message.CompareResponse(res)) } func (server *Server) handleCompareInternal(state *State, r *message.CompareRequest) (int, error) { dn := string(r.Entry()) attr := string(r.Ava().AttributeDesc()) expected := string(r.Ava().AssertionValue()) _, err := server.checkSuffix(dn, false) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } // Check permissions if !server.config.Acl.Check(&state.login, dn, "read", []string{attr}) { return ldap.LDAPResultInsufficientAccessRights, nil } // Do query exists, err := server.objectExists(dn) if err != nil { return ldap.LDAPResultOperationsError, err } if !exists { return ldap.LDAPResultNoSuchObject, fmt.Errorf("Not found: %s", dn) } values, err := server.getAttribute(dn, attr) if err != nil { return ldap.LDAPResultOperationsError, err } for _, v := range values { if v == expected { return ldap.LDAPResultCompareTrue, nil } } return ldap.LDAPResultCompareFalse, nil } func (server *Server) handleDelete(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) { state := s.(*State) r := m.GetDeleteRequest() code, err := server.handleDeleteInternal(state, &r) res := ldap.NewResponse(code) if err != nil { res.SetDiagnosticMessage(err.Error()) } if code == ldap.LDAPResultSuccess { server.logger.Printf("Successfully deleted %s", string(r)) } else { server.logger.Printf("Failed to delete %s (%s)", string(r), err) } w.Write(message.DelResponse(res)) } func (server *Server) handleDeleteInternal(state *State, r *message.DelRequest) (int, error) { dn := string(*r) _, err := server.checkSuffix(dn, false) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } // Check for delete permission if !server.config.Acl.Check(&state.login, "delete", dn, []string{}) { return ldap.LDAPResultInsufficientAccessRights, nil } // Check that this LDAP entry exists and has no children path, err := dnToConsul(dn) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } items, _, err := server.kv.List(path+"/", nil) if err != nil { return ldap.LDAPResultOperationsError, err } if len(items) == 0 { return ldap.LDAPResultNoSuchObject, fmt.Errorf("Not found: %s", dn) } for _, item := range items { itemDN, _, err := consulToDN(item.Key) if err != nil { continue } if itemDN != dn { return ldap.LDAPResultNotAllowedOnNonLeaf, fmt.Errorf( "Cannot delete %d as it has children", dn) } } // Retrieve group membership before we delete everything memberOf, err := server.getAttribute(dn, ATTR_MEMBEROF) if err != nil { return ldap.LDAPResultOperationsError, err } // Delete the LDAP entry _, err = server.kv.DeleteTree(path+"/", nil) if err != nil { return ldap.LDAPResultOperationsError, err } // Delete it from the member list of all the groups it was a member of if memberOf != nil { for _, group := range memberOf { groupMembers, err := server.getAttribute(dn, "member") if err != nil { return ldap.LDAPResultOperationsError, err } newMembers := []string{} for _, memb := range groupMembers { if memb != dn { newMembers = append(newMembers, memb) } } err = server.addElements(group, Entry{ "member": newMembers, }) if err != nil { return ldap.LDAPResultOperationsError, err } } } return ldap.LDAPResultSuccess, nil } func (server *Server) handleModify(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) { state := s.(*State) r := m.GetModifyRequest() code, err := server.handleModifyInternal(state, &r) res := ldap.NewResponse(code) if err != nil { res.SetDiagnosticMessage(err.Error()) } if code == ldap.LDAPResultSuccess { server.logger.Printf("Successfully modified %s", string(r.Object())) } else { server.logger.Printf("Failed to modifiy %s (%s)", string(r.Object()), err) } w.Write(message.ModifyResponse(res)) } func (server *Server) handleModifyInternal(state *State, r *message.ModifyRequest) (int, error) { dn := string(r.Object()) _, err := server.checkSuffix(dn, false) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } dnSplit, err := parseDN(dn) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } // First permission check with no particular attributes if !server.config.Acl.Check(&state.login, "modify", dn, []string{}) { return ldap.LDAPResultInsufficientAccessRights, nil } // Retrieve previous values (by the way, check object exists) path, err := dnToConsul(dn) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } items, _, err := server.kv.List(path+"/attribute=", nil) if err != nil { return ldap.LDAPResultOperationsError, err } if len(items) == 0 { return ldap.LDAPResultNoSuchObject, fmt.Errorf("Not found: %s", dn) } prevEntry := Entry{} for _, item := range items { itemDN, attr, err := consulToDN(item.Key) if err != nil { continue } if itemDN != dn { panic("itemDN != dn in handleModifyInternal") } vals, err := parseValue(item.Value) if err != nil { return ldap.LDAPResultOperationsError, err } prevEntry[attr] = vals } // Keep track of group members added/deleted addMembers, delMembers := []string{}, []string{} // Produce new entry values to be saved newEntry := Entry{} for _, change := range r.Changes() { attr := string(change.Modification().Type_()) values := change.Modification().Vals() err = checkRestrictedAttr(attr) if err != nil { return ldap.LDAPResultObjectClassViolation, err } if strings.EqualFold(attr, dnSplit[0].Type) { return ldap.LDAPResultObjectClassViolation, fmt.Errorf("%s may not be changed as it is part of object path", attr) } // Check for permission to modify this attribute if !server.config.Acl.Check(&state.login, "modify", dn, []string{attr}) { return ldap.LDAPResultInsufficientAccessRights, nil } if change.Operation() == ldap.ModifyRequestChangeOperationAdd { newEntry[attr] = prevEntry[attr] for _, val := range values { present := false for _, prevVal := range newEntry[attr] { if prevVal == string(val) { present = true break } } if !present { newEntry[attr] = append(newEntry[attr], string(val)) if strings.EqualFold(attr, "member") { addMembers = append(addMembers, string(val)) } } } } else if change.Operation() == ldap.ModifyRequestChangeOperationDelete { if len(values) == 0 { // Delete everything newEntry[attr] = []string{} if strings.EqualFold(attr, "member") { delMembers = append(delMembers, prevEntry[attr]...) } } else { // Delete only those specified newEntry[attr] = []string{} for _, prevVal := range prevEntry[attr] { keep := true for _, delVal := range values { if string(delVal) == prevVal { keep = false break } } if keep { newEntry[attr] = append(newEntry[attr], prevVal) } else { if strings.EqualFold(attr, "member") { delMembers = append(delMembers, prevVal) } } } } } else if change.Operation() == ldap.ModifyRequestChangeOperationReplace { newEntry[attr] = []string{} for _, newVal := range values { newEntry[attr] = append(newEntry[attr], string(newVal)) } if strings.EqualFold(attr, "member") { for _, newMem := range newEntry[attr] { mustAdd := true for _, prevMem := range prevEntry[attr] { if prevMem == newMem { mustAdd = false break } } if mustAdd { addMembers = append(addMembers, newMem) } } for _, prevMem := range prevEntry[attr] { mustDel := true for _, newMem := range newEntry[attr] { if newMem == prevMem { mustDel = false break } } if mustDel { delMembers = append(delMembers, prevMem) } } } } } // Check that added members actually exist for _, addMem := range addMembers { exists, err := server.objectExists(addMem) if err != nil { return ldap.LDAPResultOperationsError, err } if !exists { return ldap.LDAPResultNoSuchObject, fmt.Errorf( "Cannot add member %s, it does not exist", addMem) } } newEntry[ATTR_MODIFIERSNAME] = []string{state.login.user} newEntry[ATTR_MODIFYTIMESTAMP] = []string{genTimestamp()} // Save the edited values server.addElements(dn, newEntry) // Update memberOf for added members and deleted members for _, addMem := range addMembers { memberOf, err := server.getAttribute(addMem, ATTR_MEMBEROF) if err != nil { return ldap.LDAPResultOperationsError, err } if memberOf == nil { memberOf = []string{} } memberOf = append(memberOf, dn) err = server.addElements(addMem, Entry{ATTR_MEMBEROF: memberOf}) if err != nil { return ldap.LDAPResultOperationsError, err } } for _, delMem := range delMembers { memberOf, err := server.getAttribute(delMem, ATTR_MEMBEROF) if err != nil { return ldap.LDAPResultOperationsError, err } if memberOf == nil { memberOf = []string{} } newMemberOf := []string{} for _, g := range memberOf { if g != dn { newMemberOf = append(newMemberOf, g) } } err = server.addElements(delMem, Entry{ATTR_MEMBEROF: newMemberOf}) if err != nil { return ldap.LDAPResultOperationsError, err } } return ldap.LDAPResultSuccess, nil }