graph: Move LDAP groups related code to a separate file

This commit is contained in:
Ralf Haferkamp
2023-01-10 12:19:40 +01:00
committed by Ralf Haferkamp
parent 2a88301507
commit 6d5637ad79
4 changed files with 715 additions and 689 deletions

View File

@@ -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)(<filter_from_args>))"
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()},

View File

@@ -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)(<filter_from_args>))"
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
}

View File

@@ -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")
}
}
}

View File

@@ -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")
}
}
}