Merge pull request #2421 from rhafer/issue/1599

feat(graph/education): Add support of 'eq' filters on users
This commit is contained in:
Jörn Friedrich Dreyer
2026-03-06 10:43:38 +01:00
committed by GitHub
13 changed files with 429 additions and 127 deletions

View File

@@ -31,11 +31,13 @@ var demoTenants = []tenantWithUsers{
DisplayName: libregraph.PtrString("Dennis Ritchie"),
OnPremisesSamAccountName: libregraph.PtrString("dennis"),
Mail: libregraph.PtrString("dennis@example.org"),
ExternalId: libregraph.PtrString("ExternalID1"),
},
{
DisplayName: libregraph.PtrString("Grace Hopper"),
OnPremisesSamAccountName: libregraph.PtrString("grace"),
Mail: libregraph.PtrString("grace@example.org"),
ExternalId: libregraph.PtrString("ExternalID2"),
},
},
},
@@ -49,11 +51,13 @@ var demoTenants = []tenantWithUsers{
DisplayName: libregraph.PtrString("Albert Einstein"),
OnPremisesSamAccountName: libregraph.PtrString("einstein"),
Mail: libregraph.PtrString("einstein@example.org"),
ExternalId: libregraph.PtrString("ExternalID3"),
},
{
DisplayName: libregraph.PtrString("Marie Curie"),
OnPremisesSamAccountName: libregraph.PtrString("marie"),
Mail: libregraph.PtrString("marie@example.org"),
ExternalId: libregraph.PtrString("ExternalID4"),
},
},
},

View File

@@ -68,6 +68,8 @@ type EducationBackend interface {
GetEducationSchool(ctx context.Context, nameOrID string) (*libregraph.EducationSchool, error)
// GetEducationSchools lists all schools
GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error)
// FilterEducationSchoolsByAttribute list all schools where an attribute matches a value, e.g. all schools with a given externalId
FilterEducationSchoolsByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationSchool, error)
// UpdateEducationSchool updates attributes of a school
UpdateEducationSchool(ctx context.Context, numberOrID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error)
// GetEducationSchoolUsers lists all members of a school
@@ -107,6 +109,8 @@ type EducationBackend interface {
GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error)
// GetEducationUsers lists all education users
GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error)
// FilterEducationUsersByAttribute list all education users where and attribute matches a value, e.g. all users with a given externalid
FilterEducationUsersByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationUser, error)
// GetEducationClassTeachers returns the EducationUser teachers for an EducationClass
GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error)

View File

@@ -29,6 +29,11 @@ func (i *ErrEducationBackend) GetEducationSchools(ctx context.Context) ([]*libre
return nil, errNotImplemented
}
// FilterEducationSchoolsByAttribute implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) FilterEducationSchoolsByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationSchool, error) {
return nil, errNotImplemented
}
// UpdateEducationSchool implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) UpdateEducationSchool(ctx context.Context, numberOrID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
return nil, errNotImplemented
@@ -119,6 +124,11 @@ func (i *ErrEducationBackend) GetEducationUsers(ctx context.Context) ([]*libregr
return nil, errNotImplemented
}
// FilterEducationUsersByAttribute implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) FilterEducationUsersByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// GetEducationClassTeachers implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"slices"
"time"
"github.com/go-ldap/ldap/v3"
@@ -49,10 +50,9 @@ const (
)
var (
errNotSet = errors.New("attribute not set")
errSchoolNameExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that name is already present")
errSchoolNumberExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that number is already present")
errSchoolExternalIdExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that external id is already present")
errNotSet = errors.New("attribute not set")
errSchoolNameExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that name is already present")
errSchoolNumberExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that number is already present")
)
func defaultEducationConfig() educationConfig {
@@ -136,21 +136,6 @@ func (i *LDAP) CreateEducationSchool(ctx context.Context, school libregraph.Educ
}
}
// Check that the school external id is not already used
if school.HasExternalId() {
_, err := i.getSchoolByExternalId(school.GetExternalId())
switch {
case err == nil:
logger.Debug().Err(errSchoolExternalIdExists).Str("externalId", school.GetExternalId()).Msg("duplicate school external id")
return nil, errSchoolExternalIdExists
case errors.Is(err, ErrNotFound):
break
default:
logger.Error().Err(err).Str("externalId", school.GetExternalId()).Msg("error looking up school by external id")
return nil, errorcode.New(errorcode.GeneralException, "error looking up school by external id")
}
}
attributeTypeAndValue := ldap.AttributeTypeAndValue{
Type: i.educationConfig.schoolAttributeMap.displayName,
Value: school.GetDisplayName(),
@@ -299,14 +284,14 @@ func (i *LDAP) updateSchoolProperties(ctx context.Context, dn string, currentSch
}
// UpdateEducationSchool updates the supplied school in the identity backend
func (i *LDAP) UpdateEducationSchool(ctx context.Context, numberOrIDOrExternalID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
func (i *LDAP) UpdateEducationSchool(ctx context.Context, numberOrID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("UpdateEducationSchool")
if !i.writeEnabled {
return nil, ErrReadOnly
}
e, err := i.getSchoolByNumberOrIDOrExternalID(numberOrIDOrExternalID)
e, err := i.getSchoolByNumberOrID(numberOrID)
if err != nil {
return nil, err
}
@@ -329,7 +314,7 @@ func (i *LDAP) UpdateEducationSchool(ctx context.Context, numberOrIDOrExternalID
}
// Read back school from LDAP
e, err = i.getSchoolByNumberOrIDOrExternalID(i.getID(e))
e, err = i.getSchoolByNumberOrID(i.getID(e))
if err != nil {
return nil, err
}
@@ -343,7 +328,7 @@ func (i *LDAP) DeleteEducationSchool(ctx context.Context, id string) error {
if !i.writeEnabled {
return ErrReadOnly
}
e, err := i.getSchoolByNumberOrIDOrExternalID(id)
e, err := i.getSchoolByNumberOrID(id)
if err != nil {
return err
}
@@ -358,10 +343,10 @@ func (i *LDAP) DeleteEducationSchool(ctx context.Context, id string) error {
}
// GetEducationSchool implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationSchool(ctx context.Context, numberOrIDOrExternalID string) (*libregraph.EducationSchool, error) {
func (i *LDAP) GetEducationSchool(ctx context.Context, numberOrID string) (*libregraph.EducationSchool, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetEducationSchool")
e, err := i.getSchoolByNumberOrIDOrExternalID(numberOrIDOrExternalID)
e, err := i.getSchoolByNumberOrID(numberOrID)
if err != nil {
return nil, err
}
@@ -371,13 +356,36 @@ func (i *LDAP) GetEducationSchool(ctx context.Context, numberOrIDOrExternalID st
// GetEducationSchools implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error) {
var filter string
filter = fmt.Sprintf("(objectClass=%s)", i.educationConfig.schoolObjectClass)
filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.schoolObjectClass)
if i.educationConfig.schoolFilter != "" {
filter = fmt.Sprintf("(&%s%s)", i.educationConfig.schoolFilter, filter)
}
return i.searchEducationSchools(ctx, filter)
}
// FilterEducationSchoolsByAttribute implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) FilterEducationSchoolsByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationSchool, error) {
logger := i.logger.SubloggerWithRequestID(ctx).With().Str("func", "FilterEducationSchoolsByAttribute").Logger()
logger.Debug().Str("backend", "ldap").Str("attribute", attr).Str("value", value).Msg("")
var ldapAttr string
switch attr {
case "externalId":
ldapAttr = i.educationConfig.schoolAttributeMap.externalId
default:
return nil, errorcode.New(errorcode.InvalidRequest, fmt.Sprintf("filtering by attribute '%s' is not supported", attr))
}
filter := fmt.Sprintf("(&%s(objectClass=%s)(%s=%s))",
i.educationConfig.schoolFilter,
i.educationConfig.schoolObjectClass,
ldap.EscapeFilter(ldapAttr),
ldap.EscapeFilter(value),
)
return i.searchEducationSchools(ctx, filter)
}
// searchEducationSchools builds and executes an LDAP search for education schools and converts the results to EducationSchool models.
func (i *LDAP) searchEducationSchools(ctx context.Context, filter string) ([]*libregraph.EducationSchool, error) {
searchRequest := ldap.NewSearchRequest(
i.educationConfig.schoolBaseDN,
i.educationConfig.schoolScope,
@@ -386,13 +394,15 @@ func (i *LDAP) GetEducationSchools(ctx context.Context) ([]*libregraph.Education
i.getEducationSchoolAttrTypes(),
nil,
)
i.logger.Debug().Str("backend", "ldap").
logger := i.logger.SubloggerWithRequestID(ctx)
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("GetEducationSchools")
Msg("searchEducationSchools")
res, err := i.conn.Search(searchRequest)
if err != nil {
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
@@ -436,11 +446,11 @@ func (i *LDAP) GetEducationSchoolUsers(ctx context.Context, schoolNumberOrID str
}
// AddUsersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
func (i *LDAP) AddUsersToEducationSchool(ctx context.Context, schoolNumberOrIDOrExternalID string, memberIDs []string) error {
func (i *LDAP) AddUsersToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("AddUsersToEducationSchool")
schoolEntry, err := i.getSchoolByNumberOrIDOrExternalID(schoolNumberOrIDOrExternalID)
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
if err != nil {
return err
}
@@ -462,32 +472,31 @@ func (i *LDAP) AddUsersToEducationSchool(ctx context.Context, schoolNumberOrIDOr
}
for _, userEntry := range userEntries {
currentSchools := userEntry.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
found := false
for _, currentSchool := range currentSchools {
if currentSchool == schoolID {
found = true
break
}
}
if !found {
mr := ldap.ModifyRequest{DN: userEntry.DN}
mr.Add(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
if err := i.conn.Modify(&mr); err != nil {
return err
}
if err := i.addEntryToSchool(userEntry, schoolID); err != nil {
return err
}
}
return nil
}
// addEntryToSchool adds the schoolID to the entry's memberOfSchool attribute if not already present.
func (i *LDAP) addEntryToSchool(entry *ldap.Entry, schoolID string) error {
currentSchools := entry.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
if slices.Contains(currentSchools, schoolID) {
return nil
}
mr := ldap.ModifyRequest{DN: entry.DN}
mr.Add(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
return i.conn.Modify(&mr)
}
// RemoveUserFromEducationSchool removes a single member (by ID) from a school
func (i *LDAP) RemoveUserFromEducationSchool(ctx context.Context, schoolNumberOrIDOrExternalID string, memberID string) error {
func (i *LDAP) RemoveUserFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("RemoveUserFromEducationSchool")
schoolEntry, err := i.getSchoolByNumberOrIDOrExternalID(schoolNumberOrIDOrExternalID)
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
if err != nil {
return err
}
@@ -542,12 +551,12 @@ func (i *LDAP) GetEducationSchoolClasses(ctx context.Context, schoolNumberOrID s
}
func (i *LDAP) getEducationSchoolEntries(
schoolNumberOrIDOrExternalID, filter, objectClass, baseDN string,
schoolNumberOrID, filter, objectClass, baseDN string,
scope int,
attributes []string,
logger log.Logger,
) ([]*ldap.Entry, error) {
schoolEntry, err := i.getSchoolByNumberOrIDOrExternalID(schoolNumberOrIDOrExternalID)
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
if err != nil {
return nil, err
}
@@ -584,11 +593,11 @@ func (i *LDAP) getEducationSchoolEntries(
}
// AddClassesToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
func (i *LDAP) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrIDOrExternalID string, memberIDs []string) error {
func (i *LDAP) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("AddClassesToEducationSchool")
schoolEntry, err := i.getSchoolByNumberOrIDOrExternalID(schoolNumberOrIDOrExternalID)
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
if err != nil {
return err
}
@@ -610,20 +619,8 @@ func (i *LDAP) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID
}
for _, classEntry := range classEntries {
currentSchools := classEntry.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
found := false
for _, currentSchool := range currentSchools {
if currentSchool == schoolID {
found = true
break
}
}
if !found {
mr := ldap.ModifyRequest{DN: classEntry.DN}
mr.Add(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
if err := i.conn.Modify(&mr); err != nil {
return err
}
if err := i.addEntryToSchool(classEntry, schoolID); err != nil {
return err
}
}
@@ -631,11 +628,11 @@ func (i *LDAP) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID
}
// RemoveClassFromEducationSchool removes a single member (by ID) from a school
func (i *LDAP) RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrIDOrExternalID string, memberID string) error {
func (i *LDAP) RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("RemoveClassFromEducationSchool")
schoolEntry, err := i.getSchoolByNumberOrIDOrExternalID(schoolNumberOrIDOrExternalID)
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
if err != nil {
return err
}
@@ -673,16 +670,14 @@ func (i *LDAP) getSchoolByDN(dn string) (*ldap.Entry, error) {
return i.getEntryByDN(dn, i.getEducationSchoolAttrTypes(), filter)
}
func (i *LDAP) getSchoolByNumberOrIDOrExternalID(numberOrIDOrExternalID string) (*ldap.Entry, error) {
numberOrIDOrExternalID = ldap.EscapeFilter(numberOrIDOrExternalID)
func (i *LDAP) getSchoolByNumberOrID(numberOrID string) (*ldap.Entry, error) {
numberOrID = ldap.EscapeFilter(numberOrID)
filter := fmt.Sprintf(
"(|(%s=%s)(%s=%s)(%s=%s))",
"(|(%s=%s)(%s=%s))",
i.educationConfig.schoolAttributeMap.id,
numberOrIDOrExternalID,
numberOrID,
i.educationConfig.schoolAttributeMap.schoolNumber,
numberOrIDOrExternalID,
i.educationConfig.schoolAttributeMap.externalId,
numberOrIDOrExternalID,
numberOrID,
)
return i.getSchoolByFilter(filter)
}
@@ -697,16 +692,6 @@ func (i *LDAP) getSchoolByNumber(schoolNumber string) (*ldap.Entry, error) {
return i.getSchoolByFilter(filter)
}
func (i *LDAP) getSchoolByExternalId(schoolExternalId string) (*ldap.Entry, error) {
schoolExternalId = ldap.EscapeFilter(schoolExternalId)
filter := fmt.Sprintf(
"(%s=%s)",
i.educationConfig.schoolAttributeMap.externalId,
schoolExternalId,
)
return i.getSchoolByFilter(filter)
}
func (i *LDAP) getSchoolByFilter(filter string) (*ldap.Entry, error) {
filter = fmt.Sprintf("(&%s(objectClass=%s)%s)",
i.educationConfig.schoolFilter,
@@ -820,6 +805,7 @@ func (i *LDAP) getEducationSchoolAttrTypes() []string {
return []string{
i.educationConfig.schoolAttributeMap.displayName,
i.educationConfig.schoolAttributeMap.id,
i.educationConfig.schoolAttributeMap.externalId,
i.educationConfig.schoolAttributeMap.schoolNumber,
i.educationConfig.schoolAttributeMap.terminationDate,
}

View File

@@ -65,10 +65,12 @@ var schoolEntryWithTermination = ldap.NewEntry("ou=Test School",
})
var (
filterSchoolSearchByIdExisting = "(&(objectClass=openCloudEducationSchool)(|(openCloudUUID=abcd-defg)(openCloudEducationSchoolNumber=abcd-defg)(openCloudEducationExternalId=abcd-defg)))"
filterSchoolSearchByIdNonexistant = "(&(objectClass=openCloudEducationSchool)(|(openCloudUUID=xxxx-xxxx)(openCloudEducationSchoolNumber=xxxx-xxxx)(openCloudEducationExternalId=xxxx-xxxx)))"
filterSchoolSearchByNumberExisting = "(&(objectClass=openCloudEducationSchool)(|(openCloudUUID=0123)(openCloudEducationSchoolNumber=0123)(openCloudEducationExternalId=0123)))"
filterSchoolSearchByNumberNonexistant = "(&(objectClass=openCloudEducationSchool)(|(openCloudUUID=3210)(openCloudEducationSchoolNumber=3210)(openCloudEducationExternalId=3210)))"
filterSchoolSearchByIdExisting = "(&(objectClass=openCloudEducationSchool)(|(openCloudUUID=abcd-defg)(openCloudEducationSchoolNumber=abcd-defg)))"
filterSchoolSearchByIdNonexistant = "(&(objectClass=openCloudEducationSchool)(|(openCloudUUID=xxxx-xxxx)(openCloudEducationSchoolNumber=xxxx-xxxx)))"
filterSchoolSearchByNumberExisting = "(&(objectClass=openCloudEducationSchool)(|(openCloudUUID=0123)(openCloudEducationSchoolNumber=0123)))"
filterSchoolSearchByNumberNonexistant = "(&(objectClass=openCloudEducationSchool)(|(openCloudUUID=3210)(openCloudEducationSchoolNumber=3210)))"
schoolLDAPAttributeTypes = []string{"ou", "openCloudUUID", "openCloudEducationExternalId", "openCloudEducationSchoolNumber", "openCloudEducationSchoolTerminationTimestamp"}
)
func TestCreateEducationSchool(t *testing.T) {
@@ -128,7 +130,7 @@ func TestCreateEducationSchool(t *testing.T) {
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=openCloudEducationSchool)(openCloudEducationSchoolNumber=0123))",
Attributes: []string{"ou", "openCloudUUID", "openCloudEducationSchoolNumber", "openCloudEducationSchoolTerminationTimestamp"},
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
lm.On("Search", schoolNumberSearchRequest).
@@ -142,7 +144,7 @@ func TestCreateEducationSchool(t *testing.T) {
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=openCloudEducationSchool)(openCloudEducationSchoolNumber=0666))",
Attributes: []string{"ou", "openCloudUUID", "openCloudEducationSchoolNumber", "openCloudEducationSchoolTerminationTimestamp"},
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
lm.On("Search", existingSchoolNumberSearchRequest).
@@ -156,7 +158,7 @@ func TestCreateEducationSchool(t *testing.T) {
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=openCloudEducationSchool)(openCloudEducationSchoolNumber=1111))",
Attributes: []string{"ou", "openCloudUUID", "openCloudEducationSchoolNumber", "openCloudEducationSchoolTerminationTimestamp"},
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
lm.On("Search", schoolNumberSearchRequestError).
@@ -170,7 +172,7 @@ func TestCreateEducationSchool(t *testing.T) {
Scope: 0,
SizeLimit: 1,
Filter: "(objectClass=openCloudEducationSchool)",
Attributes: []string{"ou", "openCloudUUID", "openCloudEducationSchoolNumber", "openCloudEducationSchoolTerminationTimestamp"},
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
lm.On("Search", schoolLookupAfterCreate).
@@ -358,7 +360,7 @@ func TestDeleteEducationSchool(t *testing.T) {
Scope: 2,
SizeLimit: 1,
Filter: tt.filter,
Attributes: []string{"ou", "openCloudUUID", "openCloudEducationSchoolNumber", "openCloudEducationSchoolTerminationTimestamp"},
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
if tt.expectedItemNotFound {
@@ -427,7 +429,7 @@ func TestGetEducationSchool(t *testing.T) {
Scope: 2,
SizeLimit: 1,
Filter: tt.filter,
Attributes: []string{"ou", "openCloudUUID", "openCloudEducationSchoolNumber", "openCloudEducationSchoolTerminationTimestamp"},
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
if tt.expectedItemNotFound {
@@ -461,7 +463,7 @@ func TestGetEducationSchools(t *testing.T) {
Scope: 2,
SizeLimit: 0,
Filter: "(objectClass=openCloudEducationSchool)",
Attributes: []string{"ou", "openCloudUUID", "openCloudEducationSchoolNumber", "openCloudEducationSchoolTerminationTimestamp"},
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry, schoolEntry1}}, nil)
@@ -478,7 +480,7 @@ var schoolByIDSearch1 *ldap.SearchRequest = &ldap.SearchRequest{
Scope: 2,
SizeLimit: 1,
Filter: filterSchoolSearchByIdExisting,
Attributes: []string{"ou", "openCloudUUID", "openCloudEducationSchoolNumber", "openCloudEducationSchoolTerminationTimestamp"},
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
@@ -487,7 +489,7 @@ var schoolByNumberSearch *ldap.SearchRequest = &ldap.SearchRequest{
Scope: 2,
SizeLimit: 1,
Filter: filterSchoolSearchByNumberExisting,
Attributes: []string{"ou", "openCloudUUID", "openCloudEducationSchoolNumber", "openCloudEducationSchoolTerminationTimestamp"},
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}

View File

@@ -12,11 +12,13 @@ import (
type educationUserAttributeMap struct {
primaryRole string
externalID string
}
func newEducationUserAttributeMap() educationUserAttributeMap {
return educationUserAttributeMap{
primaryRole: "userClass",
externalID: "openCloudEducationExternalId",
}
}
@@ -33,7 +35,7 @@ func (i *LDAP) CreateEducationUser(ctx context.Context, user libregraph.Educatio
return nil, err
}
if err := i.conn.Add(ar); err != nil {
if err = i.conn.Add(ar); err != nil {
var lerr *ldap.Error
logger.Debug().Err(err).Msg("error adding user")
if errors.As(err, &lerr) {
@@ -118,6 +120,7 @@ func (i *LDAP) UpdateEducationUser(ctx context.Context, nameOrID string, user li
i.userAttributeMap.givenName: user.GetGivenName(),
i.userAttributeMap.userType: user.GetUserType(),
i.educationConfig.userAttributeMap.primaryRole: user.GetPrimaryRole(),
i.educationConfig.userAttributeMap.externalID: user.GetExternalId(),
}
for attribute, value := range properties {
@@ -205,39 +208,63 @@ func (i *LDAP) GetEducationUser(ctx context.Context, nameOrID string) (*libregra
// GetEducationUsers implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetEducationUsers")
var userFilter string
var filter string
if i.userFilter == "" {
userFilter = fmt.Sprintf("(objectClass=%s)", i.educationConfig.userObjectClass)
filter = fmt.Sprintf("(objectClass=%s)", i.educationConfig.userObjectClass)
} else {
userFilter = fmt.Sprintf("(&%s(objectClass=%s))", i.userFilter, i.educationConfig.userObjectClass)
filter = fmt.Sprintf("(&%s(objectClass=%s))", i.userFilter, i.educationConfig.userObjectClass)
}
return i.searchEducationUsers(ctx, filter)
}
func (i *LDAP) FilterEducationUsersByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationUser, error) {
logger := i.logger.SubloggerWithRequestID(ctx).With().Str("func", "FilterEducationUsersByAttribute").Logger()
logger.Debug().Str("backend", "ldap").Str("attribute", attr).Str("value", value).Msg("")
var ldapAttr string
switch attr {
case "displayname":
ldapAttr = i.userAttributeMap.displayName
case "mail":
ldapAttr = i.userAttributeMap.mail
case "userType":
ldapAttr = i.userAttributeMap.userType
case "primaryRole":
ldapAttr = i.educationConfig.userAttributeMap.primaryRole
case "externalId":
ldapAttr = i.educationConfig.userAttributeMap.externalID
default:
return nil, errorcode.New(errorcode.InvalidRequest, fmt.Sprintf("filtering by attribute '%s' is not supported", attr))
}
filter := fmt.Sprintf("(&%s(objectClass=%s)(%s=%s))", i.userFilter, i.educationConfig.userObjectClass, ldap.EscapeFilter(ldapAttr), ldap.EscapeFilter(value))
return i.searchEducationUsers(ctx, filter)
}
// searchEducationUsers builds and executes an LDAP search for education users and converts the results to EducationUser models.
func (i *LDAP) searchEducationUsers(ctx context.Context, filter string) ([]*libregraph.EducationUser, error) {
searchRequest := ldap.NewSearchRequest(
i.userBaseDN,
i.userScope,
ldap.NeverDerefAliases, 0, 0, false,
userFilter,
filter,
i.getEducationUserAttrTypes(),
nil,
)
logger := i.logger.SubloggerWithRequestID(ctx)
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("GetEducationUsers")
Msg("searchEducationUsers")
res, err := i.conn.Search(searchRequest)
if err != nil {
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
}
users := make([]*libregraph.EducationUser, 0, len(res.Entries))
for _, e := range res.Entries {
u := i.createEducationUserModelFromLDAP(e)
// Skip invalid LDAP users
@@ -277,6 +304,10 @@ func (i *LDAP) userToEducationUser(user libregraph.User, e *ldap.Entry) *libregr
if primaryRole := e.GetEqualFoldAttributeValue(i.educationConfig.userAttributeMap.primaryRole); primaryRole != "" {
eduUser.SetPrimaryRole(primaryRole)
}
if externalID := e.GetEqualFoldAttributeValue(i.educationConfig.userAttributeMap.externalID); externalID != "" {
eduUser.SetExternalId(externalID)
}
}
return eduUser
@@ -286,6 +317,10 @@ func (i *LDAP) educationUserToLDAPAttrValues(user libregraph.EducationUser, attr
if role, ok := user.GetPrimaryRoleOk(); ok {
attrs[i.educationConfig.userAttributeMap.primaryRole] = []string{*role}
}
if externalID, ok := user.GetExternalIdOk(); ok {
attrs[i.educationConfig.userAttributeMap.externalID] = []string{*externalID}
}
attrs["objectClass"] = append(attrs["objectClass"], i.educationConfig.userObjectClass)
return attrs, nil
}
@@ -326,6 +361,7 @@ func (i *LDAP) getEducationUserAttrTypes() []string {
i.userAttributeMap.userType,
i.userAttributeMap.identities,
i.educationConfig.userAttributeMap.primaryRole,
i.educationConfig.userAttributeMap.externalID,
i.educationConfig.memberOfSchoolAttribute,
}
}

View File

@@ -5,12 +5,14 @@ import (
"testing"
"github.com/go-ldap/ldap/v3"
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
const peopleBaseDN = "ou=people,dc=test"
var eduUserAttrs = []string{
"displayname",
"entryUUID",
@@ -22,6 +24,7 @@ var eduUserAttrs = []string{
"userTypeAttribute",
"openCloudExternalIdentity",
"userClass",
"openCloudEducationExternalId",
"openCloudMemberOfSchool",
}
@@ -68,7 +71,7 @@ var eduUserEntryWithSchool = ldap.NewEntry("uid=user,ou=people,dc=test",
})
var sr1 *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: "ou=people,dc=test",
BaseDN: peopleBaseDN,
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=openCloudEducationUser)(|(uid=abcd-defg)(entryUUID=abcd-defg)))",
@@ -76,7 +79,7 @@ var sr1 *ldap.SearchRequest = &ldap.SearchRequest{
Controls: []ldap.Control(nil),
}
var sr2 *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: "ou=people,dc=test",
BaseDN: peopleBaseDN,
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=openCloudEducationUser)(|(uid=xxxx-xxxx)(entryUUID=xxxx-xxxx)))",
@@ -163,7 +166,7 @@ func TestGetEducationUser(t *testing.T) {
func TestGetEducationUsers(t *testing.T) {
lm := &mocks.Client{}
sr := &ldap.SearchRequest{
BaseDN: "ou=people,dc=test",
BaseDN: peopleBaseDN,
Scope: 2,
SizeLimit: 0,
Filter: "(objectClass=openCloudEducationUser)",
@@ -178,12 +181,30 @@ func TestGetEducationUsers(t *testing.T) {
assert.Nil(t, err)
}
func TestFilterEducationUsersByAttr(t *testing.T) {
lm := &mocks.Client{}
sr := &ldap.SearchRequest{
BaseDN: peopleBaseDN,
Scope: 2,
SizeLimit: 0,
Filter: "(&(objectClass=openCloudEducationUser)(openCloudEducationExternalId=id1234))",
Attributes: eduUserAttrs,
Controls: []ldap.Control(nil),
}
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
_, err = b.FilterEducationUsersByAttribute(context.Background(), "externalId", "id1234")
lm.AssertNumberOfCalls(t, "Search", 1)
assert.Nil(t, err)
}
func TestUpdateEducationUser(t *testing.T) {
lm := &mocks.Client{}
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
userSearchReq := &ldap.SearchRequest{
BaseDN: "ou=people,dc=test",
BaseDN: peopleBaseDN,
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=openCloudEducationUser)(|(uid=testuser)(entryUUID=testuser)))",

View File

@@ -602,6 +602,154 @@ func (_c *EducationBackend_DeleteEducationUser_Call) RunAndReturn(run func(ctx c
return _c
}
// FilterEducationSchoolsByAttribute provides a mock function for the type EducationBackend
func (_mock *EducationBackend) FilterEducationSchoolsByAttribute(ctx context.Context, attr string, value string) ([]*libregraph.EducationSchool, error) {
ret := _mock.Called(ctx, attr, value)
if len(ret) == 0 {
panic("no return value specified for FilterEducationSchoolsByAttribute")
}
var r0 []*libregraph.EducationSchool
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) ([]*libregraph.EducationSchool, error)); ok {
return returnFunc(ctx, attr, value)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) []*libregraph.EducationSchool); ok {
r0 = returnFunc(ctx, attr, value)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*libregraph.EducationSchool)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = returnFunc(ctx, attr, value)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// EducationBackend_FilterEducationSchoolsByAttribute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FilterEducationSchoolsByAttribute'
type EducationBackend_FilterEducationSchoolsByAttribute_Call struct {
*mock.Call
}
// FilterEducationSchoolsByAttribute is a helper method to define mock.On call
// - ctx context.Context
// - attr string
// - value string
func (_e *EducationBackend_Expecter) FilterEducationSchoolsByAttribute(ctx interface{}, attr interface{}, value interface{}) *EducationBackend_FilterEducationSchoolsByAttribute_Call {
return &EducationBackend_FilterEducationSchoolsByAttribute_Call{Call: _e.mock.On("FilterEducationSchoolsByAttribute", ctx, attr, value)}
}
func (_c *EducationBackend_FilterEducationSchoolsByAttribute_Call) Run(run func(ctx context.Context, attr string, value string)) *EducationBackend_FilterEducationSchoolsByAttribute_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *EducationBackend_FilterEducationSchoolsByAttribute_Call) Return(educationSchools []*libregraph.EducationSchool, err error) *EducationBackend_FilterEducationSchoolsByAttribute_Call {
_c.Call.Return(educationSchools, err)
return _c
}
func (_c *EducationBackend_FilterEducationSchoolsByAttribute_Call) RunAndReturn(run func(ctx context.Context, attr string, value string) ([]*libregraph.EducationSchool, error)) *EducationBackend_FilterEducationSchoolsByAttribute_Call {
_c.Call.Return(run)
return _c
}
// FilterEducationUsersByAttribute provides a mock function for the type EducationBackend
func (_mock *EducationBackend) FilterEducationUsersByAttribute(ctx context.Context, attr string, value string) ([]*libregraph.EducationUser, error) {
ret := _mock.Called(ctx, attr, value)
if len(ret) == 0 {
panic("no return value specified for FilterEducationUsersByAttribute")
}
var r0 []*libregraph.EducationUser
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) ([]*libregraph.EducationUser, error)); ok {
return returnFunc(ctx, attr, value)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) []*libregraph.EducationUser); ok {
r0 = returnFunc(ctx, attr, value)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*libregraph.EducationUser)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = returnFunc(ctx, attr, value)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// EducationBackend_FilterEducationUsersByAttribute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FilterEducationUsersByAttribute'
type EducationBackend_FilterEducationUsersByAttribute_Call struct {
*mock.Call
}
// FilterEducationUsersByAttribute is a helper method to define mock.On call
// - ctx context.Context
// - attr string
// - value string
func (_e *EducationBackend_Expecter) FilterEducationUsersByAttribute(ctx interface{}, attr interface{}, value interface{}) *EducationBackend_FilterEducationUsersByAttribute_Call {
return &EducationBackend_FilterEducationUsersByAttribute_Call{Call: _e.mock.On("FilterEducationUsersByAttribute", ctx, attr, value)}
}
func (_c *EducationBackend_FilterEducationUsersByAttribute_Call) Run(run func(ctx context.Context, attr string, value string)) *EducationBackend_FilterEducationUsersByAttribute_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *EducationBackend_FilterEducationUsersByAttribute_Call) Return(educationUsers []*libregraph.EducationUser, err error) *EducationBackend_FilterEducationUsersByAttribute_Call {
_c.Call.Return(educationUsers, err)
return _c
}
func (_c *EducationBackend_FilterEducationUsersByAttribute_Call) RunAndReturn(run func(ctx context.Context, attr string, value string) ([]*libregraph.EducationUser, error)) *EducationBackend_FilterEducationUsersByAttribute_Call {
_c.Call.Return(run)
return _c
}
// GetEducationClass provides a mock function for the type EducationBackend
func (_mock *EducationBackend) GetEducationClass(ctx context.Context, namedOrID string) (*libregraph.EducationClass, error) {
ret := _mock.Called(ctx, namedOrID)

View File

@@ -1,6 +1,7 @@
package svc
import (
"context"
"errors"
"fmt"
"net/http"
@@ -30,10 +31,10 @@ func (g Graph) GetEducationSchools(w http.ResponseWriter, r *http.Request) {
return
}
schools, err := g.identityEducationBackend.GetEducationSchools(r.Context())
schools, err := g.getEducationSchoolsFromBackend(r.Context(), odataReq)
if err != nil {
logger.Debug().Err(err).Msg("could not get schools: backend error")
errorcode.RenderError(w, r, err)
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get schools")
renderEqualityFilterError(w, r, err)
return
}
@@ -616,3 +617,15 @@ func sortEducationSchools(req *godata.GoDataRequest, schools []*libregraph.Educa
return schools, nil
}
// getEducationSchoolsFromBackend fetches schools from the backend, applying an OData $filter if present.
func (g Graph) getEducationSchoolsFromBackend(ctx context.Context, odataReq *godata.GoDataRequest) ([]*libregraph.EducationSchool, error) {
if odataReq.Query.Filter != nil {
attr, value, err := g.getEqualityFilter(ctx, odataReq)
if err != nil {
return nil, err
}
return g.identityEducationBackend.FilterEducationSchoolsByAttribute(ctx, attr, value)
}
return g.identityEducationBackend.GetEducationSchools(ctx)
}

View File

@@ -8,6 +8,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
@@ -176,6 +177,18 @@ var _ = Describe("Schools", func() {
Expect(len(res.Value)).To(Equal(1))
Expect(res.Value[0].GetId()).To(Equal("school1"))
})
When("used with a filter", func() {
It("fails with an unsupported filter", func() {
svc.GetEducationSchools(rr, httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools?$filter="+url.QueryEscape("displayName ne 'test'"), nil))
Expect(rr.Code).To(Equal(http.StatusNotImplemented))
})
It("calls the backend with the externalId filter", func() {
identityEducationBackend.On("FilterEducationSchoolsByAttribute", mock.Anything, "externalId", "ext1234").Return([]*libregraph.EducationSchool{newSchool}, nil)
svc.GetEducationSchools(rr, httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools?$filter="+url.QueryEscape("externalId eq 'ext1234'"), nil))
Expect(rr.Code).To(Equal(http.StatusOK))
})
})
})
Describe("GetEducationSchool", func() {

View File

@@ -1,6 +1,8 @@
package svc
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
@@ -21,8 +23,7 @@ import (
// GetEducationUsers implements the Service interface.
func (g Graph) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Interface("query", r.URL.Query()).Msg("calling get education users")
logger := g.logger.SubloggerWithRequestID(r.Context()).With().Str("func", "GetEducationUsers").Logger()
sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
if err != nil {
@@ -31,14 +32,12 @@ func (g Graph) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
return
}
logger.Debug().Interface("query", r.URL.Query()).Msg("calling get education users on backend")
users, err := g.identityEducationBackend.GetEducationUsers(r.Context())
users, err := g.getEducationUsersFromBackend(r.Context(), odataReq)
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get education users from backend")
errorcode.RenderError(w, r, err)
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get education users")
renderEqualityFilterError(w, r, err)
return
}
users, err = sortEducationUsers(odataReq, users)
if err != nil {
logger.Debug().Interface("query", odataReq).Msg("error while sorting education users according to query")
@@ -387,3 +386,56 @@ func sortEducationUsers(req *godata.GoDataRequest, users []*libregraph.Education
}
return users, nil
}
// getEducationUsersFromBackend fetches users from the backend, applying an OData $filter if present.
func (g Graph) getEducationUsersFromBackend(ctx context.Context, odataReq *godata.GoDataRequest) ([]*libregraph.EducationUser, error) {
if odataReq.Query.Filter != nil {
attr, value, err := g.getEqualityFilter(ctx, odataReq)
if err != nil {
return nil, err
}
return g.identityEducationBackend.FilterEducationUsersByAttribute(ctx, attr, value)
}
return g.identityEducationBackend.GetEducationUsers(ctx)
}
func (g Graph) getEqualityFilter(ctx context.Context, req *godata.GoDataRequest) (string, string, error) {
logger := g.logger.SubloggerWithRequestID(ctx)
root := req.Query.Filter.Tree
if root.Token.Type != godata.ExpressionTokenLogical {
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg(unsupportedFilter)
return "", "", unsupportedFilterError()
}
if root.Token.Value != "eq" {
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg(unsupportedFilter)
return "", "", unsupportedFilterError()
}
if len(root.Children) != 2 {
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg(unsupportedFilter)
return "", "", unsupportedFilterError()
}
if root.Children[0].Token.Type != godata.ExpressionTokenLiteral || root.Children[1].Token.Type != godata.ExpressionTokenString {
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg(unsupportedFilter)
return "", "", unsupportedFilterError()
}
// unquote
value := strings.Trim(root.Children[1].Token.Value, "'")
return root.Children[0].Token.Value, value, nil
}
// renderEqualityFilterError writes the appropriate HTTP error response for errors returned by getEqualityFilter.
func renderEqualityFilterError(w http.ResponseWriter, r *http.Request, err error) {
var errcode errorcode.Error
var godataerr *godata.GoDataError
switch {
case errors.As(err, &errcode):
errcode.Render(w, r)
case errors.As(err, &godataerr):
errorcode.GeneralException.Render(w, r, godataerr.ResponseCode, err.Error())
default:
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
}

View File

@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/url"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
@@ -202,6 +203,17 @@ var _ = Describe("EducationUsers", func() {
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
When("used with a filter", func() {
It("fails with an unsupported filter ", func() {
svc.GetEducationUsers(rr, httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users?$filter="+url.QueryEscape("displayName ne 'test'"), nil))
Expect(rr.Code).To(Equal(http.StatusNotImplemented))
})
It("calls the backend with the filter", func() {
identityEducationBackend.On("FilterEducationUsersByAttribute", mock.Anything, "externalId", "id1234").Return([]*libregraph.EducationUser{}, nil)
svc.GetEducationUsers(rr, httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users?$filter="+url.QueryEscape("externalId eq 'id1234'"), nil))
Expect(rr.Code).To(Equal(http.StatusOK))
})
})
})
Describe("GetEducationUser", func() {

View File

@@ -5,14 +5,15 @@ import (
"strings"
"github.com/CiscoM31/godata"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
settingsmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/settings/v0"
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
const (
appRoleID = "appRoleId"
appRoleAssignments = "appRoleAssignments"
unsupportedFilter = "unsupported filter"
)
func invalidFilterError() error {
@@ -20,7 +21,7 @@ func invalidFilterError() error {
}
func unsupportedFilterError() error {
return godata.NotImplementedError("unsupported filter")
return godata.NotImplementedError(unsupportedFilter)
}
func (g Graph) applyUserFilter(ctx context.Context, req *godata.GoDataRequest, root *godata.ParseNode) (users []*libregraph.User, err error) {
@@ -38,7 +39,7 @@ func (g Graph) applyUserFilter(ctx context.Context, req *godata.GoDataRequest, r
case godata.ExpressionTokenFunc:
return g.applyFilterFunction(ctx, req, root)
}
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg("filter is not supported")
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg(unsupportedFilter)
return users, unsupportedFilterError()
}