Skip to content

Commit 8b25a1d

Browse files
authored
Merge pull request #2421 from rhafer/issue/1599
feat(graph/education): Add support of 'eq' filters on users
2 parents 1c5ae34 + fe3befd commit 8b25a1d

13 files changed

Lines changed: 429 additions & 127 deletions

devtools/deployments/multi-tenancy/initialize_users.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ var demoTenants = []tenantWithUsers{
3131
DisplayName: libregraph.PtrString("Dennis Ritchie"),
3232
OnPremisesSamAccountName: libregraph.PtrString("dennis"),
3333
Mail: libregraph.PtrString("dennis@example.org"),
34+
ExternalId: libregraph.PtrString("ExternalID1"),
3435
},
3536
{
3637
DisplayName: libregraph.PtrString("Grace Hopper"),
3738
OnPremisesSamAccountName: libregraph.PtrString("grace"),
3839
Mail: libregraph.PtrString("grace@example.org"),
40+
ExternalId: libregraph.PtrString("ExternalID2"),
3941
},
4042
},
4143
},
@@ -49,11 +51,13 @@ var demoTenants = []tenantWithUsers{
4951
DisplayName: libregraph.PtrString("Albert Einstein"),
5052
OnPremisesSamAccountName: libregraph.PtrString("einstein"),
5153
Mail: libregraph.PtrString("einstein@example.org"),
54+
ExternalId: libregraph.PtrString("ExternalID3"),
5255
},
5356
{
5457
DisplayName: libregraph.PtrString("Marie Curie"),
5558
OnPremisesSamAccountName: libregraph.PtrString("marie"),
5659
Mail: libregraph.PtrString("marie@example.org"),
60+
ExternalId: libregraph.PtrString("ExternalID4"),
5761
},
5862
},
5963
},

services/graph/pkg/identity/backend.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ type EducationBackend interface {
6868
GetEducationSchool(ctx context.Context, nameOrID string) (*libregraph.EducationSchool, error)
6969
// GetEducationSchools lists all schools
7070
GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error)
71+
// FilterEducationSchoolsByAttribute list all schools where an attribute matches a value, e.g. all schools with a given externalId
72+
FilterEducationSchoolsByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationSchool, error)
7173
// UpdateEducationSchool updates attributes of a school
7274
UpdateEducationSchool(ctx context.Context, numberOrID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error)
7375
// GetEducationSchoolUsers lists all members of a school
@@ -107,6 +109,8 @@ type EducationBackend interface {
107109
GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error)
108110
// GetEducationUsers lists all education users
109111
GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error)
112+
// FilterEducationUsersByAttribute list all education users where and attribute matches a value, e.g. all users with a given externalid
113+
FilterEducationUsersByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationUser, error)
110114

111115
// GetEducationClassTeachers returns the EducationUser teachers for an EducationClass
112116
GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error)

services/graph/pkg/identity/err_education.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ func (i *ErrEducationBackend) GetEducationSchools(ctx context.Context) ([]*libre
2929
return nil, errNotImplemented
3030
}
3131

32+
// FilterEducationSchoolsByAttribute implements the EducationBackend interface for the ErrEducationBackend backend.
33+
func (i *ErrEducationBackend) FilterEducationSchoolsByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationSchool, error) {
34+
return nil, errNotImplemented
35+
}
36+
3237
// UpdateEducationSchool implements the EducationBackend interface for the ErrEducationBackend backend.
3338
func (i *ErrEducationBackend) UpdateEducationSchool(ctx context.Context, numberOrID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
3439
return nil, errNotImplemented
@@ -119,6 +124,11 @@ func (i *ErrEducationBackend) GetEducationUsers(ctx context.Context) ([]*libregr
119124
return nil, errNotImplemented
120125
}
121126

127+
// FilterEducationUsersByAttribute implements the EducationBackend interface for the ErrEducationBackend backend.
128+
func (i *ErrEducationBackend) FilterEducationUsersByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationUser, error) {
129+
return nil, errNotImplemented
130+
}
131+
122132
// GetEducationClassTeachers implements the EducationBackend interface for the ErrEducationBackend backend.
123133
func (i *ErrEducationBackend) GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) {
124134
return nil, errNotImplemented

services/graph/pkg/identity/ldap_education_school.go

Lines changed: 71 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"slices"
78
"time"
89

910
"github.com/go-ldap/ldap/v3"
@@ -49,10 +50,9 @@ const (
4950
)
5051

5152
var (
52-
errNotSet = errors.New("attribute not set")
53-
errSchoolNameExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that name is already present")
54-
errSchoolNumberExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that number is already present")
55-
errSchoolExternalIdExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that external id is already present")
53+
errNotSet = errors.New("attribute not set")
54+
errSchoolNameExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that name is already present")
55+
errSchoolNumberExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that number is already present")
5656
)
5757

5858
func defaultEducationConfig() educationConfig {
@@ -136,21 +136,6 @@ func (i *LDAP) CreateEducationSchool(ctx context.Context, school libregraph.Educ
136136
}
137137
}
138138

139-
// Check that the school external id is not already used
140-
if school.HasExternalId() {
141-
_, err := i.getSchoolByExternalId(school.GetExternalId())
142-
switch {
143-
case err == nil:
144-
logger.Debug().Err(errSchoolExternalIdExists).Str("externalId", school.GetExternalId()).Msg("duplicate school external id")
145-
return nil, errSchoolExternalIdExists
146-
case errors.Is(err, ErrNotFound):
147-
break
148-
default:
149-
logger.Error().Err(err).Str("externalId", school.GetExternalId()).Msg("error looking up school by external id")
150-
return nil, errorcode.New(errorcode.GeneralException, "error looking up school by external id")
151-
}
152-
}
153-
154139
attributeTypeAndValue := ldap.AttributeTypeAndValue{
155140
Type: i.educationConfig.schoolAttributeMap.displayName,
156141
Value: school.GetDisplayName(),
@@ -299,14 +284,14 @@ func (i *LDAP) updateSchoolProperties(ctx context.Context, dn string, currentSch
299284
}
300285

301286
// UpdateEducationSchool updates the supplied school in the identity backend
302-
func (i *LDAP) UpdateEducationSchool(ctx context.Context, numberOrIDOrExternalID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
287+
func (i *LDAP) UpdateEducationSchool(ctx context.Context, numberOrID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
303288
logger := i.logger.SubloggerWithRequestID(ctx)
304289
logger.Debug().Str("backend", "ldap").Msg("UpdateEducationSchool")
305290
if !i.writeEnabled {
306291
return nil, ErrReadOnly
307292
}
308293

309-
e, err := i.getSchoolByNumberOrIDOrExternalID(numberOrIDOrExternalID)
294+
e, err := i.getSchoolByNumberOrID(numberOrID)
310295
if err != nil {
311296
return nil, err
312297
}
@@ -329,7 +314,7 @@ func (i *LDAP) UpdateEducationSchool(ctx context.Context, numberOrIDOrExternalID
329314
}
330315

331316
// Read back school from LDAP
332-
e, err = i.getSchoolByNumberOrIDOrExternalID(i.getID(e))
317+
e, err = i.getSchoolByNumberOrID(i.getID(e))
333318
if err != nil {
334319
return nil, err
335320
}
@@ -343,7 +328,7 @@ func (i *LDAP) DeleteEducationSchool(ctx context.Context, id string) error {
343328
if !i.writeEnabled {
344329
return ErrReadOnly
345330
}
346-
e, err := i.getSchoolByNumberOrIDOrExternalID(id)
331+
e, err := i.getSchoolByNumberOrID(id)
347332
if err != nil {
348333
return err
349334
}
@@ -358,10 +343,10 @@ func (i *LDAP) DeleteEducationSchool(ctx context.Context, id string) error {
358343
}
359344

360345
// GetEducationSchool implements the EducationBackend interface for the LDAP backend.
361-
func (i *LDAP) GetEducationSchool(ctx context.Context, numberOrIDOrExternalID string) (*libregraph.EducationSchool, error) {
346+
func (i *LDAP) GetEducationSchool(ctx context.Context, numberOrID string) (*libregraph.EducationSchool, error) {
362347
logger := i.logger.SubloggerWithRequestID(ctx)
363348
logger.Debug().Str("backend", "ldap").Msg("GetEducationSchool")
364-
e, err := i.getSchoolByNumberOrIDOrExternalID(numberOrIDOrExternalID)
349+
e, err := i.getSchoolByNumberOrID(numberOrID)
365350
if err != nil {
366351
return nil, err
367352
}
@@ -371,13 +356,36 @@ func (i *LDAP) GetEducationSchool(ctx context.Context, numberOrIDOrExternalID st
371356

372357
// GetEducationSchools implements the EducationBackend interface for the LDAP backend.
373358
func (i *LDAP) GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error) {
374-
var filter string
375-
filter = fmt.Sprintf("(objectClass=%s)", i.educationConfig.schoolObjectClass)
376-
359+
filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.schoolObjectClass)
377360
if i.educationConfig.schoolFilter != "" {
378361
filter = fmt.Sprintf("(&%s%s)", i.educationConfig.schoolFilter, filter)
379362
}
363+
return i.searchEducationSchools(ctx, filter)
364+
}
365+
366+
// FilterEducationSchoolsByAttribute implements the EducationBackend interface for the LDAP backend.
367+
func (i *LDAP) FilterEducationSchoolsByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationSchool, error) {
368+
logger := i.logger.SubloggerWithRequestID(ctx).With().Str("func", "FilterEducationSchoolsByAttribute").Logger()
369+
logger.Debug().Str("backend", "ldap").Str("attribute", attr).Str("value", value).Msg("")
370+
371+
var ldapAttr string
372+
switch attr {
373+
case "externalId":
374+
ldapAttr = i.educationConfig.schoolAttributeMap.externalId
375+
default:
376+
return nil, errorcode.New(errorcode.InvalidRequest, fmt.Sprintf("filtering by attribute '%s' is not supported", attr))
377+
}
378+
filter := fmt.Sprintf("(&%s(objectClass=%s)(%s=%s))",
379+
i.educationConfig.schoolFilter,
380+
i.educationConfig.schoolObjectClass,
381+
ldap.EscapeFilter(ldapAttr),
382+
ldap.EscapeFilter(value),
383+
)
384+
return i.searchEducationSchools(ctx, filter)
385+
}
380386

387+
// searchEducationSchools builds and executes an LDAP search for education schools and converts the results to EducationSchool models.
388+
func (i *LDAP) searchEducationSchools(ctx context.Context, filter string) ([]*libregraph.EducationSchool, error) {
381389
searchRequest := ldap.NewSearchRequest(
382390
i.educationConfig.schoolBaseDN,
383391
i.educationConfig.schoolScope,
@@ -386,13 +394,15 @@ func (i *LDAP) GetEducationSchools(ctx context.Context) ([]*libregraph.Education
386394
i.getEducationSchoolAttrTypes(),
387395
nil,
388396
)
389-
i.logger.Debug().Str("backend", "ldap").
397+
logger := i.logger.SubloggerWithRequestID(ctx)
398+
logger.Debug().Str("backend", "ldap").
390399
Str("base", searchRequest.BaseDN).
391400
Str("filter", searchRequest.Filter).
392401
Int("scope", searchRequest.Scope).
393402
Int("sizelimit", searchRequest.SizeLimit).
394403
Interface("attributes", searchRequest.Attributes).
395-
Msg("GetEducationSchools")
404+
Msg("searchEducationSchools")
405+
396406
res, err := i.conn.Search(searchRequest)
397407
if err != nil {
398408
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
@@ -436,11 +446,11 @@ func (i *LDAP) GetEducationSchoolUsers(ctx context.Context, schoolNumberOrID str
436446
}
437447

438448
// AddUsersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
439-
func (i *LDAP) AddUsersToEducationSchool(ctx context.Context, schoolNumberOrIDOrExternalID string, memberIDs []string) error {
449+
func (i *LDAP) AddUsersToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error {
440450
logger := i.logger.SubloggerWithRequestID(ctx)
441451
logger.Debug().Str("backend", "ldap").Msg("AddUsersToEducationSchool")
442452

443-
schoolEntry, err := i.getSchoolByNumberOrIDOrExternalID(schoolNumberOrIDOrExternalID)
453+
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
444454
if err != nil {
445455
return err
446456
}
@@ -462,32 +472,31 @@ func (i *LDAP) AddUsersToEducationSchool(ctx context.Context, schoolNumberOrIDOr
462472
}
463473

464474
for _, userEntry := range userEntries {
465-
currentSchools := userEntry.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
466-
found := false
467-
for _, currentSchool := range currentSchools {
468-
if currentSchool == schoolID {
469-
found = true
470-
break
471-
}
472-
}
473-
if !found {
474-
mr := ldap.ModifyRequest{DN: userEntry.DN}
475-
mr.Add(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
476-
if err := i.conn.Modify(&mr); err != nil {
477-
return err
478-
}
475+
if err := i.addEntryToSchool(userEntry, schoolID); err != nil {
476+
return err
479477
}
480478
}
481479

482480
return nil
483481
}
484482

483+
// addEntryToSchool adds the schoolID to the entry's memberOfSchool attribute if not already present.
484+
func (i *LDAP) addEntryToSchool(entry *ldap.Entry, schoolID string) error {
485+
currentSchools := entry.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
486+
if slices.Contains(currentSchools, schoolID) {
487+
return nil
488+
}
489+
mr := ldap.ModifyRequest{DN: entry.DN}
490+
mr.Add(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
491+
return i.conn.Modify(&mr)
492+
}
493+
485494
// RemoveUserFromEducationSchool removes a single member (by ID) from a school
486-
func (i *LDAP) RemoveUserFromEducationSchool(ctx context.Context, schoolNumberOrIDOrExternalID string, memberID string) error {
495+
func (i *LDAP) RemoveUserFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error {
487496
logger := i.logger.SubloggerWithRequestID(ctx)
488497
logger.Debug().Str("backend", "ldap").Msg("RemoveUserFromEducationSchool")
489498

490-
schoolEntry, err := i.getSchoolByNumberOrIDOrExternalID(schoolNumberOrIDOrExternalID)
499+
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
491500
if err != nil {
492501
return err
493502
}
@@ -542,12 +551,12 @@ func (i *LDAP) GetEducationSchoolClasses(ctx context.Context, schoolNumberOrID s
542551
}
543552

544553
func (i *LDAP) getEducationSchoolEntries(
545-
schoolNumberOrIDOrExternalID, filter, objectClass, baseDN string,
554+
schoolNumberOrID, filter, objectClass, baseDN string,
546555
scope int,
547556
attributes []string,
548557
logger log.Logger,
549558
) ([]*ldap.Entry, error) {
550-
schoolEntry, err := i.getSchoolByNumberOrIDOrExternalID(schoolNumberOrIDOrExternalID)
559+
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
551560
if err != nil {
552561
return nil, err
553562
}
@@ -584,11 +593,11 @@ func (i *LDAP) getEducationSchoolEntries(
584593
}
585594

586595
// AddClassesToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
587-
func (i *LDAP) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrIDOrExternalID string, memberIDs []string) error {
596+
func (i *LDAP) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error {
588597
logger := i.logger.SubloggerWithRequestID(ctx)
589598
logger.Debug().Str("backend", "ldap").Msg("AddClassesToEducationSchool")
590599

591-
schoolEntry, err := i.getSchoolByNumberOrIDOrExternalID(schoolNumberOrIDOrExternalID)
600+
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
592601
if err != nil {
593602
return err
594603
}
@@ -610,32 +619,20 @@ func (i *LDAP) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID
610619
}
611620

612621
for _, classEntry := range classEntries {
613-
currentSchools := classEntry.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
614-
found := false
615-
for _, currentSchool := range currentSchools {
616-
if currentSchool == schoolID {
617-
found = true
618-
break
619-
}
620-
}
621-
if !found {
622-
mr := ldap.ModifyRequest{DN: classEntry.DN}
623-
mr.Add(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
624-
if err := i.conn.Modify(&mr); err != nil {
625-
return err
626-
}
622+
if err := i.addEntryToSchool(classEntry, schoolID); err != nil {
623+
return err
627624
}
628625
}
629626

630627
return nil
631628
}
632629

633630
// RemoveClassFromEducationSchool removes a single member (by ID) from a school
634-
func (i *LDAP) RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrIDOrExternalID string, memberID string) error {
631+
func (i *LDAP) RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error {
635632
logger := i.logger.SubloggerWithRequestID(ctx)
636633
logger.Debug().Str("backend", "ldap").Msg("RemoveClassFromEducationSchool")
637634

638-
schoolEntry, err := i.getSchoolByNumberOrIDOrExternalID(schoolNumberOrIDOrExternalID)
635+
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
639636
if err != nil {
640637
return err
641638
}
@@ -673,16 +670,14 @@ func (i *LDAP) getSchoolByDN(dn string) (*ldap.Entry, error) {
673670
return i.getEntryByDN(dn, i.getEducationSchoolAttrTypes(), filter)
674671
}
675672

676-
func (i *LDAP) getSchoolByNumberOrIDOrExternalID(numberOrIDOrExternalID string) (*ldap.Entry, error) {
677-
numberOrIDOrExternalID = ldap.EscapeFilter(numberOrIDOrExternalID)
673+
func (i *LDAP) getSchoolByNumberOrID(numberOrID string) (*ldap.Entry, error) {
674+
numberOrID = ldap.EscapeFilter(numberOrID)
678675
filter := fmt.Sprintf(
679-
"(|(%s=%s)(%s=%s)(%s=%s))",
676+
"(|(%s=%s)(%s=%s))",
680677
i.educationConfig.schoolAttributeMap.id,
681-
numberOrIDOrExternalID,
678+
numberOrID,
682679
i.educationConfig.schoolAttributeMap.schoolNumber,
683-
numberOrIDOrExternalID,
684-
i.educationConfig.schoolAttributeMap.externalId,
685-
numberOrIDOrExternalID,
680+
numberOrID,
686681
)
687682
return i.getSchoolByFilter(filter)
688683
}
@@ -697,16 +692,6 @@ func (i *LDAP) getSchoolByNumber(schoolNumber string) (*ldap.Entry, error) {
697692
return i.getSchoolByFilter(filter)
698693
}
699694

700-
func (i *LDAP) getSchoolByExternalId(schoolExternalId string) (*ldap.Entry, error) {
701-
schoolExternalId = ldap.EscapeFilter(schoolExternalId)
702-
filter := fmt.Sprintf(
703-
"(%s=%s)",
704-
i.educationConfig.schoolAttributeMap.externalId,
705-
schoolExternalId,
706-
)
707-
return i.getSchoolByFilter(filter)
708-
}
709-
710695
func (i *LDAP) getSchoolByFilter(filter string) (*ldap.Entry, error) {
711696
filter = fmt.Sprintf("(&%s(objectClass=%s)%s)",
712697
i.educationConfig.schoolFilter,
@@ -820,6 +805,7 @@ func (i *LDAP) getEducationSchoolAttrTypes() []string {
820805
return []string{
821806
i.educationConfig.schoolAttributeMap.displayName,
822807
i.educationConfig.schoolAttributeMap.id,
808+
i.educationConfig.schoolAttributeMap.externalId,
823809
i.educationConfig.schoolAttributeMap.schoolNumber,
824810
i.educationConfig.schoolAttributeMap.terminationDate,
825811
}

0 commit comments

Comments
 (0)