diff --git a/main.go b/main.go index d55601b..d5dcdb4 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main -// @FIXME: Auto populate entryuuid creatorsname createtimestamp modifiersname modifytimestamp (uuid: github.com/google/uuid, timestamps: 20060102150405Z) -// @FIXME: Use memberof and not memberOf +// @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 @@ -26,6 +25,14 @@ import ( 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"` @@ -220,7 +227,7 @@ func (server *Server) init() error { "objectClass": []string{"simpleSecurityObject", "organizationalRole"}, "description": []string{"LDAP administrator"}, "cn": []string{"admin"}, - "userpassword": []string{admin_pass_hash}, + ATTR_USERPASSWORD: []string{admin_pass_hash}, "structuralObjectClass": []string{"organizationalRole"}, "permissions": []string{"read", "write"}, } @@ -342,7 +349,7 @@ func (server *Server) handleBindInternal(state *State, r *message.BindRequest) ( } // Try to retrieve password and check for match - passwd, err := server.getAttribute(string(r.Name()), "userpassword") + passwd, err := server.getAttribute(string(r.Name()), ATTR_USERPASSWORD) if err != nil { return ldap.LDAPResultOperationsError, err } @@ -353,7 +360,7 @@ func (server *Server) handleBindInternal(state *State, r *message.BindRequest) ( for _, hash := range passwd { valid := SSHAMatches(hash, []byte(r.AuthenticationSimple())) if valid { - groups, err := server.getAttribute(string(r.Name()), "memberOf") + groups, err := server.getAttribute(string(r.Name()), ATTR_MEMBEROF) if err != nil { return ldap.LDAPResultOperationsError, err } @@ -547,6 +554,11 @@ func (server *Server) handleAddInternal(state *State, r *message.AddRequest) (in return ldap.LDAPResultInvalidDNSyntax, err } + dnSplit, err := parseDN(dn) + if err != nil { + return ldap.LDAPResultInvalidDNSyntax, err + } + // Check permissions attrListStr := []string{} for _, attribute := range r.Attributes() { @@ -579,9 +591,9 @@ func (server *Server) handleAddInternal(state *State, r *message.AddRequest) (in } // Fail if they are trying to write memberOf, we manage this ourselves - if strings.EqualFold(key, "memberOf") { - return ldap.LDAPResultObjectClassViolation, fmt.Errorf( - "memberOf cannot be defined directly, membership must be specified in the group itself") + 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") { @@ -605,6 +617,11 @@ func (server *Server) handleAddInternal(state *State, r *message.AddRequest) (in 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 @@ -612,7 +629,7 @@ func (server *Server) handleAddInternal(state *State, r *message.AddRequest) (in if members != nil { for _, member := range members { - memberGroups, err := server.getAttribute(member, "memberOf") + memberGroups, err := server.getAttribute(member, ATTR_MEMBEROF) if err != nil { return ldap.LDAPResultOperationsError, err } @@ -633,7 +650,7 @@ func (server *Server) handleAddInternal(state *State, r *message.AddRequest) (in if !alreadyMember { memberGroups = append(memberGroups, dn) err = server.addElements(member, Entry{ - "memberOf": memberGroups, + ATTR_MEMBEROF: memberGroups, }) if err != nil { return ldap.LDAPResultOperationsError, err @@ -753,7 +770,7 @@ func (server *Server) handleDeleteInternal(state *State, r *message.DelRequest) } // Retrieve group membership before we delete everything - memberOf, err := server.getAttribute(dn, "memberOf") + memberOf, err := server.getAttribute(dn, ATTR_MEMBEROF) if err != nil { return ldap.LDAPResultOperationsError, err } @@ -817,6 +834,11 @@ func (server *Server) handleModifyInternal(state *State, r *message.ModifyReques 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 @@ -862,6 +884,14 @@ func (server *Server) handleModifyInternal(state *State, r *message.ModifyReques 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 @@ -957,12 +987,15 @@ func (server *Server) handleModifyInternal(state *State, r *message.ModifyReques } } + 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, "memberOf") + memberOf, err := server.getAttribute(addMem, ATTR_MEMBEROF) if err != nil { return ldap.LDAPResultOperationsError, err } @@ -970,14 +1003,14 @@ func (server *Server) handleModifyInternal(state *State, r *message.ModifyReques memberOf = []string{} } memberOf = append(memberOf, dn) - err = server.addElements(addMem, Entry{"memberOf": memberOf}) + err = server.addElements(addMem, Entry{ATTR_MEMBEROF: memberOf}) if err != nil { return ldap.LDAPResultOperationsError, err } } for _, delMem := range delMembers { - memberOf, err := server.getAttribute(delMem, "memberOf") + memberOf, err := server.getAttribute(delMem, ATTR_MEMBEROF) if err != nil { return ldap.LDAPResultOperationsError, err } @@ -991,7 +1024,7 @@ func (server *Server) handleModifyInternal(state *State, r *message.ModifyReques } } - err = server.addElements(delMem, Entry{"memberOf": newMemberOf}) + err = server.addElements(delMem, Entry{ATTR_MEMBEROF: newMemberOf}) if err != nil { return ldap.LDAPResultOperationsError, err } diff --git a/util.go b/util.go index 014805c..d7c42d7 100644 --- a/util.go +++ b/util.go @@ -3,8 +3,11 @@ package main import ( "encoding/json" "fmt" + "log" "strings" + "time" + uuid "github.com/google/uuid" consul "github.com/hashicorp/consul/api" ) @@ -101,3 +104,37 @@ func parseDN(dn string) ([]DNComponent, error) { } return ret, nil } + +func checkRestrictedAttr(attr string) error { + RESTRICTED_ATTRS := []string{ + ATTR_MEMBEROF, + ATTR_ENTRYUUID, + ATTR_CREATORSNAME, + ATTR_CREATETIMESTAMP, + ATTR_MODIFIERSNAME, + ATTR_MODIFYTIMESTAMP, + } + + if strings.EqualFold(attr, ATTR_MEMBEROF) { + return fmt.Errorf("memberOf cannot be defined directly, membership must be specified in the group itself") + } + + for _, s := range RESTRICTED_ATTRS { + if strings.EqualFold(attr, s) { + return fmt.Errorf("Attribute %s is restricted and may only be set by the system", s) + } + } + return nil +} + +func genTimestamp() string { + return time.Now().Format("20060102150405Z") +} + +func genUuid() string { + uuid, err := uuid.NewRandom() + if err != nil { + log.Panicf("UUID generation error: %s", err) + } + return uuid.String() +}