package main import ( "encoding/json" "fmt" "strings" ldap "bottin/ldapserver" consul "github.com/hashicorp/consul/api" message "bottin/goldap" ) // Generic item modification function -------- func (server *Server) putAttributes(dn string, attrs Entry) error { prefix, err := dnToConsul(dn) if err != nil { return err } // Normalize attribute names: if we have several times the same attr // but with different cases, put that in the same attr normalized := make(Entry) for k, values := range attrs { found := false for k2 := range normalized { if strings.EqualFold(k, k2) { normalized[k2] = append(normalized[k2], values...) found = true break } } if !found { normalized[k] = values } } // Retreieve previously existing attributes, which we will use to delete // entries with the wrong case previous_pairs, _, err := server.kv.List(prefix + "/attribute=", &server.readOpts) if err != nil { return err } for k, valuesNC := range normalized { path := prefix + "/attribute=" + k // Trim spaces and remove empty values values := []string{} for _, v := range valuesNC { vv := strings.TrimSpace(v) if len(vv) > 0 { values = append(values, vv) } } // If previously existing pairs with the wrong case exist, delete them for _, prev_pair := range previous_pairs { if strings.EqualFold(prev_pair.Key, path) && prev_pair.Key != path { _, err := server.kv.Delete(prev_pair.Key, nil) if err != nil { return err } } } // If we have zero values, delete associated k/v pair // Otherwise, write new values if len(values) == 0 { _, err := server.kv.Delete(path, nil) if err != nil { return err } } else { json, err := json.MarshalIndent(values, "", " ") 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 } // Add request ------------------------ 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, err := server.checkDN(string(r.Entry()), 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 } // Check that parent object exists parentDn := unparseDN(dnSplit[1:]) parentExists, err := server.objectExists(parentDn) if err != nil { return ldap.LDAPResultOperationsError, err } if !parentExists { return ldap.LDAPResultNoSuchObject, fmt.Errorf( "Parent object %s does not exist", parentDn) } // If adding a group, track of who the members will be so that their memberOf field can be updated later members := []string{} // Check attributes 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 strings.EqualFold(key, ATTR_MEMBER) { // If they are writing a member list, we have to check they are adding valid members // Also, rewrite member list to use canonical DN syntax (no spaces, all lowercase) for _, member := range vals_str { member_canonical, err := server.checkDN(member, false) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } exists, err = server.objectExists(member_canonical) if err != nil { return ldap.LDAPResultOperationsError, err } if !exists { return ldap.LDAPResultNoSuchObject, fmt.Errorf( "Cannot add %s to members, it does not exist!", member_canonical) } members = append(members, member_canonical) } entry[key] = members } else { if prev, ok := entry[key]; ok { entry[key] = append(prev, vals_str...) } else { entry[key] = vals_str } } } // Ensure object has at least one objectclass value hasObjectClass := false for k := range entry { if strings.EqualFold(k, ATTR_OBJECTCLASS) { hasObjectClass = true break } } if !hasObjectClass { entry[ATTR_OBJECTCLASS] = []string{"top"} } // Write system attributes 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} // Add our item in the DB err = server.putAttributes(dn, entry) if err != nil { return ldap.LDAPResultOperationsError, err } // ~~ from this point on, our operation succeeded ~~ // ~~ future errors cause inconsistencies in the DB and are logged ~~ // If our item has a member list, add it to all of its member's memberOf attribute for _, member := range members { server.memberOfAdd(member, dn) } return ldap.LDAPResultSuccess, nil } // Delete request ------------------------ 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, err := server.checkDN(string(*r), 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+"/", &server.readOpts) 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 %s 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 } memberList, err := server.getAttribute(dn, ATTR_MEMBER) if err != nil { return ldap.LDAPResultOperationsError, err } // Delete the LDAP entry _, err = server.kv.DeleteTree(path+"/", nil) if err != nil { return ldap.LDAPResultOperationsError, err } // ~~ from this point on, our operation succeeded ~~ // ~~ future errors cause inconsistencies in the DB and are logged ~~ // 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(group, ATTR_MEMBER) if err != nil { server.logger.Warnf("Could not remove %s from members of %s: %s", dn, group, err) continue } newMembers := []string{} for _, memb := range groupMembers { if memb != dn { newMembers = append(newMembers, memb) } } err = server.putAttributes(group, Entry{ ATTR_MEMBER: newMembers, }) if err != nil { server.logger.Warnf("Could not remove %s from members of %s: %s", dn, group, err) } } } // Delete it from all of its member's memberOf info for _, member := range memberList { server.memberOfRemove(member, dn) } return ldap.LDAPResultSuccess, nil } // Modify request ------------------------ 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, err := server.checkDN(string(r.Object()), 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{}) && !server.config.Acl.Check(&state.login, "modifyAdd", 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=", &server.readOpts) 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 { server.logger.Fatal("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 entry := Entry{} for _, change := range r.Changes() { attr := string(change.Modification().Type_()) changeValues := []string{} for _, v := range change.Modification().Vals() { changeValues = append(changeValues, string(v)) } // If we already had an attribute with this name before, // make sure we are using the same lowercase/uppercase for prevAttr := range prevEntry { if strings.EqualFold(attr, prevAttr) { attr = prevAttr break } } // Check that this attribute is not system-managed thus restricted 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}) || (change.Operation() == ldap.ModifyRequestChangeOperationAdd && server.config.Acl.Check(&state.login, "modifyAdd", dn, []string{attr}))) { return ldap.LDAPResultInsufficientAccessRights, nil } // If we are changing ATTR_MEMBER, rewrite all values to canonical form if strings.EqualFold(attr, ATTR_MEMBER) { for i := range changeValues { canonical_val, err := server.checkDN(changeValues[i], false) if err != nil { return ldap.LDAPResultInvalidDNSyntax, err } changeValues[i] = canonical_val } } // If we don't yet have a new value for this attr, // but one existed before, initialize entry[attr] to the old value // so that later on what we do is simply modify entry[attr] in place // (this allows to handle sequences of several changes on the same attr) if _, ok := entry[attr]; !ok { if _, ok := prevEntry[attr]; ok { entry[attr] = prevEntry[attr] } } // Apply effective modification on entry[attr] if change.Operation() == ldap.ModifyRequestChangeOperationAdd { for _, val := range changeValues { if !listContains(entry[attr], val) { entry[attr] = append(entry[attr], val) if strings.EqualFold(attr, ATTR_MEMBER) { addMembers = append(addMembers, val) } } } } else if change.Operation() == ldap.ModifyRequestChangeOperationDelete { if len(changeValues) == 0 { // Delete everything if strings.EqualFold(attr, ATTR_MEMBER) { delMembers = append(delMembers, entry[attr]...) } entry[attr] = []string{} } else { // Delete only those specified newList := []string{} for _, prevVal := range entry[attr] { if !listContains(changeValues, prevVal) { newList = append(newList, prevVal) } else { if strings.EqualFold(attr, ATTR_MEMBER) { delMembers = append(delMembers, prevVal) } } } entry[attr] = newList } } else if change.Operation() == ldap.ModifyRequestChangeOperationReplace { if strings.EqualFold(attr, ATTR_MEMBER) { for _, newMem := range changeValues { if !listContains(entry[attr], newMem) { addMembers = append(addMembers, newMem) } } for _, prevMem := range entry[attr] { if !listContains(changeValues, prevMem) { delMembers = append(delMembers, prevMem) } } } entry[attr] = changeValues } } // Check that added members actually exist for i := range addMembers { exists, err := server.objectExists(addMembers[i]) if err != nil { return ldap.LDAPResultOperationsError, err } if !exists { return ldap.LDAPResultNoSuchObject, fmt.Errorf( "Cannot add member %s, it does not exist", addMembers[i]) } } for k, v := range entry { if strings.EqualFold(k, ATTR_OBJECTCLASS) && len(v) == 0 { return ldap.LDAPResultInsufficientAccessRights, fmt.Errorf( "Cannot remove all objectclass values") } } // Now, the modification has been processed and accepted and we want to commit it entry[ATTR_MODIFIERSNAME] = []string{state.login.user} entry[ATTR_MODIFYTIMESTAMP] = []string{genTimestamp()} // Save the edited values err = server.putAttributes(dn, entry) if err != nil { return ldap.LDAPResultOperationsError, err } // ~~ from this point on, our operation succeeded ~~ // ~~ future errors cause inconsistencies in the DB and are logged ~~ // Update memberOf for added members and deleted members for _, addMem := range addMembers { server.memberOfAdd(addMem, dn) } for _, delMem := range delMembers { server.memberOfRemove(delMem, dn) } return ldap.LDAPResultSuccess, nil }