diff --git a/services/graph/pkg/identity/ldap.go b/services/graph/pkg/identity/ldap.go index 460628ab6..2db8d547d 100644 --- a/services/graph/pkg/identity/ldap.go +++ b/services/graph/pkg/identity/ldap.go @@ -9,7 +9,6 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/gofrs/uuid" - ldapdn "github.com/libregraph/idm/pkg/ldapdn" libregraph "github.com/owncloud/libre-graph-api-go" oldap "github.com/owncloud/ocis/v2/ocis-pkg/ldap" "github.com/owncloud/ocis/v2/ocis-pkg/log" @@ -55,13 +54,6 @@ type userAttributeMap struct { surname string } -type groupAttributeMap struct { - name string - id string - member string - memberSyntax string -} - type ldapAttributeValues map[string][]string func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LDAP, error) { @@ -288,19 +280,6 @@ func (i *LDAP) getUserByDN(dn string) (*ldap.Entry, error) { return i.getEntryByDN(dn, attrs, filter) } -func (i *LDAP) getGroupByDN(dn string) (*ldap.Entry, error) { - attrs := []string{ - i.groupAttributeMap.id, - i.groupAttributeMap.name, - } - filter := fmt.Sprintf("(objectClass=%s)", i.groupObjectClass) - - if i.groupFilter != "" { - filter = fmt.Sprintf("(&%s(%s))", filter, i.groupFilter) - } - return i.getEntryByDN(dn, attrs, filter) -} - func (i *LDAP) getEntryByDN(dn string, attrs []string, filter string) (*ldap.Entry, error) { if filter == "" { filter = "(objectclass=*)" @@ -489,435 +468,6 @@ func (i *LDAP) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregra return users, nil } -func (i *LDAP) getGroupsForUser(dn string) ([]*ldap.Entry, error) { - groupFilter := fmt.Sprintf( - "(%s=%s)", - i.groupAttributeMap.member, dn, - ) - userGroups, err := i.getLDAPGroupsByFilter(groupFilter, false, false) - if err != nil { - return nil, err - } - return userGroups, nil -} - -func (i *LDAP) GetGroup(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.Group, error) { - logger := i.logger.SubloggerWithRequestID(ctx) - logger.Debug().Str("backend", "ldap").Msg("GetGroup") - e, err := i.getLDAPGroupByNameOrID(nameOrID, true) - if err != nil { - return nil, err - } - sel := strings.Split(queryParam.Get("$select"), ",") - exp := strings.Split(queryParam.Get("$expand"), ",") - var g *libregraph.Group - if g = i.createGroupModelFromLDAP(e); g == nil { - return nil, errorcode.New(errorcode.ItemNotFound, "not found") - } - if slices.Contains(sel, "members") || slices.Contains(exp, "members") { - members, err := i.expandLDAPGroupMembers(ctx, e) - if err != nil { - return nil, err - } - if len(members) > 0 { - m := make([]libregraph.User, 0, len(members)) - for _, ue := range members { - if u := i.createUserModelFromLDAP(ue); u != nil { - m = append(m, *u) - } - } - g.Members = m - } - } - return g, nil -} - -func (i *LDAP) getLDAPGroupByID(id string, requestMembers bool) (*ldap.Entry, error) { - id = ldap.EscapeFilter(id) - filter := fmt.Sprintf("(%s=%s)", i.groupAttributeMap.id, id) - return i.getLDAPGroupByFilter(filter, requestMembers) -} - -func (i *LDAP) getLDAPGroupByNameOrID(nameOrID string, requestMembers bool) (*ldap.Entry, error) { - nameOrID = ldap.EscapeFilter(nameOrID) - filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", i.groupAttributeMap.name, nameOrID, i.groupAttributeMap.id, nameOrID) - return i.getLDAPGroupByFilter(filter, requestMembers) -} - -func (i *LDAP) getLDAPGroupByFilter(filter string, requestMembers bool) (*ldap.Entry, error) { - e, err := i.getLDAPGroupsByFilter(filter, requestMembers, true) - if err != nil { - return nil, err - } - if len(e) == 0 { - return nil, errorcode.New(errorcode.ItemNotFound, "not found") - } - - return e[0], nil -} - -// Search for LDAP Groups matching the specified filter, if requestMembers is true the groupMemberShip -// attribute will be part of the result attributes. The LDAP filter is combined with the configured groupFilter -// resulting in a filter like "(&(LDAP.groupFilter)(objectClass=LDAP.groupObjectClass)())" -func (i *LDAP) getLDAPGroupsByFilter(filter string, requestMembers, single bool) ([]*ldap.Entry, error) { - attrs := []string{ - i.groupAttributeMap.name, - i.groupAttributeMap.id, - } - - if requestMembers { - attrs = append(attrs, i.groupAttributeMap.member) - } - - sizelimit := 0 - if single { - sizelimit = 1 - } - searchRequest := ldap.NewSearchRequest( - i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, sizelimit, 0, false, - fmt.Sprintf("(&%s(objectClass=%s)%s)", i.groupFilter, i.groupObjectClass, filter), - attrs, - nil, - ) - i.logger.Debug().Str("backend", "ldap"). - Str("base", searchRequest.BaseDN). - Str("filter", searchRequest.Filter). - Int("scope", searchRequest.Scope). - Int("sizelimit", searchRequest.SizeLimit). - Interface("attributes", searchRequest.Attributes). - Msg("getLDAPGroupsByFilter") - res, err := i.conn.Search(searchRequest) - if err != nil { - var errmsg string - if lerr, ok := err.(*ldap.Error); ok { - if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded { - errmsg = fmt.Sprintf("too many results searching for group '%s'", filter) - i.logger.Debug().Str("backend", "ldap").Err(lerr).Msg(errmsg) - } - } - return nil, errorcode.New(errorcode.ItemNotFound, errmsg) - } - return res.Entries, nil -} - -// removeMemberFromGroupEntry creates an LDAP Modify request (not sending it) -// that would update the supplied entry to remove the specified member from the -// group -func (i *LDAP) removeMemberFromGroupEntry(group *ldap.Entry, memberDN string) (*ldap.ModifyRequest, error) { - nOldMemberDN, err := ldapdn.ParseNormalize(memberDN) - if err != nil { - return nil, err - } - members := group.GetEqualFoldAttributeValues(i.groupAttributeMap.member) - found := false - for _, member := range members { - if member == "" { - continue - } - if nMember, err := ldapdn.ParseNormalize(member); err != nil { - // We couldn't parse the member value as a DN. Let's keep it - // as it is but log a warning - i.logger.Warn().Str("memberDN", member).Err(err).Msg("Couldn't parse DN") - continue - } else { - if nMember == nOldMemberDN { - found = true - } - } - } - if !found { - i.logger.Debug().Str("backend", "ldap").Str("groupdn", group.DN).Str("member", memberDN). - Msg("The target is not a member of the group") - return nil, nil - } - - mr := ldap.ModifyRequest{DN: group.DN} - if len(members) == 1 { - mr.Add(i.groupAttributeMap.member, []string{""}) - } - mr.Delete(i.groupAttributeMap.member, []string{memberDN}) - return &mr, nil -} - -func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregraph.Group, error) { - logger := i.logger.SubloggerWithRequestID(ctx) - logger.Debug().Str("backend", "ldap").Msg("GetGroups") - - search := queryParam.Get("search") - if search == "" { - search = queryParam.Get("$search") - } - - var expandMembers bool - sel := strings.Split(queryParam.Get("$select"), ",") - exp := strings.Split(queryParam.Get("$expand"), ",") - if slices.Contains(sel, "members") || slices.Contains(exp, "members") { - expandMembers = true - } - - var groupFilter string - if search != "" { - search = ldap.EscapeFilter(search) - groupFilter = fmt.Sprintf( - "(|(%s=%s*)(%s=%s*))", - i.groupAttributeMap.name, search, - i.groupAttributeMap.id, search, - ) - } - groupFilter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.groupFilter, i.groupObjectClass, groupFilter) - - groupAttrs := []string{ - i.groupAttributeMap.name, - i.groupAttributeMap.id, - } - if expandMembers { - groupAttrs = append(groupAttrs, i.groupAttributeMap.member) - } - - searchRequest := ldap.NewSearchRequest( - i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, 0, 0, false, - groupFilter, - groupAttrs, - nil, - ) - logger.Debug().Str("backend", "ldap"). - Str("base", searchRequest.BaseDN). - Str("filter", searchRequest.Filter). - Int("scope", searchRequest.Scope). - Int("sizelimit", searchRequest.SizeLimit). - Interface("attributes", searchRequest.Attributes). - Msg("GetGroups") - res, err := i.conn.Search(searchRequest) - if err != nil { - return nil, errorcode.New(errorcode.ItemNotFound, err.Error()) - } - - groups := make([]*libregraph.Group, 0, len(res.Entries)) - - var g *libregraph.Group - for _, e := range res.Entries { - if g = i.createGroupModelFromLDAP(e); g == nil { - continue - } - if expandMembers { - members, err := i.expandLDAPGroupMembers(ctx, e) - if err != nil { - return nil, err - } - if len(members) > 0 { - m := make([]libregraph.User, 0, len(members)) - for _, ue := range members { - if u := i.createUserModelFromLDAP(ue); u != nil { - m = append(m, *u) - } - } - g.Members = m - } - } - groups = append(groups, g) - } - return groups, nil -} - -// GetGroupMembers implements the Backend Interface for the LDAP Backend -func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string) ([]*libregraph.User, error) { - logger := i.logger.SubloggerWithRequestID(ctx) - logger.Debug().Str("backend", "ldap").Msg("GetGroupMembers") - e, err := i.getLDAPGroupByNameOrID(groupID, true) - if err != nil { - return nil, err - } - - memberEntries, err := i.expandLDAPGroupMembers(ctx, e) - result := make([]*libregraph.User, 0, len(memberEntries)) - if err != nil { - return nil, err - } - for _, member := range memberEntries { - if u := i.createUserModelFromLDAP(member); u != nil { - result = append(result, u) - } - } - - return result, nil -} - -func (i *LDAP) expandLDAPGroupMembers(ctx context.Context, e *ldap.Entry) ([]*ldap.Entry, error) { - logger := i.logger.SubloggerWithRequestID(ctx) - logger.Debug().Str("backend", "ldap").Msg("expandLDAPGroupMembers") - result := []*ldap.Entry{} - - for _, memberDN := range e.GetEqualFoldAttributeValues(i.groupAttributeMap.member) { - if memberDN == "" { - continue - } - logger.Debug().Str("memberDN", memberDN).Msg("lookup") - ue, err := i.getUserByDN(memberDN) - if err != nil { - // Ignore errors when reading a specific member fails, just log them and continue - logger.Debug().Err(err).Str("member", memberDN).Msg("error reading group member") - continue - } - result = append(result, ue) - } - - return result, nil -} - -// CreateGroup implements the Backend Interface for the LDAP Backend -// It is currently restricted to managing groups based on the "groupOfNames" ObjectClass. -// As "groupOfNames" requires a "member" Attribute to be present. Empty Groups (groups -// without a member) a represented by adding an empty DN as the single member. -func (i *LDAP) CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) { - logger := i.logger.SubloggerWithRequestID(ctx) - logger.Debug().Str("backend", "ldap").Msg("create group") - if !i.writeEnabled { - return nil, errorcode.New(errorcode.NotAllowed, "server is configured read-only") - } - ar := ldap.AddRequest{ - DN: fmt.Sprintf("cn=%s,%s", oldap.EscapeDNAttributeValue(*group.DisplayName), i.groupBaseDN), - Attributes: []ldap.Attribute{ - { - Type: i.groupAttributeMap.name, - Vals: []string{*group.DisplayName}, - }, - // This is a crutch to allow groups without members for LDAP Server's which - // that apply strict Schema checking. The RFCs define "member/uniqueMember" - // as required attribute for groupOfNames/groupOfUniqueNames. So we - // add an empty string (which is a valid DN) as the initial member. - // It will be replace once real members are added. - // We might wanna use the newer, but not so broadly used "groupOfMembers" - // objectclass (RFC2307bis-02) where "member" is optional. - { - Type: i.groupAttributeMap.member, - Vals: []string{""}, - }, - }, - } - - // TODO make group objectclass configurable to support e.g. posixGroup, groupOfUniqueNames, groupOfMembers?} - objectClasses := []string{"groupOfNames", "top"} - - if !i.useServerUUID { - ar.Attribute("owncloudUUID", []string{uuid.Must(uuid.NewV4()).String()}) - objectClasses = append(objectClasses, "owncloud") - } - ar.Attribute("objectClass", objectClasses) - - if err := i.conn.Add(&ar); err != nil { - return nil, err - } - - // Read back group from LDAP to get the generated UUID - e, err := i.getGroupByDN(ar.DN) - if err != nil { - return nil, err - } - return i.createGroupModelFromLDAP(e), nil -} - -// DeleteGroup implements the Backend Interface. -func (i *LDAP) DeleteGroup(ctx context.Context, id string) error { - logger := i.logger.SubloggerWithRequestID(ctx) - logger.Debug().Str("backend", "ldap").Msg("DeleteGroup") - if !i.writeEnabled { - return errorcode.New(errorcode.NotAllowed, "server is configured read-only") - } - e, err := i.getLDAPGroupByID(id, false) - if err != nil { - return err - } - dr := ldap.DelRequest{DN: e.DN} - if err = i.conn.Del(&dr); err != nil { - return err - } - return nil -} - -// AddMembersToGroup implements the Backend Interface for the LDAP backend. -// Currently it is limited to adding Users as Group members. Adding other groups -// as members is not yet implemented -func (i *LDAP) AddMembersToGroup(ctx context.Context, groupID string, memberIDs []string) error { - logger := i.logger.SubloggerWithRequestID(ctx) - logger.Debug().Str("backend", "ldap").Msg("AddMembersToGroup") - ge, err := i.getLDAPGroupByID(groupID, true) - if err != nil { - return err - } - - mr := ldap.ModifyRequest{DN: ge.DN} - // Handle empty groups (using the empty member attribute) - current := ge.GetEqualFoldAttributeValues(i.groupAttributeMap.member) - if len(current) == 1 && current[0] == "" { - mr.Delete(i.groupAttributeMap.member, []string{""}) - } - - // Create a Set of current members for faster lookups - currentSet := make(map[string]struct{}, len(current)) - for _, currentMember := range current { - // We can ignore any empty member value here - if currentMember == "" { - continue - } - nCurrentMember, err := ldapdn.ParseNormalize(currentMember) - if err != nil { - // We couldn't parse the member value as a DN. Let's skip it, but log a warning - logger.Warn().Str("memberDN", currentMember).Err(err).Msg("Couldn't parse DN") - continue - } - currentSet[nCurrentMember] = struct{}{} - } - - var newMemberDNs []string - for _, memberID := range memberIDs { - me, err := i.getLDAPUserByID(memberID) - if err != nil { - return err - } - nDN, err := ldapdn.ParseNormalize(me.DN) - if err != nil { - logger.Error().Str("new member", me.DN).Err(err).Msg("Couldn't parse DN") - return err - } - if _, present := currentSet[nDN]; !present { - newMemberDNs = append(newMemberDNs, me.DN) - } else { - logger.Debug().Str("memberDN", me.DN).Msg("Member already present in group. Skipping") - } - } - - if len(newMemberDNs) > 0 { - mr.Add(i.groupAttributeMap.member, newMemberDNs) - - if err := i.conn.Modify(&mr); err != nil { - return err - } - } - return nil -} - -// RemoveMemberFromGroup implements the Backend Interface. -func (i *LDAP) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error { - logger := i.logger.SubloggerWithRequestID(ctx) - logger.Debug().Str("backend", "ldap").Msg("RemoveMemberFromGroup") - ge, err := i.getLDAPGroupByID(groupID, true) - if err != nil { - logger.Debug().Str("backend", "ldap").Str("groupID", groupID).Msg("Error looking up group") - return err - } - me, err := i.getLDAPUserByID(memberID) - if err != nil { - logger.Debug().Str("backend", "ldap").Str("memberID", memberID).Msg("Error looking up group member") - return err - } - logger.Debug().Str("backend", "ldap").Str("groupdn", ge.DN).Str("member", me.DN).Msg("remove member") - - if mr, err := i.removeMemberFromGroupEntry(ge, me.DN); err == nil && mr != nil { - return i.conn.Modify(mr) - } - return nil -} - func (i *LDAP) updateUserPassowrd(ctx context.Context, dn, password string) error { logger := i.logger.SubloggerWithRequestID(ctx) logger.Debug().Str("backend", "ldap").Msg("updateUserPassowrd") @@ -964,30 +514,6 @@ func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User { return nil } -func (i *LDAP) createGroupModelFromLDAP(e *ldap.Entry) *libregraph.Group { - name := e.GetEqualFoldAttributeValue(i.groupAttributeMap.name) - id := e.GetEqualFoldAttributeValue(i.groupAttributeMap.id) - - if id != "" && name != "" { - return &libregraph.Group{ - DisplayName: &name, - Id: &id, - } - } - i.logger.Warn().Str("dn", e.DN).Msg("Group is missing name or id") - return nil -} - -func (i *LDAP) groupsFromLDAPEntries(e []*ldap.Entry) []libregraph.Group { - groups := make([]libregraph.Group, 0, len(e)) - for _, g := range e { - if grp := i.createGroupModelFromLDAP(g); grp != nil { - groups = append(groups, *grp) - } - } - return groups -} - func (i *LDAP) userToLDAPAttrValues(user libregraph.User) (map[string][]string, error) { attrs := map[string][]string{ i.userAttributeMap.displayName: {user.GetDisplayName()}, diff --git a/services/graph/pkg/identity/ldap_group.go b/services/graph/pkg/identity/ldap_group.go new file mode 100644 index 000000000..ebb428a8a --- /dev/null +++ b/services/graph/pkg/identity/ldap_group.go @@ -0,0 +1,489 @@ +package identity + +import ( + "context" + "fmt" + "net/url" + "strings" + + "github.com/go-ldap/ldap/v3" + "github.com/gofrs/uuid" + ldapdn "github.com/libregraph/idm/pkg/ldapdn" + libregraph "github.com/owncloud/libre-graph-api-go" + oldap "github.com/owncloud/ocis/v2/ocis-pkg/ldap" + "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" + "golang.org/x/exp/slices" +) + +type groupAttributeMap struct { + name string + id string + member string + memberSyntax string +} + +func (i *LDAP) GetGroup(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.Group, error) { + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("GetGroup") + e, err := i.getLDAPGroupByNameOrID(nameOrID, true) + if err != nil { + return nil, err + } + sel := strings.Split(queryParam.Get("$select"), ",") + exp := strings.Split(queryParam.Get("$expand"), ",") + var g *libregraph.Group + if g = i.createGroupModelFromLDAP(e); g == nil { + return nil, errorcode.New(errorcode.ItemNotFound, "not found") + } + if slices.Contains(sel, "members") || slices.Contains(exp, "members") { + members, err := i.expandLDAPGroupMembers(ctx, e) + if err != nil { + return nil, err + } + if len(members) > 0 { + m := make([]libregraph.User, 0, len(members)) + for _, ue := range members { + if u := i.createUserModelFromLDAP(ue); u != nil { + m = append(m, *u) + } + } + g.Members = m + } + } + return g, nil +} + +func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregraph.Group, error) { + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("GetGroups") + + search := queryParam.Get("search") + if search == "" { + search = queryParam.Get("$search") + } + + var expandMembers bool + sel := strings.Split(queryParam.Get("$select"), ",") + exp := strings.Split(queryParam.Get("$expand"), ",") + if slices.Contains(sel, "members") || slices.Contains(exp, "members") { + expandMembers = true + } + + var groupFilter string + if search != "" { + search = ldap.EscapeFilter(search) + groupFilter = fmt.Sprintf( + "(|(%s=%s*)(%s=%s*))", + i.groupAttributeMap.name, search, + i.groupAttributeMap.id, search, + ) + } + groupFilter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.groupFilter, i.groupObjectClass, groupFilter) + + groupAttrs := []string{ + i.groupAttributeMap.name, + i.groupAttributeMap.id, + } + if expandMembers { + groupAttrs = append(groupAttrs, i.groupAttributeMap.member) + } + + searchRequest := ldap.NewSearchRequest( + i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, 0, 0, false, + groupFilter, + groupAttrs, + nil, + ) + logger.Debug().Str("backend", "ldap"). + Str("base", searchRequest.BaseDN). + Str("filter", searchRequest.Filter). + Int("scope", searchRequest.Scope). + Int("sizelimit", searchRequest.SizeLimit). + Interface("attributes", searchRequest.Attributes). + Msg("GetGroups") + res, err := i.conn.Search(searchRequest) + if err != nil { + return nil, errorcode.New(errorcode.ItemNotFound, err.Error()) + } + + groups := make([]*libregraph.Group, 0, len(res.Entries)) + + var g *libregraph.Group + for _, e := range res.Entries { + if g = i.createGroupModelFromLDAP(e); g == nil { + continue + } + if expandMembers { + members, err := i.expandLDAPGroupMembers(ctx, e) + if err != nil { + return nil, err + } + if len(members) > 0 { + m := make([]libregraph.User, 0, len(members)) + for _, ue := range members { + if u := i.createUserModelFromLDAP(ue); u != nil { + m = append(m, *u) + } + } + g.Members = m + } + } + groups = append(groups, g) + } + return groups, nil +} + +// GetGroupMembers implements the Backend Interface for the LDAP Backend +func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string) ([]*libregraph.User, error) { + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("GetGroupMembers") + e, err := i.getLDAPGroupByNameOrID(groupID, true) + if err != nil { + return nil, err + } + + memberEntries, err := i.expandLDAPGroupMembers(ctx, e) + result := make([]*libregraph.User, 0, len(memberEntries)) + if err != nil { + return nil, err + } + for _, member := range memberEntries { + if u := i.createUserModelFromLDAP(member); u != nil { + result = append(result, u) + } + } + + return result, nil +} + +// CreateGroup implements the Backend Interface for the LDAP Backend +// It is currently restricted to managing groups based on the "groupOfNames" ObjectClass. +// As "groupOfNames" requires a "member" Attribute to be present. Empty Groups (groups +// without a member) a represented by adding an empty DN as the single member. +func (i *LDAP) CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) { + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("create group") + if !i.writeEnabled { + return nil, errorcode.New(errorcode.NotAllowed, "server is configured read-only") + } + ar := ldap.AddRequest{ + DN: fmt.Sprintf("cn=%s,%s", oldap.EscapeDNAttributeValue(*group.DisplayName), i.groupBaseDN), + Attributes: []ldap.Attribute{ + { + Type: i.groupAttributeMap.name, + Vals: []string{*group.DisplayName}, + }, + // This is a crutch to allow groups without members for LDAP Server's which + // that apply strict Schema checking. The RFCs define "member/uniqueMember" + // as required attribute for groupOfNames/groupOfUniqueNames. So we + // add an empty string (which is a valid DN) as the initial member. + // It will be replace once real members are added. + // We might wanna use the newer, but not so broadly used "groupOfMembers" + // objectclass (RFC2307bis-02) where "member" is optional. + { + Type: i.groupAttributeMap.member, + Vals: []string{""}, + }, + }, + } + + // TODO make group objectclass configurable to support e.g. posixGroup, groupOfUniqueNames, groupOfMembers?} + objectClasses := []string{"groupOfNames", "top"} + + if !i.useServerUUID { + ar.Attribute("owncloudUUID", []string{uuid.Must(uuid.NewV4()).String()}) + objectClasses = append(objectClasses, "owncloud") + } + ar.Attribute("objectClass", objectClasses) + + if err := i.conn.Add(&ar); err != nil { + return nil, err + } + + // Read back group from LDAP to get the generated UUID + e, err := i.getGroupByDN(ar.DN) + if err != nil { + return nil, err + } + return i.createGroupModelFromLDAP(e), nil +} + +// DeleteGroup implements the Backend Interface. +func (i *LDAP) DeleteGroup(ctx context.Context, id string) error { + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("DeleteGroup") + if !i.writeEnabled { + return errorcode.New(errorcode.NotAllowed, "server is configured read-only") + } + e, err := i.getLDAPGroupByID(id, false) + if err != nil { + return err + } + dr := ldap.DelRequest{DN: e.DN} + if err = i.conn.Del(&dr); err != nil { + return err + } + return nil +} + +// AddMembersToGroup implements the Backend Interface for the LDAP backend. +// Currently it is limited to adding Users as Group members. Adding other groups +// as members is not yet implemented +func (i *LDAP) AddMembersToGroup(ctx context.Context, groupID string, memberIDs []string) error { + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("AddMembersToGroup") + ge, err := i.getLDAPGroupByID(groupID, true) + if err != nil { + return err + } + + mr := ldap.ModifyRequest{DN: ge.DN} + // Handle empty groups (using the empty member attribute) + current := ge.GetEqualFoldAttributeValues(i.groupAttributeMap.member) + if len(current) == 1 && current[0] == "" { + mr.Delete(i.groupAttributeMap.member, []string{""}) + } + + // Create a Set of current members for faster lookups + currentSet := make(map[string]struct{}, len(current)) + for _, currentMember := range current { + // We can ignore any empty member value here + if currentMember == "" { + continue + } + nCurrentMember, err := ldapdn.ParseNormalize(currentMember) + if err != nil { + // We couldn't parse the member value as a DN. Let's skip it, but log a warning + logger.Warn().Str("memberDN", currentMember).Err(err).Msg("Couldn't parse DN") + continue + } + currentSet[nCurrentMember] = struct{}{} + } + + var newMemberDNs []string + for _, memberID := range memberIDs { + me, err := i.getLDAPUserByID(memberID) + if err != nil { + return err + } + nDN, err := ldapdn.ParseNormalize(me.DN) + if err != nil { + logger.Error().Str("new member", me.DN).Err(err).Msg("Couldn't parse DN") + return err + } + if _, present := currentSet[nDN]; !present { + newMemberDNs = append(newMemberDNs, me.DN) + } else { + logger.Debug().Str("memberDN", me.DN).Msg("Member already present in group. Skipping") + } + } + + if len(newMemberDNs) > 0 { + mr.Add(i.groupAttributeMap.member, newMemberDNs) + + if err := i.conn.Modify(&mr); err != nil { + return err + } + } + return nil +} + +// RemoveMemberFromGroup implements the Backend Interface. +func (i *LDAP) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error { + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("RemoveMemberFromGroup") + ge, err := i.getLDAPGroupByID(groupID, true) + if err != nil { + logger.Debug().Str("backend", "ldap").Str("groupID", groupID).Msg("Error looking up group") + return err + } + me, err := i.getLDAPUserByID(memberID) + if err != nil { + logger.Debug().Str("backend", "ldap").Str("memberID", memberID).Msg("Error looking up group member") + return err + } + logger.Debug().Str("backend", "ldap").Str("groupdn", ge.DN).Str("member", me.DN).Msg("remove member") + + if mr, err := i.removeMemberFromGroupEntry(ge, me.DN); err == nil && mr != nil { + return i.conn.Modify(mr) + } + return nil +} + +func (i *LDAP) expandLDAPGroupMembers(ctx context.Context, e *ldap.Entry) ([]*ldap.Entry, error) { + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("expandLDAPGroupMembers") + result := []*ldap.Entry{} + + for _, memberDN := range e.GetEqualFoldAttributeValues(i.groupAttributeMap.member) { + if memberDN == "" { + continue + } + logger.Debug().Str("memberDN", memberDN).Msg("lookup") + ue, err := i.getUserByDN(memberDN) + if err != nil { + // Ignore errors when reading a specific member fails, just log them and continue + logger.Debug().Err(err).Str("member", memberDN).Msg("error reading group member") + continue + } + result = append(result, ue) + } + + return result, nil +} + +func (i *LDAP) getLDAPGroupByID(id string, requestMembers bool) (*ldap.Entry, error) { + id = ldap.EscapeFilter(id) + filter := fmt.Sprintf("(%s=%s)", i.groupAttributeMap.id, id) + return i.getLDAPGroupByFilter(filter, requestMembers) +} + +func (i *LDAP) getLDAPGroupByNameOrID(nameOrID string, requestMembers bool) (*ldap.Entry, error) { + nameOrID = ldap.EscapeFilter(nameOrID) + filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", i.groupAttributeMap.name, nameOrID, i.groupAttributeMap.id, nameOrID) + return i.getLDAPGroupByFilter(filter, requestMembers) +} + +func (i *LDAP) getLDAPGroupByFilter(filter string, requestMembers bool) (*ldap.Entry, error) { + e, err := i.getLDAPGroupsByFilter(filter, requestMembers, true) + if err != nil { + return nil, err + } + if len(e) == 0 { + return nil, errorcode.New(errorcode.ItemNotFound, "not found") + } + + return e[0], nil +} + +// Search for LDAP Groups matching the specified filter, if requestMembers is true the groupMemberShip +// attribute will be part of the result attributes. The LDAP filter is combined with the configured groupFilter +// resulting in a filter like "(&(LDAP.groupFilter)(objectClass=LDAP.groupObjectClass)())" +func (i *LDAP) getLDAPGroupsByFilter(filter string, requestMembers, single bool) ([]*ldap.Entry, error) { + attrs := []string{ + i.groupAttributeMap.name, + i.groupAttributeMap.id, + } + + if requestMembers { + attrs = append(attrs, i.groupAttributeMap.member) + } + + sizelimit := 0 + if single { + sizelimit = 1 + } + searchRequest := ldap.NewSearchRequest( + i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, sizelimit, 0, false, + fmt.Sprintf("(&%s(objectClass=%s)%s)", i.groupFilter, i.groupObjectClass, filter), + attrs, + nil, + ) + i.logger.Debug().Str("backend", "ldap"). + Str("base", searchRequest.BaseDN). + Str("filter", searchRequest.Filter). + Int("scope", searchRequest.Scope). + Int("sizelimit", searchRequest.SizeLimit). + Interface("attributes", searchRequest.Attributes). + Msg("getLDAPGroupsByFilter") + res, err := i.conn.Search(searchRequest) + if err != nil { + var errmsg string + if lerr, ok := err.(*ldap.Error); ok { + if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded { + errmsg = fmt.Sprintf("too many results searching for group '%s'", filter) + i.logger.Debug().Str("backend", "ldap").Err(lerr).Msg(errmsg) + } + } + return nil, errorcode.New(errorcode.ItemNotFound, errmsg) + } + return res.Entries, nil +} + +// removeMemberFromGroupEntry creates an LDAP Modify request (not sending it) +// that would update the supplied entry to remove the specified member from the +// group +func (i *LDAP) removeMemberFromGroupEntry(group *ldap.Entry, memberDN string) (*ldap.ModifyRequest, error) { + nOldMemberDN, err := ldapdn.ParseNormalize(memberDN) + if err != nil { + return nil, err + } + members := group.GetEqualFoldAttributeValues(i.groupAttributeMap.member) + found := false + for _, member := range members { + if member == "" { + continue + } + if nMember, err := ldapdn.ParseNormalize(member); err != nil { + // We couldn't parse the member value as a DN. Let's keep it + // as it is but log a warning + i.logger.Warn().Str("memberDN", member).Err(err).Msg("Couldn't parse DN") + continue + } else { + if nMember == nOldMemberDN { + found = true + } + } + } + if !found { + i.logger.Debug().Str("backend", "ldap").Str("groupdn", group.DN).Str("member", memberDN). + Msg("The target is not a member of the group") + return nil, nil + } + + mr := ldap.ModifyRequest{DN: group.DN} + if len(members) == 1 { + mr.Add(i.groupAttributeMap.member, []string{""}) + } + mr.Delete(i.groupAttributeMap.member, []string{memberDN}) + return &mr, nil +} + +func (i *LDAP) getGroupByDN(dn string) (*ldap.Entry, error) { + attrs := []string{ + i.groupAttributeMap.id, + i.groupAttributeMap.name, + } + filter := fmt.Sprintf("(objectClass=%s)", i.groupObjectClass) + + if i.groupFilter != "" { + filter = fmt.Sprintf("(&%s(%s))", filter, i.groupFilter) + } + return i.getEntryByDN(dn, attrs, filter) +} + +func (i *LDAP) getGroupsForUser(dn string) ([]*ldap.Entry, error) { + groupFilter := fmt.Sprintf( + "(%s=%s)", + i.groupAttributeMap.member, dn, + ) + userGroups, err := i.getLDAPGroupsByFilter(groupFilter, false, false) + if err != nil { + return nil, err + } + return userGroups, nil +} + +func (i *LDAP) createGroupModelFromLDAP(e *ldap.Entry) *libregraph.Group { + name := e.GetEqualFoldAttributeValue(i.groupAttributeMap.name) + id := e.GetEqualFoldAttributeValue(i.groupAttributeMap.id) + + if id != "" && name != "" { + return &libregraph.Group{ + DisplayName: &name, + Id: &id, + } + } + i.logger.Warn().Str("dn", e.DN).Msg("Group is missing name or id") + return nil +} + +func (i *LDAP) groupsFromLDAPEntries(e []*ldap.Entry) []libregraph.Group { + groups := make([]libregraph.Group, 0, len(e)) + for _, g := range e { + if grp := i.createGroupModelFromLDAP(g); grp != nil { + groups = append(groups, *grp) + } + } + return groups +} diff --git a/services/graph/pkg/identity/ldap_group_test.go b/services/graph/pkg/identity/ldap_group_test.go new file mode 100644 index 000000000..da79a9772 --- /dev/null +++ b/services/graph/pkg/identity/ldap_group_test.go @@ -0,0 +1,226 @@ +package identity + +import ( + "context" + "errors" + "net/url" + "testing" + + "github.com/go-ldap/ldap/v3" + "github.com/owncloud/ocis/v2/services/graph/mocks" + "github.com/test-go/testify/mock" +) + +var groupEntry = ldap.NewEntry("cn=group", + map[string][]string{ + "cn": {"group"}, + "entryuuid": {"abcd-defg"}, + "member": { + "uid=user,ou=people,dc=test", + "uid=invalid,ou=people,dc=test", + }, + }) +var invalidGroupEntry = ldap.NewEntry("cn=invalid", + map[string][]string{ + "cn": {"invalid"}, + }) + +func TestGetGroup(t *testing.T) { + // Mock a Sizelimit Error + lm := &mocks.Client{} + lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock"))) + + queryParamExpand := url.Values{ + "$expand": []string{"members"}, + } + queryParamSelect := url.Values{ + "$select": []string{"members"}, + } + b, _ := getMockedBackend(lm, lconfig, &logger) + _, err := b.GetGroup(context.Background(), "group", nil) + if err == nil || err.Error() != "itemNotFound" { + t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) + } + _, err = b.GetGroup(context.Background(), "group", queryParamExpand) + if err == nil || err.Error() != "itemNotFound" { + t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) + } + _, err = b.GetGroup(context.Background(), "group", queryParamSelect) + if err == nil || err.Error() != "itemNotFound" { + t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) + } + + // Mock an empty Search Result + lm = &mocks.Client{} + lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil) + b, _ = getMockedBackend(lm, lconfig, &logger) + _, err = b.GetGroup(context.Background(), "group", nil) + if err == nil || err.Error() != "itemNotFound" { + t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) + } + _, err = b.GetGroup(context.Background(), "group", queryParamExpand) + if err == nil || err.Error() != "itemNotFound" { + t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) + } + _, err = b.GetGroup(context.Background(), "group", queryParamSelect) + if err == nil || err.Error() != "itemNotFound" { + t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) + } + + // Mock an invalid Search Result + lm = &mocks.Client{} + lm.On("Search", mock.Anything).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{invalidGroupEntry}, + }, nil) + b, _ = getMockedBackend(lm, lconfig, &logger) + g, err := b.GetGroup(context.Background(), "group", nil) + if err == nil || err.Error() != "itemNotFound" { + t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) + } + g, err = b.GetGroup(context.Background(), "group", queryParamExpand) + if err == nil || err.Error() != "itemNotFound" { + t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) + } + g, err = b.GetGroup(context.Background(), "group", queryParamSelect) + if err == nil || err.Error() != "itemNotFound" { + t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) + } + + // Mock a valid Search Result + lm = &mocks.Client{} + sr1 := &ldap.SearchRequest{ + BaseDN: "ou=groups,dc=test", + Scope: 2, + SizeLimit: 1, + Filter: "(&(objectClass=groupOfNames)(|(cn=group)(entryUUID=group)))", + Attributes: []string{"cn", "entryUUID", "member"}, + Controls: []ldap.Control(nil), + } + sr2 := &ldap.SearchRequest{ + BaseDN: "uid=user,ou=people,dc=test", + SizeLimit: 1, + Filter: "(objectClass=inetOrgPerson)", + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"}, + Controls: []ldap.Control(nil), + } + sr3 := &ldap.SearchRequest{ + BaseDN: "uid=invalid,ou=people,dc=test", + SizeLimit: 1, + Filter: "(objectClass=inetOrgPerson)", + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"}, + Controls: []ldap.Control(nil), + } + + lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil) + lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil) + lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{invalidUserEntry}}, nil) + b, _ = getMockedBackend(lm, lconfig, &logger) + g, err = b.GetGroup(context.Background(), "group", nil) + if err != nil { + t.Errorf("Expected GetGroup to succeed. Got %s", err.Error()) + } else if *g.Id != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) { + t.Errorf("Expected GetGroup to return a valid group") + } + g, err = b.GetGroup(context.Background(), "group", queryParamExpand) + switch { + case err != nil: + t.Errorf("Expected GetGroup to succeed. Got %s", err.Error()) + case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id): + t.Errorf("Expected GetGroup to return a valid group") + case len(g.Members) != 1: + t.Errorf("Expected GetGroup with expand to return one member") + case g.Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id): + t.Errorf("Expected GetGroup with expand to return correct member") + } + g, err = b.GetGroup(context.Background(), "group", queryParamSelect) + switch { + case err != nil: + t.Errorf("Expected GetGroup to succeed. Got %s", err.Error()) + case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id): + t.Errorf("Expected GetGroup to return a valid group") + case len(g.Members) != 1: + t.Errorf("Expected GetGroup with expand to return one member") + case g.Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id): + t.Errorf("Expected GetGroup with expand to return correct member") + } +} + +func TestGetGroups(t *testing.T) { + queryParamExpand := url.Values{ + "$expand": []string{"members"}, + } + queryParamSelect := url.Values{ + "$select": []string{"members"}, + } + lm := &mocks.Client{} + lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock"))) + b, _ := getMockedBackend(lm, lconfig, &logger) + _, err := b.GetGroups(context.Background(), url.Values{}) + if err == nil || err.Error() != "itemNotFound" { + t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) + } + + lm = &mocks.Client{} + lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil) + b, _ = getMockedBackend(lm, lconfig, &logger) + g, err := b.GetGroups(context.Background(), url.Values{}) + if err != nil { + t.Errorf("Expected success, got '%s'", err.Error()) + } else if g == nil || len(g) != 0 { + t.Errorf("Expected zero length user slice") + } + + lm = &mocks.Client{} + lm.On("Search", mock.Anything).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{groupEntry}, + }, nil) + b, _ = getMockedBackend(lm, lconfig, &logger) + g, err = b.GetGroups(context.Background(), url.Values{}) + if err != nil { + t.Errorf("Expected GetGroup to succeed. Got %s", err.Error()) + } else if *g[0].Id != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) { + t.Errorf("Expected GetGroup to return a valid group") + } + + // Mock a valid Search Result with expanded group members + lm = &mocks.Client{} + sr1 := &ldap.SearchRequest{ + BaseDN: "ou=groups,dc=test", + Scope: 2, + Filter: "(&(objectClass=groupOfNames))", + Attributes: []string{"cn", "entryUUID", "member"}, + Controls: []ldap.Control(nil), + } + sr2 := &ldap.SearchRequest{ + BaseDN: "uid=user,ou=people,dc=test", + SizeLimit: 1, + Filter: "(objectClass=inetOrgPerson)", + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"}, + Controls: []ldap.Control(nil), + } + sr3 := &ldap.SearchRequest{ + BaseDN: "uid=invalid,ou=people,dc=test", + SizeLimit: 1, + Filter: "(objectClass=inetOrgPerson)", + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"}, + Controls: []ldap.Control(nil), + } + + for _, param := range []url.Values{queryParamSelect, queryParamExpand} { + lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil) + lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil) + lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{invalidUserEntry}}, nil) + b, _ = getMockedBackend(lm, lconfig, &logger) + g, err = b.GetGroups(context.Background(), param) + switch { + case err != nil: + t.Errorf("Expected GetGroup to succeed. Got %s", err.Error()) + case g[0].GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id): + t.Errorf("Expected GetGroup to return a valid group") + case len(g[0].Members) != 1: + t.Errorf("Expected GetGroup to return group with one member") + case g[0].Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id): + t.Errorf("Expected GetGroup to return group with correct member") + } + } +} diff --git a/services/graph/pkg/identity/ldap_test.go b/services/graph/pkg/identity/ldap_test.go index 92e7c156e..1f92e5021 100644 --- a/services/graph/pkg/identity/ldap_test.go +++ b/services/graph/pkg/identity/ldap_test.go @@ -57,21 +57,6 @@ var invalidUserEntry = ldap.NewEntry("uid=user", "mail": {"user@example"}, }) -var groupEntry = ldap.NewEntry("cn=group", - map[string][]string{ - "cn": {"group"}, - "entryuuid": {"abcd-defg"}, - "member": { - "uid=user,ou=people,dc=test", - "uid=invalid,ou=people,dc=test", - }, - }) - -var invalidGroupEntry = ldap.NewEntry("cn=invalid", - map[string][]string{ - "cn": {"invalid"}, - }) - var logger = log.NewLogger(log.Level("debug")) func TestNewLDAPBackend(t *testing.T) { @@ -297,203 +282,3 @@ func TestGetUsers(t *testing.T) { t.Errorf("Expected zero length user slice") } } - -func TestGetGroup(t *testing.T) { - // Mock a Sizelimit Error - lm := &mocks.Client{} - lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock"))) - - queryParamExpand := url.Values{ - "$expand": []string{"members"}, - } - queryParamSelect := url.Values{ - "$select": []string{"members"}, - } - b, _ := getMockedBackend(lm, lconfig, &logger) - _, err := b.GetGroup(context.Background(), "group", nil) - if err == nil || err.Error() != "itemNotFound" { - t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) - } - _, err = b.GetGroup(context.Background(), "group", queryParamExpand) - if err == nil || err.Error() != "itemNotFound" { - t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) - } - _, err = b.GetGroup(context.Background(), "group", queryParamSelect) - if err == nil || err.Error() != "itemNotFound" { - t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) - } - - // Mock an empty Search Result - lm = &mocks.Client{} - lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil) - b, _ = getMockedBackend(lm, lconfig, &logger) - _, err = b.GetGroup(context.Background(), "group", nil) - if err == nil || err.Error() != "itemNotFound" { - t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) - } - _, err = b.GetGroup(context.Background(), "group", queryParamExpand) - if err == nil || err.Error() != "itemNotFound" { - t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) - } - _, err = b.GetGroup(context.Background(), "group", queryParamSelect) - if err == nil || err.Error() != "itemNotFound" { - t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) - } - - // Mock an invalid Search Result - lm = &mocks.Client{} - lm.On("Search", mock.Anything).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{invalidGroupEntry}, - }, nil) - b, _ = getMockedBackend(lm, lconfig, &logger) - g, err := b.GetGroup(context.Background(), "group", nil) - if err == nil || err.Error() != "itemNotFound" { - t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) - } - g, err = b.GetGroup(context.Background(), "group", queryParamExpand) - if err == nil || err.Error() != "itemNotFound" { - t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) - } - g, err = b.GetGroup(context.Background(), "group", queryParamSelect) - if err == nil || err.Error() != "itemNotFound" { - t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) - } - - // Mock a valid Search Result - lm = &mocks.Client{} - sr1 := &ldap.SearchRequest{ - BaseDN: "ou=groups,dc=test", - Scope: 2, - SizeLimit: 1, - Filter: "(&(objectClass=groupOfNames)(|(cn=group)(entryUUID=group)))", - Attributes: []string{"cn", "entryUUID", "member"}, - Controls: []ldap.Control(nil), - } - sr2 := &ldap.SearchRequest{ - BaseDN: "uid=user,ou=people,dc=test", - SizeLimit: 1, - Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"}, - Controls: []ldap.Control(nil), - } - sr3 := &ldap.SearchRequest{ - BaseDN: "uid=invalid,ou=people,dc=test", - SizeLimit: 1, - Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"}, - Controls: []ldap.Control(nil), - } - - lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil) - lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil) - lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{invalidUserEntry}}, nil) - b, _ = getMockedBackend(lm, lconfig, &logger) - g, err = b.GetGroup(context.Background(), "group", nil) - if err != nil { - t.Errorf("Expected GetGroup to succeed. Got %s", err.Error()) - } else if *g.Id != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) { - t.Errorf("Expected GetGroup to return a valid group") - } - g, err = b.GetGroup(context.Background(), "group", queryParamExpand) - switch { - case err != nil: - t.Errorf("Expected GetGroup to succeed. Got %s", err.Error()) - case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id): - t.Errorf("Expected GetGroup to return a valid group") - case len(g.Members) != 1: - t.Errorf("Expected GetGroup with expand to return one member") - case g.Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id): - t.Errorf("Expected GetGroup with expand to return correct member") - } - g, err = b.GetGroup(context.Background(), "group", queryParamSelect) - switch { - case err != nil: - t.Errorf("Expected GetGroup to succeed. Got %s", err.Error()) - case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id): - t.Errorf("Expected GetGroup to return a valid group") - case len(g.Members) != 1: - t.Errorf("Expected GetGroup with expand to return one member") - case g.Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id): - t.Errorf("Expected GetGroup with expand to return correct member") - } -} - -func TestGetGroups(t *testing.T) { - queryParamExpand := url.Values{ - "$expand": []string{"members"}, - } - queryParamSelect := url.Values{ - "$select": []string{"members"}, - } - lm := &mocks.Client{} - lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock"))) - b, _ := getMockedBackend(lm, lconfig, &logger) - _, err := b.GetGroups(context.Background(), url.Values{}) - if err == nil || err.Error() != "itemNotFound" { - t.Errorf("Expected 'itemNotFound' got '%s'", err.Error()) - } - - lm = &mocks.Client{} - lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil) - b, _ = getMockedBackend(lm, lconfig, &logger) - g, err := b.GetGroups(context.Background(), url.Values{}) - if err != nil { - t.Errorf("Expected success, got '%s'", err.Error()) - } else if g == nil || len(g) != 0 { - t.Errorf("Expected zero length user slice") - } - - lm = &mocks.Client{} - lm.On("Search", mock.Anything).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{groupEntry}, - }, nil) - b, _ = getMockedBackend(lm, lconfig, &logger) - g, err = b.GetGroups(context.Background(), url.Values{}) - if err != nil { - t.Errorf("Expected GetGroup to succeed. Got %s", err.Error()) - } else if *g[0].Id != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) { - t.Errorf("Expected GetGroup to return a valid group") - } - - // Mock a valid Search Result with expanded group members - lm = &mocks.Client{} - sr1 := &ldap.SearchRequest{ - BaseDN: "ou=groups,dc=test", - Scope: 2, - Filter: "(&(objectClass=groupOfNames))", - Attributes: []string{"cn", "entryUUID", "member"}, - Controls: []ldap.Control(nil), - } - sr2 := &ldap.SearchRequest{ - BaseDN: "uid=user,ou=people,dc=test", - SizeLimit: 1, - Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"}, - Controls: []ldap.Control(nil), - } - sr3 := &ldap.SearchRequest{ - BaseDN: "uid=invalid,ou=people,dc=test", - SizeLimit: 1, - Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"}, - Controls: []ldap.Control(nil), - } - - for _, param := range []url.Values{queryParamSelect, queryParamExpand} { - lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil) - lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil) - lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{invalidUserEntry}}, nil) - b, _ = getMockedBackend(lm, lconfig, &logger) - g, err = b.GetGroups(context.Background(), param) - switch { - case err != nil: - t.Errorf("Expected GetGroup to succeed. Got %s", err.Error()) - case g[0].GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id): - t.Errorf("Expected GetGroup to return a valid group") - case len(g[0].Members) != 1: - t.Errorf("Expected GetGroup to return group with one member") - case g[0].Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id): - t.Errorf("Expected GetGroup to return group with correct member") - } - } -}