Skip to content

Commit 2fcfa32

Browse files
author
Julian Koberg
committed
feat: use externalID for provisioning api
Signed-off-by: Julian Koberg <julian.koberg@kiteworks.com>
1 parent 4d94064 commit 2fcfa32

File tree

11 files changed

+113
-14
lines changed

11 files changed

+113
-14
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Enhancement: Use externalID in Provisioning API
2+
3+
This PR adds the externalID as optional parameter to the Provisioning API that can be used as the primary identifier. It also contains a switch to enable this setting.
4+
5+
https://github.com/owncloud/ocis/pull/11799

services/graph/pkg/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ type LDAP struct {
7575
UserIDIsOctetString bool `yaml:"user_id_is_octet_string" env:"OCIS_LDAP_USER_SCHEMA_ID_IS_OCTETSTRING;GRAPH_LDAP_USER_SCHEMA_ID_IS_OCTETSTRING" desc:"Set this to true if the defined 'ID' attribute for users is of the 'OCTETSTRING' syntax. This is required when using the 'objectGUID' attribute of Active Directory for the user ID's." introductionVersion:"pre5.0"`
7676
UserTypeAttribute string `yaml:"user_type_attribute" env:"OCIS_LDAP_USER_SCHEMA_USER_TYPE;GRAPH_LDAP_USER_TYPE_ATTRIBUTE" desc:"LDAP Attribute to distinguish between 'Member' and 'Guest' users. Default is 'ownCloudUserType'." introductionVersion:"pre5.0"`
7777
UserEnabledAttribute string `yaml:"user_enabled_attribute" env:"OCIS_LDAP_USER_ENABLED_ATTRIBUTE;GRAPH_USER_ENABLED_ATTRIBUTE" desc:"LDAP Attribute to use as a flag telling if the user is enabled or disabled." introductionVersion:"pre5.0"`
78+
ExternalIDAttribute string `yaml:"external_id_attribute" env:"OCIS_LDAP_USER_SCHEMA_EXTERNAL_ID;GRAPH_LDAP_EXTERNAL_ID_ATTRIBUTE" desc:"LDAP attribute that references the external ID of users during the provisioning process. The final ID is provided by an external identity provider. If it is not set, a default attribute will be used instead." introductionVersion:"Curie"`
7879
DisableUserMechanism string `yaml:"disable_user_mechanism" env:"OCIS_LDAP_DISABLE_USER_MECHANISM;GRAPH_DISABLE_USER_MECHANISM" desc:"An option to control the behavior for disabling users. Supported options are 'none', 'attribute' and 'group'. If set to 'group', disabling a user via API will add the user to the configured group for disabled users, if set to 'attribute' this will be done in the ldap user entry, if set to 'none' the disable request is not processed. Default is 'attribute'." introductionVersion:"pre5.0"`
7980
LdapDisabledUsersGroupDN string `yaml:"ldap_disabled_users_group_dn" env:"OCIS_LDAP_DISABLED_USERS_GROUP_DN;GRAPH_DISABLED_USERS_GROUP_DN" desc:"The distinguished name of the group to which added users will be classified as disabled when 'disable_user_mechanism' is set to 'group'." introductionVersion:"pre5.0"`
8081

@@ -90,6 +91,7 @@ type LDAP struct {
9091

9192
EducationResourcesEnabled bool `yaml:"education_resources_enabled" env:"GRAPH_LDAP_EDUCATION_RESOURCES_ENABLED" desc:"Enable LDAP support for managing education related resources." introductionVersion:"pre5.0"`
9293
EducationConfig LDAPEducationConfig
94+
RequireExternalID bool `yaml:"require_external_id" env:"GRAPH_LDAP_REQUIRE_EXTERNAL_ID" desc:"If enabled, the 'OCIS_LDAP_USER_SCHEMA_EXTERNAL_ID' is used as primary identifier for the provisioning API." introductionVersion:"Curie"`
9395
}
9496

9597
// LDAPEducationConfig represents the LDAP configuration for education related resources

services/graph/pkg/config/defaults/defaultconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ func DefaultConfig() *config.Config {
101101
UserIDAttribute: "owncloudUUID",
102102
UserTypeAttribute: "ownCloudUserType",
103103
UserEnabledAttribute: "ownCloudUserEnabled",
104+
ExternalIDAttribute: "owncloudExternalID",
104105
DisableUserMechanism: "attribute",
105106
LdapDisabledUsersGroupDN: "cn=DisabledUsersGroup,ou=groups,o=libregraph-idm",
106107
GroupBaseDN: "ou=groups,o=libregraph-idm",

services/graph/pkg/identity/ldap.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ type LDAP struct {
7272

7373
educationConfig educationConfig
7474

75+
useExternalID bool
76+
7577
logger *log.Logger
7678
conn ldap.Client
7779
}
@@ -87,6 +89,7 @@ type userAttributeMap struct {
8789
userType string
8890
identities string
8991
lastSignIn string
92+
externalID string
9093
}
9194

9295
type ldapAttributeValues map[string][]string
@@ -122,6 +125,7 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LD
122125
userType: config.UserTypeAttribute,
123126
identities: identitiesAttribute,
124127
lastSignIn: lastSignAttribute,
128+
externalID: config.ExternalIDAttribute,
125129
}
126130

127131
if config.GroupNameAttribute == "" || config.GroupIDAttribute == "" {
@@ -176,6 +180,7 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LD
176180
conn: lc,
177181
writeEnabled: config.WriteEnabled,
178182
refintEnabled: config.RefintEnabled,
183+
useExternalID: config.RequireExternalID,
179184
}, nil
180185
}
181186

@@ -844,6 +849,7 @@ func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User {
844849
GivenName: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.givenName)),
845850
Surname: &surname,
846851
AccountEnabled: booleanOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.accountEnabled)),
852+
ExternalID: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.externalID)),
847853
}
848854

849855
userType := e.GetEqualFoldAttributeValue(i.userAttributeMap.userType)
@@ -887,6 +893,7 @@ func (i *LDAP) userToLDAPAttrValues(user libregraph.User) (map[string][]string,
887893
"objectClass": {"inetOrgPerson", "organizationalPerson", "person", "top", "ownCloudUser"},
888894
"cn": {user.GetOnPremisesSamAccountName()},
889895
i.userAttributeMap.userType: {user.GetUserType()},
896+
i.userAttributeMap.externalID: {user.GetExternalID()},
890897
}
891898

892899
if identities, ok := user.GetIdentitiesOk(); ok {
@@ -957,6 +964,7 @@ func (i *LDAP) getUserAttrTypesForSearch() []string {
957964
i.userAttributeMap.userType,
958965
i.userAttributeMap.identities,
959966
i.userAttributeMap.lastSignIn,
967+
i.userAttributeMap.externalID,
960968
}
961969
}
962970

services/graph/pkg/identity/ldap_education_school.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ func (i *LDAP) AddUsersToEducationSchool(ctx context.Context, schoolNumberOrID s
428428

429429
userEntries := make([]*ldap.Entry, 0, len(memberIDs))
430430
for _, memberID := range memberIDs {
431-
user, err := i.getEducationUserByNameOrID(memberID)
431+
user, err := i.getEducationUser(memberID)
432432
if err != nil {
433433
i.logger.Warn().Str("userid", memberID).Msg("User does not exist")
434434
return errorcode.New(errorcode.ItemNotFound, fmt.Sprintf("user '%s' not found", memberID))
@@ -472,7 +472,7 @@ func (i *LDAP) RemoveUserFromEducationSchool(ctx context.Context, schoolNumberOr
472472
}
473473

474474
schoolID := schoolEntry.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
475-
user, err := i.getEducationUserByNameOrID(memberID)
475+
user, err := i.getEducationUser(memberID)
476476
if err != nil {
477477
i.logger.Warn().Str("userid", memberID).Msg("User does not exist")
478478
return err

services/graph/pkg/identity/ldap_education_school_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var eduConfig = config.LDAP{
2525
UserEmailAttribute: "mail",
2626
UserNameAttribute: "uid",
2727
UserEnabledAttribute: "userEnabledAttribute",
28+
ExternalIDAttribute: "externalID",
2829
DisableUserMechanism: "attribute",
2930
UserTypeAttribute: "userTypeAttribute",
3031

services/graph/pkg/identity/ldap_education_user.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,13 @@ func (i *LDAP) CreateEducationUser(ctx context.Context, user libregraph.Educatio
5353
}
5454

5555
// DeleteEducationUser deletes a given education user, identified by username or id, from the backend
56-
func (i *LDAP) DeleteEducationUser(ctx context.Context, nameOrID string) error {
56+
func (i *LDAP) DeleteEducationUser(ctx context.Context, id string) error {
5757
logger := i.logger.SubloggerWithRequestID(ctx)
5858
logger.Debug().Str("backend", "ldap").Msg("DeleteEducationUser")
5959
if !i.writeEnabled {
6060
return ErrReadOnly
6161
}
62-
// TODO, implement a proper lookup for education Users here
63-
e, err := i.getEducationUserByNameOrID(nameOrID)
62+
e, err := i.getEducationUser(id)
6463
if err != nil {
6564
return err
6665
}
@@ -73,13 +72,13 @@ func (i *LDAP) DeleteEducationUser(ctx context.Context, nameOrID string) error {
7372
}
7473

7574
// UpdateEducationUser applies changes to given education user, identified by username or id
76-
func (i *LDAP) UpdateEducationUser(ctx context.Context, nameOrID string, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
75+
func (i *LDAP) UpdateEducationUser(ctx context.Context, id string, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
7776
logger := i.logger.SubloggerWithRequestID(ctx)
7877
logger.Debug().Str("backend", "ldap").Msg("UpdateEducationUser")
7978
if !i.writeEnabled {
8079
return nil, ErrReadOnly
8180
}
82-
e, err := i.getEducationUserByNameOrID(nameOrID)
81+
e, err := i.getEducationUser(id)
8382
if err != nil {
8483
return nil, err
8584
}
@@ -189,10 +188,10 @@ func (i *LDAP) UpdateEducationUser(ctx context.Context, nameOrID string, user li
189188
}
190189

191190
// GetEducationUser implements the EducationBackend interface for the LDAP backend.
192-
func (i *LDAP) GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error) {
191+
func (i *LDAP) GetEducationUser(ctx context.Context, id string) (*libregraph.EducationUser, error) {
193192
logger := i.logger.SubloggerWithRequestID(ctx)
194193
logger.Debug().Str("backend", "ldap").Msg("GetEducationUser")
195-
e, err := i.getEducationUserByNameOrID(nameOrID)
194+
e, err := i.getEducationUser(id)
196195
if err != nil {
197196
return nil, err
198197
}
@@ -257,6 +256,7 @@ func (i *LDAP) educationUserToUser(eduUser libregraph.EducationUser) *libregraph
257256
user.Mail = eduUser.Mail
258257
user.UserType = eduUser.UserType
259258
user.Identities = eduUser.Identities
259+
user.ExternalID = eduUser.ExternalID
260260

261261
return user
262262
}
@@ -272,6 +272,7 @@ func (i *LDAP) userToEducationUser(user libregraph.User, e *ldap.Entry) *libregr
272272
eduUser.Mail = user.Mail
273273
eduUser.UserType = user.UserType
274274
eduUser.Identities = user.Identities
275+
eduUser.ExternalID = user.ExternalID
275276

276277
if e != nil {
277278
// Set the education User specific Attributes from the supplied LDAP Entry
@@ -326,6 +327,7 @@ func (i *LDAP) getEducationUserAttrTypes() []string {
326327
i.userAttributeMap.accountEnabled,
327328
i.userAttributeMap.userType,
328329
i.userAttributeMap.identities,
330+
i.userAttributeMap.externalID,
329331
i.educationConfig.userAttributeMap.primaryRole,
330332
i.educationConfig.memberOfSchoolAttribute,
331333
}
@@ -341,6 +343,13 @@ func (i *LDAP) getEducationUserByDN(dn string) (*ldap.Entry, error) {
341343
return i.getEntryByDN(dn, i.getEducationUserAttrTypes(), filter)
342344
}
343345

346+
func (i *LDAP) getEducationUser(nameOrID string) (*ldap.Entry, error) {
347+
if i.useExternalID {
348+
return i.getEducationUserByExternalID(nameOrID)
349+
}
350+
return i.getEducationUserByNameOrID(nameOrID)
351+
}
352+
344353
func (i *LDAP) getEducationUserByNameOrID(nameOrID string) (*ldap.Entry, error) {
345354
return i.getEducationObjectByNameOrID(
346355
nameOrID,
@@ -353,12 +362,28 @@ func (i *LDAP) getEducationUserByNameOrID(nameOrID string) (*ldap.Entry, error)
353362
)
354363
}
355364

365+
func (i *LDAP) getEducationUserByExternalID(id string) (*ldap.Entry, error) {
366+
return i.getEducationObjectByID(
367+
id,
368+
i.userAttributeMap.externalID,
369+
i.userFilter,
370+
i.educationConfig.userObjectClass,
371+
i.userBaseDN,
372+
i.getEducationUserAttrTypes(),
373+
)
374+
}
375+
356376
func (i *LDAP) getEducationObjectByNameOrID(nameOrID, nameAttribute, idAttribute, objectFilter, objectClass, baseDN string, attributes []string) (*ldap.Entry, error) {
357377
nameOrID = ldap.EscapeFilter(nameOrID)
358378
filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", nameAttribute, nameOrID, idAttribute, nameOrID)
359379
return i.getEducationObjectByFilter(filter, baseDN, objectFilter, objectClass, attributes)
360380
}
361381

382+
func (i *LDAP) getEducationObjectByID(id, idAttribute, objectFilter, objectClass, baseDN string, attributes []string) (*ldap.Entry, error) {
383+
filter := fmt.Sprintf("(%s=%s)", idAttribute, id)
384+
return i.getEducationObjectByFilter(filter, baseDN, objectFilter, objectClass, attributes)
385+
}
386+
362387
func (i *LDAP) getEducationObjectByFilter(filter, baseDN, objectFilter, objectClass string, attributes []string) (*ldap.Entry, error) {
363388
filter = fmt.Sprintf("(&%s(objectClass=%s)%s)", objectFilter, objectClass, filter)
364389
return i.searchLDAPEntryByFilter(baseDN, attributes, filter)

services/graph/pkg/identity/ldap_education_user_test.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var eduUserAttrs = []string{
2121
"userEnabledAttribute",
2222
"userTypeAttribute",
2323
"oCExternalIdentity",
24+
"externalID",
2425
"userClass",
2526
"ocMemberOfSchool",
2627
}
@@ -38,6 +39,7 @@ var eduUserEntry = ldap.NewEntry("uid=user,ou=people,dc=test",
3839
},
3940
"userTypeAttribute": {"Member"},
4041
"userEnabledAttribute": {"FALSE"},
42+
"externalID": {"ext-ernal-id"},
4143
})
4244
var renamedEduUserEntry = ldap.NewEntry("uid=newtestuser,ou=people,dc=test",
4345
map[string][]string{
@@ -52,6 +54,7 @@ var renamedEduUserEntry = ldap.NewEntry("uid=newtestuser,ou=people,dc=test",
5254
},
5355
"userTypeAttribute": {"Guest"},
5456
"userEnabledAttribute": {"TRUE"},
57+
"externalID": {"ext-ernal-id"},
5558
})
5659
var eduUserEntryWithSchool = ldap.NewEntry("uid=user,ou=people,dc=test",
5760
map[string][]string{
@@ -83,6 +86,14 @@ var sr2 *ldap.SearchRequest = &ldap.SearchRequest{
8386
Attributes: eduUserAttrs,
8487
Controls: []ldap.Control(nil),
8588
}
89+
var sr3 *ldap.SearchRequest = &ldap.SearchRequest{
90+
BaseDN: "ou=people,dc=test",
91+
Scope: 2,
92+
SizeLimit: 1,
93+
Filter: "(&(objectClass=ocEducationUser)(externalID=ext-ernal-id))",
94+
Attributes: eduUserAttrs,
95+
Controls: []ldap.Control(nil),
96+
}
8697

8798
func TestCreateEducationUser(t *testing.T) {
8899
lm := &mocks.Client{}
@@ -106,6 +117,8 @@ func TestCreateEducationUser(t *testing.T) {
106117
user.SetPrimaryRole("student")
107118
user.SetUserType(("Member"))
108119
user.SetAccountEnabled(false)
120+
user.SetExternalID("ext-ernal-id")
121+
109122
eduUser, err := b.CreateEducationUser(context.Background(), *user)
110123
lm.AssertNumberOfCalls(t, "Add", 1)
111124
lm.AssertNumberOfCalls(t, "Search", 1)
@@ -117,19 +130,22 @@ func TestCreateEducationUser(t *testing.T) {
117130
assert.Equal(t, eduUser.GetPrimaryRole(), user.GetPrimaryRole())
118131
assert.Equal(t, eduUser.GetUserType(), user.GetUserType())
119132
assert.Equal(t, eduUser.GetAccountEnabled(), false)
133+
assert.Equal(t, eduUser.GetExternalID(), user.GetExternalID())
120134
}
121135

122136
func TestDeleteEducationUser(t *testing.T) {
123137
lm := &mocks.Client{}
124138

125139
lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
126140
lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
141+
lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
127142
dr1 := &ldap.DelRequest{
128143
DN: "uid=user,ou=people,dc=test",
129144
}
130145
lm.On("Del", dr1).Return(nil)
131146
b, err := getMockedBackend(lm, eduConfig, &logger)
132147
assert.Nil(t, err)
148+
133149
err = b.DeleteEducationUser(context.Background(), "abcd-defg")
134150
lm.AssertNumberOfCalls(t, "Search", 1)
135151
lm.AssertNumberOfCalls(t, "Del", 1)
@@ -140,14 +156,23 @@ func TestDeleteEducationUser(t *testing.T) {
140156
lm.AssertNumberOfCalls(t, "Del", 1)
141157
assert.NotNil(t, err)
142158
assert.Equal(t, "itemNotFound: not found", err.Error())
159+
160+
b.useExternalID = true
161+
err = b.DeleteEducationUser(context.Background(), "ext-ernal-id")
162+
lm.AssertNumberOfCalls(t, "Search", 3)
163+
lm.AssertNumberOfCalls(t, "Del", 2)
164+
assert.Nil(t, err)
143165
}
144166

145167
func TestGetEducationUser(t *testing.T) {
146168
lm := &mocks.Client{}
147169
lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
148170
lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
171+
lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
172+
149173
b, err := getMockedBackend(lm, eduConfig, &logger)
150174
assert.Nil(t, err)
175+
151176
user, err := b.GetEducationUser(context.Background(), "abcd-defg")
152177
lm.AssertNumberOfCalls(t, "Search", 1)
153178
assert.Nil(t, err)
@@ -158,6 +183,13 @@ func TestGetEducationUser(t *testing.T) {
158183
lm.AssertNumberOfCalls(t, "Search", 2)
159184
assert.NotNil(t, err)
160185
assert.Equal(t, "itemNotFound: not found", err.Error())
186+
187+
b.useExternalID = true
188+
user, err = b.GetEducationUser(context.Background(), "ext-ernal-id")
189+
lm.AssertNumberOfCalls(t, "Search", 3)
190+
assert.Nil(t, err)
191+
assert.Equal(t, "Test User", user.GetDisplayName())
192+
assert.Equal(t, "ext-ernal-id", user.GetExternalID())
161193
}
162194

163195
func TestGetEducationUsers(t *testing.T) {
@@ -179,14 +211,25 @@ func TestGetEducationUsers(t *testing.T) {
179211
}
180212

181213
func TestUpdateEducationUser(t *testing.T) {
214+
testUpdateEducationUser(t, "(&(objectClass=ocEducationUser)(|(uid=testuser)(entryUUID=testuser)))", false, "testuser")
215+
}
216+
217+
func TestUpdateEducationUserExternalID(t *testing.T) {
218+
testUpdateEducationUser(t, "(&(objectClass=ocEducationUser)(externalID=ext-ernal-id))", true, "ext-ernal-id")
219+
}
220+
221+
func testUpdateEducationUser(t *testing.T, userSearchFilter string, useExternalID bool, id string) {
182222
lm := &mocks.Client{}
183223
b, err := getMockedBackend(lm, eduConfig, &logger)
184224
assert.Nil(t, err)
225+
226+
b.useExternalID = useExternalID
227+
185228
userSearchReq := &ldap.SearchRequest{
186229
BaseDN: "ou=people,dc=test",
187230
Scope: 2,
188231
SizeLimit: 1,
189-
Filter: "(&(objectClass=ocEducationUser)(|(uid=testuser)(entryUUID=testuser)))",
232+
Filter: userSearchFilter,
190233
Attributes: eduUserAttrs,
191234
}
192235
userLookupReq := &ldap.SearchRequest{
@@ -268,15 +311,19 @@ func TestUpdateEducationUser(t *testing.T) {
268311
}
269312
lm.On("ModifyDN", &modDNReq).Return(nil)
270313
lm.On("Modify", &modReq).Return(nil)
314+
271315
user := libregraph.NewEducationUser()
272316
user.SetOnPremisesSamAccountName("newtestuser")
273317
user.SetMail("new@mail.org")
274318
user.SetAccountEnabled(true)
275-
eduUser, err := b.UpdateEducationUser(context.Background(), "testuser", *user)
319+
user.SetExternalID("ext-ernal-id")
320+
321+
eduUser, err := b.UpdateEducationUser(context.Background(), id, *user)
276322
assert.NotNil(t, eduUser)
277323
assert.Nil(t, err)
278324
assert.Equal(t, eduUser.GetOnPremisesSamAccountName(), "newtestuser")
279325
assert.Equal(t, "abcd-defg", eduUser.GetId())
280326
assert.Equal(t, "Guest", eduUser.GetUserType())
281327
assert.Equal(t, eduUser.GetAccountEnabled(), true)
328+
assert.Equal(t, "ext-ernal-id", eduUser.GetExternalID())
282329
}

0 commit comments

Comments
 (0)