From 80a2c724912cb2857dbe30f2c30edce22ad16dae Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Thu, 15 Dec 2022 17:27:04 +0100 Subject: [PATCH] graph: Initial LDAP support for /education/users This implements GetEducationUser, GetEducationUsers, DeleteEducationUser and CreateEducationUser methods for the LDAP backend. It's still very basic and no fancy filtering or expanding is there yet. --- services/graph/pkg/identity/ldap.go | 179 +++++++------ .../graph/pkg/identity/ldap_education_user.go | 249 +++++++++++++++++- .../pkg/identity/ldap_education_user_test.go | 132 ++++++++++ services/graph/pkg/identity/ldap_school.go | 6 + 4 files changed, 481 insertions(+), 85 deletions(-) create mode 100644 services/graph/pkg/identity/ldap_education_user_test.go diff --git a/services/graph/pkg/identity/ldap.go b/services/graph/pkg/identity/ldap.go index 2256a46b28..cfa611b830 100644 --- a/services/graph/pkg/identity/ldap.go +++ b/services/graph/pkg/identity/ldap.go @@ -60,6 +60,8 @@ type groupAttributeMap struct { memberSyntax string } +type ldapAttributeValues map[string][]string + func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LDAP, error) { if config.UserDisplayNameAttribute == "" || config.UserIDAttribute == "" || config.UserEmailAttribute == "" || config.UserNameAttribute == "" { @@ -126,54 +128,13 @@ func (i *LDAP) CreateUser(ctx context.Context, user libregraph.User) (*libregrap if !i.writeEnabled { return nil, errReadOnly } - ar := ldap.AddRequest{ - DN: fmt.Sprintf("uid=%s,%s", oldap.EscapeDNAttributeValue(*user.OnPremisesSamAccountName), i.userBaseDN), - Attributes: []ldap.Attribute{ - // inetOrgPerson requires "cn" - { - Type: "cn", - Vals: []string{*user.OnPremisesSamAccountName}, - }, - { - Type: i.userAttributeMap.mail, - Vals: []string{*user.Mail}, - }, - { - Type: i.userAttributeMap.userName, - Vals: []string{*user.OnPremisesSamAccountName}, - }, - { - Type: i.userAttributeMap.displayName, - Vals: []string{*user.DisplayName}, - }, - }, + + ar, err := i.userToAddRequest(user) + if err != nil { + return nil, err } - objectClasses := []string{"inetOrgPerson", "organizationalPerson", "person", "top"} - - if !i.usePwModifyExOp && user.PasswordProfile != nil && user.PasswordProfile.Password != nil { - // Depending on the LDAP server implementation this might cause the - // password to be stored in cleartext in the LDAP database. Using the - // "Password Modify LDAP Extended Operation" is recommended. - ar.Attribute("userPassword", []string{*user.PasswordProfile.Password}) - } - if !i.useServerUUID { - ar.Attribute("owncloudUUID", []string{uuid.Must(uuid.NewV4()).String()}) - objectClasses = append(objectClasses, "owncloud") - } - ar.Attribute("objectClass", objectClasses) - - // inetOrgPerson requires "sn" to be set. Set it to the Username if - // Surname is not set in the Request - var sn string - if user.Surname != nil && *user.Surname != "" { - sn = *user.Surname - } else { - sn = *user.OnPremisesSamAccountName - } - ar.Attribute("sn", []string{sn}) - - 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) { @@ -366,6 +327,40 @@ func (i *LDAP) getEntryByDN(dn string, attrs []string, filter string) (*ldap.Ent return res.Entries[0], nil } +func (i *LDAP) searchLDAPEntryByFilter(basedn string, attrs []string, filter string) (*ldap.Entry, error) { + if filter == "" { + filter = "(objectclass=*)" + } + + searchRequest := ldap.NewSearchRequest( + basedn, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, 1, 0, false, + 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("getEntryByFilter") + res, err := i.conn.Search(searchRequest) + + if err != nil { + i.logger.Error().Err(err).Str("backend", "ldap").Str("dn", basedn).Str("filter", filter).Msg("Search user by filter failed") + return nil, errorcode.New(errorcode.ItemNotFound, err.Error()) + } + if len(res.Entries) == 0 { + return nil, errNotFound + } + + return res.Entries[0], nil +} + func (i *LDAP) getLDAPUserByID(id string) (*ldap.Entry, error) { id = ldap.EscapeFilter(id) filter := fmt.Sprintf("(%s=%s)", i.userAttributeMap.id, id) @@ -379,42 +374,14 @@ func (i *LDAP) getLDAPUserByNameOrID(nameOrID string) (*ldap.Entry, error) { } func (i *LDAP) getLDAPUserByFilter(filter string) (*ldap.Entry, error) { - searchRequest := ldap.NewSearchRequest( - i.userBaseDN, i.userScope, ldap.NeverDerefAliases, 1, 0, false, - fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.userObjectClass, filter), - []string{ - i.userAttributeMap.displayName, - i.userAttributeMap.id, - i.userAttributeMap.mail, - i.userAttributeMap.userName, - }, - 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("getLDAPUserByFilter") - 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 user '%s'", filter) - i.logger.Debug().Str("backend", "ldap").Err(lerr). - Str("userfilter", filter).Msg("too many results searching for user") - } - } - return nil, errorcode.New(errorcode.ItemNotFound, errmsg) + filter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.userObjectClass, filter) + attrs := []string{ + i.userAttributeMap.displayName, + i.userAttributeMap.id, + i.userAttributeMap.mail, + i.userAttributeMap.userName, } - if len(res.Entries) == 0 { - return nil, errNotFound - } - - return res.Entries[0], nil + return i.searchLDAPEntryByFilter(i.userBaseDN, attrs, filter) } func (i *LDAP) GetUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.User, error) { @@ -1010,6 +977,56 @@ func (i *LDAP) groupsFromLDAPEntries(e []*ldap.Entry) []libregraph.Group { return groups } +func (i *LDAP) userToLDAPAttrValues(user libregraph.User) (map[string][]string, error) { + attrs := map[string][]string{ + i.userAttributeMap.displayName: {user.GetDisplayName()}, + i.userAttributeMap.userName: {user.GetOnPremisesSamAccountName()}, + i.userAttributeMap.mail: {user.GetMail()}, + "objectClass": {"inetOrgPerson", "organizationalPerson", "person", "top"}, + "cn": {user.GetOnPremisesSamAccountName()}, + } + + if !i.useServerUUID { + attrs["owncloudUUID"] = []string{uuid.Must(uuid.NewV4()).String()} + attrs["objectClass"] = append(attrs["objectClass"], "owncloud") + } + + // inetOrgPerson requires "sn" to be set. Set it to the Username if + // Surname is not set in the Request + var sn string + if user.Surname != nil && *user.Surname != "" { + sn = *user.Surname + } else { + sn = *user.OnPremisesSamAccountName + } + attrs["sn"] = []string{sn} + + if !i.usePwModifyExOp && user.PasswordProfile != nil && user.PasswordProfile.Password != nil { + // Depending on the LDAP server implementation this might cause the + // password to be stored in cleartext in the LDAP database. Using the + // "Password Modify LDAP Extended Operation" is recommended. + attrs["userPassword"] = []string{*user.PasswordProfile.Password} + } + return attrs, nil +} + +func (i *LDAP) getUserLDAPDN(user libregraph.User) string { + return fmt.Sprintf("uid=%s,%s", oldap.EscapeDNAttributeValue(*user.OnPremisesSamAccountName), i.userBaseDN) +} + +func (i *LDAP) userToAddRequest(user libregraph.User) (*ldap.AddRequest, error) { + ar := ldap.NewAddRequest(i.getUserLDAPDN(user), nil) + + attrMap, err := i.userToLDAPAttrValues(user) + if err != nil { + return nil, err + } + for attrType, values := range attrMap { + ar.Attribute(attrType, values) + } + return ar, nil +} + func pointerOrNil(val string) *string { if val == "" { return nil diff --git a/services/graph/pkg/identity/ldap_education_user.go b/services/graph/pkg/identity/ldap_education_user.go index 391584b25f..099cb7912b 100644 --- a/services/graph/pkg/identity/ldap_education_user.go +++ b/services/graph/pkg/identity/ldap_education_user.go @@ -2,19 +2,78 @@ package identity import ( "context" + "errors" + "fmt" "net/url" + "strings" + "github.com/go-ldap/ldap/v3" libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" ) +type educationUserAttributeMap struct { + identities string + primaryRole string +} + +func newEducationUserAttributeMap() educationUserAttributeMap { + return educationUserAttributeMap{ + identities: "oCExternalIdentity", + primaryRole: "userClass", + } +} + // CreateEducationUser creates a given education user in the identity backend. func (i *LDAP) CreateEducationUser(ctx context.Context, user libregraph.EducationUser) (*libregraph.EducationUser, error) { - return nil, errNotImplemented + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("CreateEducationUser") + if !i.writeEnabled { + return nil, errReadOnly + } + + ar, err := i.educationUserToAddRequest(user) + if err != nil { + return nil, err + } + + 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) { + if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists { + err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error()) + } + } + return nil, err + } + + // Read back user from LDAP to get the generated UUID + e, err := i.getEducationUserByDN(ar.DN) + if err != nil { + return nil, err + } + return i.createEducationUserModelFromLDAP(e), nil } // DeleteEducationUser deletes a given educationuser, identified by username or id, from the backend func (i *LDAP) DeleteEducationUser(ctx context.Context, nameOrID string) error { - return errNotImplemented + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("DeleteEducationUser") + if !i.writeEnabled { + return errReadOnly + } + // TODO, implement a proper lookup for education Users here + e, err := i.getEducationUserByNameOrID(nameOrID) + if err != nil { + return err + } + + dr := ldap.DelRequest{DN: e.DN} + if err = i.conn.Del(&dr); err != nil { + return err + } + return nil } // UpdateEducationUser applies changes to given education user, identified by username or id @@ -24,10 +83,192 @@ func (i *LDAP) UpdateEducationUser(ctx context.Context, nameOrID string, user li // GetEducationUser implements the EducationBackend interface for the LDAP backend. func (i *LDAP) GetEducationUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationUser, error) { - return nil, errNotImplemented + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("GetEducationUser") + e, err := i.getEducationUserByNameOrID(nameOrID) + if err != nil { + return nil, err + } + u := i.createEducationUserModelFromLDAP(e) + if u == nil { + return nil, errNotFound + } + return u, nil } // GetEducationUsers implements the EducationBackend interface for the LDAP backend. func (i *LDAP) GetEducationUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationUser, error) { - return nil, errNotImplemented + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("GetEducationUsers") + + search := queryParam.Get("search") + if search == "" { + search = queryParam.Get("$search") + } + var userFilter string + if search != "" { + search = ldap.EscapeFilter(search) + userFilter = fmt.Sprintf( + "(|(%s=%s*)(%s=%s*)(%s=%s*))", + i.userAttributeMap.userName, search, + i.userAttributeMap.mail, search, + i.userAttributeMap.displayName, search, + ) + } + + if userFilter == "" && i.userFilter == "" { + userFilter = fmt.Sprintf("(objectClass=%s)", i.educationConfig.userObjectClass) + } else { + userFilter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.educationConfig.userObjectClass, userFilter) + } + + searchRequest := ldap.NewSearchRequest( + i.userBaseDN, + i.userScope, + ldap.NeverDerefAliases, 0, 0, false, + userFilter, + i.getEducationUserAttrTypes(), + 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("GetEducationUsers") + 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 + if u == nil { + continue + } + users = append(users, u) + } + return users, nil +} + +func (i *LDAP) educationUserToUser(eduUser libregraph.EducationUser) *libregraph.User { + user := libregraph.NewUser() + user.OnPremisesSamAccountName = eduUser.OnPremisesSamAccountName + user.Surname = eduUser.Surname + user.AccountEnabled = eduUser.AccountEnabled + user.GivenName = eduUser.GivenName + user.DisplayName = eduUser.DisplayName + user.Mail = eduUser.Mail + return user +} +func (i *LDAP) userToEducationUser(user libregraph.User, e *ldap.Entry) *libregraph.EducationUser { + eduUser := libregraph.NewEducationUser() + eduUser.Id = user.Id + eduUser.OnPremisesSamAccountName = user.OnPremisesSamAccountName + eduUser.Surname = user.Surname + eduUser.AccountEnabled = user.AccountEnabled + eduUser.GivenName = user.GivenName + eduUser.DisplayName = user.DisplayName + eduUser.Mail = user.Mail + + if e != nil { + // Set the education User specific Attributes from the supplied LDAP Entry + if primaryRole := e.GetEqualFoldAttributeValue(i.educationConfig.userAttributeMap.primaryRole); primaryRole != "" { + eduUser.SetPrimaryRole(primaryRole) + } + var identities []libregraph.ObjectIdentity + for _, identityStr := range e.GetEqualFoldAttributeValues(i.educationConfig.userAttributeMap.identities) { + parts := strings.SplitN(identityStr, "$", 3) + identity := libregraph.NewObjectIdentity() + identity.SetIssuer(strings.TrimSpace(parts[1])) + identity.SetIssuerAssignedId(strings.TrimSpace(parts[2])) + identities = append(identities, *identity) + } + if len(identities) > 0 { + eduUser.SetIdentities(identities) + } + } + + return eduUser +} + +func (i *LDAP) educationUserToLDAPAttrValues(user libregraph.EducationUser, attrs ldapAttributeValues) (ldapAttributeValues, error) { + if role, ok := user.GetPrimaryRoleOk(); ok { + attrs[i.educationConfig.userAttributeMap.primaryRole] = []string{*role} + } + if identities, ok := user.GetIdentitiesOk(); ok { + for _, identity := range identities { + // TODO add support for the "signInType" of objectIdentity + if identity.GetIssuer() == "" || identity.GetIssuerAssignedId() == "" { + return nil, fmt.Errorf("missing Attribute for objectIdentity") + } + identityStr := fmt.Sprintf(" $ %s $ %s", identity.GetIssuer(), identity.GetIssuerAssignedId()) + attrs[i.educationConfig.userAttributeMap.identities] = append( + attrs[i.educationConfig.userAttributeMap.identities], + identityStr, + ) + } + } + attrs["objectClass"] = append(attrs["objectClass"], i.educationConfig.userObjectClass) + return attrs, nil +} + +func (i *LDAP) educationUserToAddRequest(user libregraph.EducationUser) (*ldap.AddRequest, error) { + plainUser := i.educationUserToUser(user) + ldapAttrs, err := i.userToLDAPAttrValues(*plainUser) + if err != nil { + return nil, err + } + ldapAttrs, err = i.educationUserToLDAPAttrValues(user, ldapAttrs) + if err != nil { + return nil, err + } + + ar := ldap.NewAddRequest(i.getUserLDAPDN(*plainUser), nil) + + for attrType, values := range ldapAttrs { + ar.Attribute(attrType, values) + } + return ar, nil +} + +func (i *LDAP) createEducationUserModelFromLDAP(e *ldap.Entry) *libregraph.EducationUser { + user := i.createUserModelFromLDAP(e) + return i.userToEducationUser(*user, e) +} + +func (i *LDAP) getEducationUserAttrTypes() []string { + return []string{ + i.userAttributeMap.displayName, + i.userAttributeMap.id, + i.userAttributeMap.mail, + i.userAttributeMap.userName, + i.educationConfig.userAttributeMap.identities, + i.educationConfig.userAttributeMap.primaryRole, + } +} + +func (i *LDAP) getEducationUserByDN(dn string) (*ldap.Entry, error) { + filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.userObjectClass) + + if i.userFilter != "" { + filter = fmt.Sprintf("(&%s(%s))", filter, i.userFilter) + } + + return i.getEntryByDN(dn, i.getEducationUserAttrTypes(), filter) +} + +func (i *LDAP) getEducationUserByNameOrID(nameOrID string) (*ldap.Entry, error) { + nameOrID = ldap.EscapeFilter(nameOrID) + filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", i.userAttributeMap.userName, nameOrID, i.userAttributeMap.id, nameOrID) + return i.getEducationUserByFilter(filter) +} + +func (i *LDAP) getEducationUserByFilter(filter string) (*ldap.Entry, error) { + filter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.educationConfig.userObjectClass, filter) + return i.searchLDAPEntryByFilter(i.userBaseDN, i.getEducationUserAttrTypes(), filter) } diff --git a/services/graph/pkg/identity/ldap_education_user_test.go b/services/graph/pkg/identity/ldap_education_user_test.go new file mode 100644 index 0000000000..8d42549dbf --- /dev/null +++ b/services/graph/pkg/identity/ldap_education_user_test.go @@ -0,0 +1,132 @@ +package identity + +import ( + "context" + "testing" + + "github.com/go-ldap/ldap/v3" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/services/graph/mocks" + "github.com/test-go/testify/assert" + "github.com/test-go/testify/mock" +) + +var eduUserEntry = ldap.NewEntry("uid=user,ou=people,dc=test", + map[string][]string{ + "uid": {"testuser"}, + "displayname": {"Test User"}, + "mail": {"user@example"}, + "entryuuid": {"abcd-defg"}, + "userClass": {"student"}, + "oCExternalIdentity": { + "$ http://idp $ testuser", + "xxx $ http://idpnew $ xxxxx-xxxxx-xxxxx", + }, + }) + +var sr1 *ldap.SearchRequest = &ldap.SearchRequest{ + BaseDN: "ou=people,dc=test", + Scope: 2, + SizeLimit: 1, + Filter: "(&(objectClass=ocEducationUser)(|(uid=abcd-defg)(entryUUID=abcd-defg)))", + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "oCExternalIdentity", "userClass"}, + Controls: []ldap.Control(nil), +} +var sr2 *ldap.SearchRequest = &ldap.SearchRequest{ + BaseDN: "ou=people,dc=test", + Scope: 2, + SizeLimit: 1, + Filter: "(&(objectClass=ocEducationUser)(|(uid=xxxx-xxxx)(entryUUID=xxxx-xxxx)))", + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "oCExternalIdentity", "userClass"}, + Controls: []ldap.Control(nil), +} + +func TestCreateEducationUser(t *testing.T) { + lm := &mocks.Client{} + b, err := getMockedBackend(lm, eduConfig, &logger) + assert.Nil(t, err) + //assert.NotEqual(t, "", b.educationConfig.schoolObjectClass) + lm.On("Add", mock.Anything).Return(nil) + + lm.On("Search", mock.Anything). + Return( + &ldap.SearchResult{ + Entries: []*ldap.Entry{ + eduUserEntry, + }, + }, + nil) + user := libregraph.NewEducationUser() + user.SetDisplayName("Test User") + user.SetOnPremisesSamAccountName("testuser") + user.SetMail("testuser@example.org") + user.SetPrimaryRole("student") + eduUser, err := b.CreateEducationUser(context.Background(), *user) + lm.AssertNumberOfCalls(t, "Add", 1) + lm.AssertNumberOfCalls(t, "Search", 1) + assert.NotNil(t, eduUser) + assert.Nil(t, err) + assert.Equal(t, eduUser.GetDisplayName(), user.GetDisplayName()) + assert.Equal(t, eduUser.GetOnPremisesSamAccountName(), user.GetOnPremisesSamAccountName()) + assert.Equal(t, "abcd-defg", eduUser.GetId()) + assert.Equal(t, eduUser.GetPrimaryRole(), user.GetPrimaryRole()) +} + +func TestDeleteEducationUser(t *testing.T) { + lm := &mocks.Client{} + + lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil) + lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil) + dr1 := &ldap.DelRequest{ + DN: "uid=user,ou=people,dc=test", + } + lm.On("Del", dr1).Return(nil) + b, err := getMockedBackend(lm, eduConfig, &logger) + assert.Nil(t, err) + err = b.DeleteEducationUser(context.Background(), "abcd-defg") + lm.AssertNumberOfCalls(t, "Search", 1) + lm.AssertNumberOfCalls(t, "Del", 1) + assert.Nil(t, err) + + err = b.DeleteEducationUser(context.Background(), "xxxx-xxxx") + lm.AssertNumberOfCalls(t, "Search", 2) + lm.AssertNumberOfCalls(t, "Del", 1) + assert.NotNil(t, err) + assert.Equal(t, "itemNotFound", err.Error()) +} + +func TestGetEducationUser(t *testing.T) { + lm := &mocks.Client{} + lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil) + lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil) + b, err := getMockedBackend(lm, eduConfig, &logger) + assert.Nil(t, err) + user, err := b.GetEducationUser(context.Background(), "abcd-defg", nil) + lm.AssertNumberOfCalls(t, "Search", 1) + assert.Nil(t, err) + assert.Equal(t, "Test User", user.GetDisplayName()) + assert.Equal(t, "abcd-defg", user.GetId()) + + user, err = b.GetEducationUser(context.Background(), "xxxx-xxxx", nil) + lm.AssertNumberOfCalls(t, "Search", 2) + assert.NotNil(t, err) + assert.Equal(t, "itemNotFound", err.Error()) +} + +func TestGetEducationUsers(t *testing.T) { + lm := &mocks.Client{} + sr := &ldap.SearchRequest{ + BaseDN: "ou=people,dc=test", + Scope: 2, + SizeLimit: 0, + Filter: "(objectClass=ocEducationUser)", + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "oCExternalIdentity", "userClass"}, + 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.GetEducationUsers(context.Background(), nil) + lm.AssertNumberOfCalls(t, "Search", 1) + assert.Nil(t, err) +} diff --git a/services/graph/pkg/identity/ldap_school.go b/services/graph/pkg/identity/ldap_school.go index ec30b1ccc1..d3413cc45b 100644 --- a/services/graph/pkg/identity/ldap_school.go +++ b/services/graph/pkg/identity/ldap_school.go @@ -20,6 +20,9 @@ type educationConfig struct { schoolObjectClass string schoolScope int schoolAttributeMap schoolAttributeMap + + userObjectClass string + userAttributeMap educationUserAttributeMap } type schoolAttributeMap struct { @@ -33,6 +36,9 @@ func defaultEducationConfig() educationConfig { schoolObjectClass: "ocEducationSchool", schoolScope: ldap.ScopeWholeSubtree, schoolAttributeMap: newSchoolAttributeMap(), + + userObjectClass: "ocEducationUser", + userAttributeMap: newEducationUserAttributeMap(), } }