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.
This commit is contained in:
Ralf Haferkamp
2022-12-15 17:27:04 +01:00
committed by Ralf Haferkamp
parent 78637a2f00
commit 80a2c72491
4 changed files with 481 additions and 85 deletions

View File

@@ -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

View File

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

View File

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

View File

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