diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index 382ba7898..24588c4e3 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -61,6 +61,7 @@ type LDAP struct { UserDisplayNameAttribute string `yaml:"user_displayname_attribute" env:"LDAP_USER_SCHEMA_DISPLAY_NAME;GRAPH_LDAP_USER_DISPLAYNAME_ATTRIBUTE" desc:"LDAP Attribute to use for the displayname of users."` UserNameAttribute string `yaml:"user_name_attribute" env:"LDAP_USER_SCHEMA_USERNAME;GRAPH_LDAP_USER_NAME_ATTRIBUTE" desc:"LDAP Attribute to use for username of users."` UserIDAttribute string `yaml:"user_id_attribute" env:"LDAP_USER_SCHEMA_ID;GRAPH_LDAP_USER_UID_ATTRIBUTE" desc:"LDAP Attribute to use as the unique ID for users. This should be a stable globally unique ID like a UUID."` + UserTypeAttribute string `yaml:"user_type_attribute" env:"LDAP_USER_SCHEMA_USER_TYPE;GRAPH_LDAP_USER_TYPE_ATTRIBUTE" desc:"LDAP Attribute to distinguish between 'Member' and 'Guest' users."` UserEnabledAttribute string `yaml:"user_enabled_attribute" env:"LDAP_USER_ENABLED_ATTRIBUTE;GRAPH_USER_ENABLED_ATTRIBUTE" desc:"LDAP Attribute to use as a flag telling if the user is enabled or disabled."` DisableUserMechanism string `yaml:"disable_user_mechanism" env:"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'."` LdapDisabledUsersGroupDN string `yaml:"ldap_disabled_users_group_dn" env:"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'."` diff --git a/services/graph/pkg/config/defaults/defaultconfig.go b/services/graph/pkg/config/defaults/defaultconfig.go index d6c11b6f9..1d313a378 100644 --- a/services/graph/pkg/config/defaults/defaultconfig.go +++ b/services/graph/pkg/config/defaults/defaultconfig.go @@ -71,6 +71,7 @@ func DefaultConfig() *config.Config { // FIXME: switch this to some more widely available attribute by default // ideally this needs to be constant for the lifetime of a users UserIDAttribute: "owncloudUUID", + UserTypeAttribute: "ownCloudUserType", UserEnabledAttribute: "ownCloudUserEnabled", DisableUserMechanism: "attribute", LdapDisabledUsersGroupDN: "cn=DisabledUsersGroup,ou=groups,o=libregraph-idm", diff --git a/services/graph/pkg/identity/ldap.go b/services/graph/pkg/identity/ldap.go index 4a5dffbb4..a18e07c0c 100644 --- a/services/graph/pkg/identity/ldap.go +++ b/services/graph/pkg/identity/ldap.go @@ -77,6 +77,7 @@ type userAttributeMap struct { givenName string surname string accountEnabled string + userType string } type ldapAttributeValues map[string][]string @@ -105,6 +106,7 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LD accountEnabled: config.UserEnabledAttribute, givenName: _givenNameAttribute, surname: _surNameAttribute, + userType: config.UserTypeAttribute, } if config.GroupNameAttribute == "" || config.GroupIDAttribute == "" { @@ -296,6 +298,12 @@ func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph. updateNeeded = true } } + if user.GetUserType() != "" { + if e.GetEqualFoldAttributeValue(i.userAttributeMap.userType) != user.GetUserType() { + mr.Replace(i.userAttributeMap.userType, []string{user.GetUserType()}) + updateNeeded = true + } + } if user.PasswordProfile != nil && user.PasswordProfile.GetPassword() != "" { if i.usePwModifyExOp { if err := i.updateUserPassowrd(ctx, e.DN, user.PasswordProfile.GetPassword()); err != nil { @@ -372,6 +380,7 @@ func (i *LDAP) getUserByDN(dn string) (*ldap.Entry, error) { i.userAttributeMap.surname, i.userAttributeMap.givenName, i.userAttributeMap.accountEnabled, + i.userAttributeMap.userType, } filter := fmt.Sprintf("(objectClass=%s)", i.userObjectClass) @@ -469,6 +478,7 @@ func (i *LDAP) getLDAPUserByFilter(filter string) (*ldap.Entry, error) { i.userAttributeMap.surname, i.userAttributeMap.givenName, i.userAttributeMap.accountEnabled, + i.userAttributeMap.userType, } return i.searchLDAPEntryByFilter(i.userBaseDN, attrs, filter) } @@ -707,6 +717,7 @@ func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User { id := e.GetEqualFoldAttributeValue(i.userAttributeMap.id) givenName := e.GetEqualFoldAttributeValue(i.userAttributeMap.givenName) surname := e.GetEqualFoldAttributeValue(i.userAttributeMap.surname) + userType := e.GetEqualFoldAttributeValue(i.userAttributeMap.userType) if id != "" && opsan != "" { return &libregraph.User{ @@ -716,6 +727,7 @@ func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User { Id: &id, GivenName: &givenName, Surname: &surname, + UserType: &userType, AccountEnabled: booleanOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.accountEnabled)), } } @@ -730,6 +742,7 @@ func (i *LDAP) userToLDAPAttrValues(user libregraph.User) (map[string][]string, i.userAttributeMap.mail: {user.GetMail()}, "objectClass": {"inetOrgPerson", "organizationalPerson", "person", "top", "ownCloudUser"}, "cn": {user.GetOnPremisesSamAccountName()}, + i.userAttributeMap.userType: {user.GetUserType()}, } if !i.useServerUUID { @@ -778,6 +791,7 @@ func (i *LDAP) getUserAttrTypes() []string { "owncloudUUID", "userPassword", i.userAttributeMap.accountEnabled, + i.userAttributeMap.userType, } } diff --git a/services/graph/pkg/identity/ldap_education_class_test.go b/services/graph/pkg/identity/ldap_education_class_test.go index 682e4d907..3f328a4cb 100644 --- a/services/graph/pkg/identity/ldap_education_class_test.go +++ b/services/graph/pkg/identity/ldap_education_class_test.go @@ -275,7 +275,7 @@ func TestGetEducationClassMembers(t *testing.T) { Scope: 0, SizeLimit: 1, Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, Controls: []ldap.Control(nil), } lm.On("Search", user_sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil) diff --git a/services/graph/pkg/identity/ldap_education_school_test.go b/services/graph/pkg/identity/ldap_education_school_test.go index ffcbd918a..c1b4e3d2f 100644 --- a/services/graph/pkg/identity/ldap_education_school_test.go +++ b/services/graph/pkg/identity/ldap_education_school_test.go @@ -22,6 +22,7 @@ var eduConfig = config.LDAP{ UserEmailAttribute: "mail", UserNameAttribute: "uid", UserEnabledAttribute: "userEnabledAttribute", + UserTypeAttribute: "userTypeAttribute", GroupBaseDN: "ou=groups,dc=test", GroupObjectClass: "groupOfNames", diff --git a/services/graph/pkg/identity/ldap_education_user.go b/services/graph/pkg/identity/ldap_education_user.go index a0b32e57d..8bafdd62f 100644 --- a/services/graph/pkg/identity/ldap_education_user.go +++ b/services/graph/pkg/identity/ldap_education_user.go @@ -133,6 +133,12 @@ func (i *LDAP) UpdateEducationUser(ctx context.Context, nameOrID string, user li updateNeeded = true } } + if user.GetUserType() != "" { + if e.GetEqualFoldAttributeValue(i.userAttributeMap.userType) != user.GetUserType() { + mr.Replace(i.userAttributeMap.userType, []string{user.GetUserType()}) + updateNeeded = true + } + } if user.PasswordProfile != nil && user.PasswordProfile.GetPassword() != "" { if i.usePwModifyExOp { if err := i.updateUserPassowrd(ctx, e.DN, user.PasswordProfile.GetPassword()); err != nil { @@ -250,6 +256,8 @@ func (i *LDAP) educationUserToUser(eduUser libregraph.EducationUser) *libregraph user.GivenName = eduUser.GivenName user.DisplayName = eduUser.DisplayName user.Mail = eduUser.Mail + user.UserType = eduUser.UserType + return user } @@ -262,6 +270,7 @@ func (i *LDAP) userToEducationUser(user libregraph.User, e *ldap.Entry) *libregr eduUser.GivenName = user.GivenName eduUser.DisplayName = user.DisplayName eduUser.Mail = user.Mail + eduUser.UserType = user.UserType if e != nil { // Set the education User specific Attributes from the supplied LDAP Entry @@ -345,6 +354,7 @@ func (i *LDAP) getEducationUserAttrTypes() []string { i.userAttributeMap.surname, i.userAttributeMap.givenName, i.userAttributeMap.accountEnabled, + i.userAttributeMap.userType, i.educationConfig.userAttributeMap.identities, i.educationConfig.userAttributeMap.primaryRole, i.educationConfig.memberOfSchoolAttribute, diff --git a/services/graph/pkg/identity/ldap_education_user_test.go b/services/graph/pkg/identity/ldap_education_user_test.go index dc4777e4d..3e7d38717 100644 --- a/services/graph/pkg/identity/ldap_education_user_test.go +++ b/services/graph/pkg/identity/ldap_education_user_test.go @@ -11,7 +11,19 @@ import ( "github.com/test-go/testify/mock" ) -var eduUserAttrs = []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "oCExternalIdentity", "userClass", "ocMemberOfSchool"} +var eduUserAttrs = []string{ + "displayname", + "entryUUID", + "mail", + "uid", + "sn", + "givenname", + "userEnabledAttribute", + "userTypeAttribute", + "oCExternalIdentity", + "userClass", + "ocMemberOfSchool", +} var eduUserEntry = ldap.NewEntry("uid=user,ou=people,dc=test", map[string][]string{ @@ -24,6 +36,7 @@ var eduUserEntry = ldap.NewEntry("uid=user,ou=people,dc=test", "$ http://idp $ testuser", "xxx $ http://idpnew $ xxxxx-xxxxx-xxxxx", }, + "userTypeAttribute": {"Member"}, }) var renamedEduUserEntry = ldap.NewEntry("uid=newtestuser,ou=people,dc=test", map[string][]string{ @@ -36,6 +49,7 @@ var renamedEduUserEntry = ldap.NewEntry("uid=newtestuser,ou=people,dc=test", "$ http://idp $ testuser", "xxx $ http://idpnew $ xxxxx-xxxxx-xxxxx", }, + "userTypeAttribute": {"Guest"}, }) var eduUserEntryWithSchool = ldap.NewEntry("uid=user,ou=people,dc=test", map[string][]string{ @@ -88,6 +102,7 @@ func TestCreateEducationUser(t *testing.T) { user.SetOnPremisesSamAccountName("testuser") user.SetMail("testuser@example.org") user.SetPrimaryRole("student") + user.SetUserType(("Member")) eduUser, err := b.CreateEducationUser(context.Background(), *user) lm.AssertNumberOfCalls(t, "Add", 1) lm.AssertNumberOfCalls(t, "Search", 1) @@ -97,6 +112,7 @@ func TestCreateEducationUser(t *testing.T) { assert.Equal(t, eduUser.GetOnPremisesSamAccountName(), user.GetOnPremisesSamAccountName()) assert.Equal(t, "abcd-defg", eduUser.GetId()) assert.Equal(t, eduUser.GetPrimaryRole(), user.GetPrimaryRole()) + assert.Equal(t, eduUser.GetUserType(), user.GetUserType()) } func TestDeleteEducationUser(t *testing.T) { @@ -174,7 +190,7 @@ func TestUpdateEducationUser(t *testing.T) { Scope: 0, SizeLimit: 1, Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, } eduUserLookupReq := &ldap.SearchRequest{ BaseDN: "uid=newtestuser,ou=people,dc=test", @@ -249,4 +265,5 @@ func TestUpdateEducationUser(t *testing.T) { assert.Nil(t, err) assert.Equal(t, eduUser.GetOnPremisesSamAccountName(), "newtestuser") assert.Equal(t, "abcd-defg", eduUser.GetId()) + assert.Equal(t, "Guest", eduUser.GetUserType()) } diff --git a/services/graph/pkg/identity/ldap_group_test.go b/services/graph/pkg/identity/ldap_group_test.go index 511076b01..d375d1cef 100644 --- a/services/graph/pkg/identity/ldap_group_test.go +++ b/services/graph/pkg/identity/ldap_group_test.go @@ -100,14 +100,14 @@ func TestGetGroup(t *testing.T) { BaseDN: "uid=user,ou=people,dc=test", SizeLimit: 1, Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, Controls: []ldap.Control(nil), } sr3 := &ldap.SearchRequest{ BaseDN: "uid=invalid,ou=people,dc=test", SizeLimit: 1, Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, Controls: []ldap.Control(nil), } @@ -195,14 +195,14 @@ func TestGetGroups(t *testing.T) { BaseDN: "uid=user,ou=people,dc=test", SizeLimit: 1, Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, Controls: []ldap.Control(nil), } sr3 := &ldap.SearchRequest{ BaseDN: "uid=invalid,ou=people,dc=test", SizeLimit: 1, Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, Controls: []ldap.Control(nil), } diff --git a/services/graph/pkg/identity/ldap_test.go b/services/graph/pkg/identity/ldap_test.go index bf99dca81..27623832d 100644 --- a/services/graph/pkg/identity/ldap_test.go +++ b/services/graph/pkg/identity/ldap_test.go @@ -36,6 +36,7 @@ var lconfig = config.LDAP{ UserEmailAttribute: "mail", UserNameAttribute: "uid", UserEnabledAttribute: "userEnabledAttribute", + UserTypeAttribute: "userTypeAttribute", LdapDisabledUsersGroupDN: disableUsersGroup, DisableUserMechanism: "attribute", @@ -58,6 +59,7 @@ var userEntry = ldap.NewEntry("uid=user", "sn": {"surname"}, "givenname": {"givenName"}, "userenabledattribute": {"TRUE"}, + "usertypeattribute": {"Member"}, }) var invalidUserEntry = ldap.NewEntry("uid=user", @@ -107,6 +109,7 @@ func TestCreateUser(t *testing.T) { userName := "user" surname := "surname" givenName := "givenName" + userType := "Member" ar := ldap.NewAddRequest(fmt.Sprintf("uid=user,%s", lconfig.UserBaseDN), nil) ar.Attribute(lconfig.UserDisplayNameAttribute, []string{displayName}) @@ -117,6 +120,7 @@ func TestCreateUser(t *testing.T) { ar.Attribute("objectClass", []string{"inetOrgPerson", "organizationalPerson", "person", "top", "ownCloudUser"}) ar.Attribute("cn", []string{userName}) ar.Attribute(lconfig.UserEnabledAttribute, []string{"TRUE"}) + ar.Attribute(lconfig.UserTypeAttribute, []string{"Member"}) l := &mocks.Client{} l.On("Search", mock.Anything). @@ -135,6 +139,7 @@ func TestCreateUser(t *testing.T) { user.SetSurname(surname) user.SetGivenName(givenName) user.SetAccountEnabled(true) + user.SetUserType(userType) c := lconfig c.UseServerUUID = true @@ -148,6 +153,7 @@ func TestCreateUser(t *testing.T) { assert.Equal(t, givenName, newUser.GetGivenName()) assert.Equal(t, surname, newUser.GetSurname()) assert.True(t, newUser.GetAccountEnabled()) + assert.Equal(t, userType, newUser.GetUserType()) } func TestCreateUserModelFromLDAP(t *testing.T) { @@ -301,6 +307,7 @@ func TestUpdateUser(t *testing.T) { displayName string onPremisesSamAccountName string accountEnabled *bool + userType string } type args struct { nameOrID string @@ -340,7 +347,7 @@ func TestUpdateUser(t *testing.T) { ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, "(&(objectClass=inetOrgPerson)(|(uid=testUser)(entryUUID=testUser)))", - []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, nil, ), }, @@ -384,7 +391,7 @@ func TestUpdateUser(t *testing.T) { ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, "(&(objectClass=inetOrgPerson)(|(uid=testUser)(entryUUID=testUser)))", - []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, nil, ), }, @@ -428,7 +435,7 @@ func TestUpdateUser(t *testing.T) { TimeLimit: 0, TypesOnly: false, Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, Controls: []ldap.Control(nil), }, }, @@ -513,7 +520,7 @@ func TestUpdateUser(t *testing.T) { ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, "(&(objectClass=inetOrgPerson)(|(uid=testUser)(entryUUID=testUser)))", - []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, nil, ), }, @@ -557,7 +564,7 @@ func TestUpdateUser(t *testing.T) { TimeLimit: 0, TypesOnly: false, Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, Controls: []ldap.Control(nil), }, }, @@ -642,7 +649,7 @@ func TestUpdateUser(t *testing.T) { ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, "(&(objectClass=inetOrgPerson)(|(uid=testUser)(entryUUID=testUser)))", - []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, nil, ), }, @@ -738,7 +745,7 @@ func TestUpdateUser(t *testing.T) { TimeLimit: 0, TypesOnly: false, Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, Controls: []ldap.Control(nil), }, }, @@ -831,7 +838,7 @@ func TestUpdateUser(t *testing.T) { ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, "(&(objectClass=inetOrgPerson)(|(uid=testUser)(entryUUID=testUser)))", - []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, nil, ), }, @@ -875,7 +882,7 @@ func TestUpdateUser(t *testing.T) { TimeLimit: 0, TypesOnly: false, Filter: "(objectClass=inetOrgPerson)", - Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, Controls: []ldap.Control(nil), }, }, @@ -961,7 +968,7 @@ func TestUpdateUser(t *testing.T) { ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, "(&(objectClass=inetOrgPerson)(|(uid=testUser)(entryUUID=testUser)))", - []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, nil, ), }, @@ -1064,7 +1071,7 @@ func TestUpdateUser(t *testing.T) { ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, "(objectClass=inetOrgPerson)", - []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, []ldap.Control(nil), ), }, @@ -1127,7 +1134,7 @@ func TestUpdateUser(t *testing.T) { ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, "(&(objectClass=inetOrgPerson)(|(uid=testUser)(entryUUID=testUser)))", - []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, nil, ), }, @@ -1230,7 +1237,7 @@ func TestUpdateUser(t *testing.T) { ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, "(objectClass=inetOrgPerson)", - []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"}, + []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, []ldap.Control(nil), ), }, @@ -1265,6 +1272,144 @@ func TestUpdateUser(t *testing.T) { }, }, }, + { + name: "Test changing userType", + args: args{ + nameOrID: "testUser", + userProps: userProps{ + userType: "Member", + }, + disableUserMechanism: "group", + }, + want: &userProps{ + id: "testUser", + mail: "testuser@example.org", + displayName: "testUser", + onPremisesSamAccountName: "testUser", + userType: "Member", + }, + assertion: func(t assert.TestingT, err error, args ...interface{}) bool { + return assert.Nil(t, err, args...) + }, + ldapMocks: []mockInputs{ + { + funcName: "Search", + args: []interface{}{ + ldap.NewSearchRequest( + "ou=people,dc=test", + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, 1, 0, false, + "(&(objectClass=inetOrgPerson)(|(uid=testUser)(entryUUID=testUser)))", + []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, + nil, + ), + }, + returns: []interface{}{ + &ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=name", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayname", + Values: []string{"testuser@example.org"}, + }, + { + Name: "entryUUID", + Values: []string{"testUser"}, + }, + { + Name: "mail", + Values: []string{"testuser@example.org"}, + }, + { + Name: lconfig.UserTypeAttribute, + Values: []string{"Guest"}, + }, + }, + }, + }, + }, + nil, + }, + }, + { + funcName: "Modify", + args: []interface{}{ + &ldap.ModifyRequest{ + DN: "uid=name", + Changes: []ldap.Change{ + { + Operation: ldap.ReplaceAttribute, + Modification: ldap.PartialAttribute{ + Type: "userTypeAttribute", + Vals: []string{"Member"}, + }, + }, + }, + Controls: []ldap.Control(nil), + }, + }, + returns: []interface{}{nil}, + }, + { + funcName: "Modify", + args: []interface{}{ + &ldap.ModifyRequest{ + DN: "uid=name", + Changes: []ldap.Change(nil), + Controls: []ldap.Control(nil), + }, + }, + returns: []interface{}{nil}, + }, + { + funcName: "Search", + args: []interface{}{ + ldap.NewSearchRequest( + "uid=name", + ldap.ScopeBaseObject, + ldap.NeverDerefAliases, 1, 0, false, + "(objectClass=inetOrgPerson)", + []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute", "userTypeAttribute"}, + []ldap.Control(nil), + ), + }, + returns: []interface{}{ + &ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=name", + Attributes: []*ldap.EntryAttribute{ + { + Name: "uid", + Values: []string{"testUser"}, + }, + { + Name: "displayname", + Values: []string{"testUser"}, + }, + { + Name: "entryUUID", + Values: []string{"testUser"}, + }, + { + Name: "mail", + Values: []string{"testuser@example.org"}, + }, + { + Name: lconfig.UserTypeAttribute, + Values: []string{"Member"}, + }, + }, + }, + }, + }, + nil, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1283,6 +1428,7 @@ func TestUpdateUser(t *testing.T) { DisplayName: &tt.args.userProps.displayName, OnPremisesSamAccountName: &tt.args.userProps.onPremisesSamAccountName, AccountEnabled: tt.args.userProps.accountEnabled, + UserType: &tt.args.userProps.userType, } emptyString := "" @@ -1295,6 +1441,7 @@ func TestUpdateUser(t *testing.T) { OnPremisesSamAccountName: &tt.want.onPremisesSamAccountName, Surname: &emptyString, GivenName: &emptyString, + UserType: &tt.want.userType, } if tt.want.accountEnabled != nil { diff --git a/services/graph/pkg/service/v0/educationuser.go b/services/graph/pkg/service/v0/educationuser.go index dab3d4754..97f6dcb88 100644 --- a/services/graph/pkg/service/v0/educationuser.go +++ b/services/graph/pkg/service/v0/educationuser.go @@ -132,6 +132,16 @@ func (g Graph) PostEducationUser(w http.ResponseWriter, r *http.Request) { return } + if u.HasUserType() { + if !isValidUserType(*u.UserType) { + logger.Debug().Interface("user", u).Msg("invalid userType attribute") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid userType attribute, valid options are 'Member' or 'Guest'") + return + } + } else { + u.SetUserType("Member") + } + if _, ok := u.GetPrimaryRoleOk(); !ok { logger.Debug().Err(err).Interface("user", u).Msg("could not create education user: missing required Attribute: 'primaryRole'") errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing required Attribute: 'primaryRole'") @@ -368,6 +378,14 @@ func (g Graph) PatchEducationUser(w http.ResponseWriter, r *http.Request) { features = append(features, events.UserFeature{Name: "displayname", Value: *name}) } + if changes.HasUserType() { + if !isValidUserType(*changes.UserType) { + logger.Debug().Interface("user", changes).Msg("invalid userType attribute") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid userType attribute, valid options are 'Member' or 'Guest'") + return + } + } + logger.Debug().Str("nameid", nameOrID).Interface("changes", *changes).Msg("calling update education user on backend") u, err := g.identityEducationBackend.UpdateEducationUser(r.Context(), nameOrID, *changes) if err != nil { diff --git a/services/graph/pkg/service/v0/educationuser_test.go b/services/graph/pkg/service/v0/educationuser_test.go index ed1c833e1..aa04ae833 100644 --- a/services/graph/pkg/service/v0/educationuser_test.go +++ b/services/graph/pkg/service/v0/educationuser_test.go @@ -297,7 +297,65 @@ var _ = Describe("EducationUsers", func() { svc.PostEducationUser(rr, r) Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + createdUser := libregraph.EducationUser{} + err = json.Unmarshal(data, &createdUser) + Expect(err).ToNot(HaveOccurred()) + Expect(createdUser.GetUserType()).To(Equal("Member")) }) + + It("creates a guest user", func() { + roleService.On("AssignRoleToUser", mock.Anything, mock.Anything).Return(&settingssvc.AssignRoleToUserResponse{}, nil) + identityEducationBackend.On("CreateEducationUser", mock.Anything, mock.Anything).Return(func(ctx context.Context, user libregraph.EducationUser) *libregraph.EducationUser { + user.SetId("/users/user") + return &user + }, nil) + + user.SetUserType("Guest") + userJson, err := json.Marshal(user) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/users", bytes.NewBuffer(userJson)) + r = r.WithContext(revactx.ContextSetUser(ctx, currentUser)) + svc.PostEducationUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + createdUser := libregraph.EducationUser{} + err = json.Unmarshal(data, &createdUser) + Expect(err).ToNot(HaveOccurred()) + Expect(createdUser.GetUserType()).To(Equal("Guest")) + }) + + It("creates a member user", func() { + roleService.On("AssignRoleToUser", mock.Anything, mock.Anything).Return(&settingssvc.AssignRoleToUserResponse{}, nil) + identityEducationBackend.On("CreateEducationUser", mock.Anything, mock.Anything).Return(func(ctx context.Context, user libregraph.EducationUser) *libregraph.EducationUser { + user.SetId("/users/user") + return &user + }, nil) + + user.SetUserType("Member") + userJson, err := json.Marshal(user) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/users", bytes.NewBuffer(userJson)) + r = r.WithContext(revactx.ContextSetUser(ctx, currentUser)) + svc.PostEducationUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + createdUser := libregraph.EducationUser{} + err = json.Unmarshal(data, &createdUser) + Expect(err).ToNot(HaveOccurred()) + Expect(createdUser.GetUserType()).To(Equal("Member")) + }) + }) Describe("DeleteEducationUser", func() { @@ -406,6 +464,20 @@ var _ = Describe("EducationUsers", func() { Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) + It("handles invalid userType", func() { + user.SetUserType("Clown") + data, err := json.Marshal(user) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/users?$invalid=true", bytes.NewBuffer(data)) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", user.GetId()) + r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.PatchEducationUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + It("updates attributes", func() { identityEducationBackend.On("UpdateEducationUser", mock.Anything, user.GetId(), mock.Anything).Return(user, nil) diff --git a/services/graph/pkg/service/v0/users.go b/services/graph/pkg/service/v0/users.go index 5d4beb6c4..87ce1136e 100644 --- a/services/graph/pkg/service/v0/users.go +++ b/services/graph/pkg/service/v0/users.go @@ -305,6 +305,16 @@ func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) { return } + if u.HasUserType() { + if !isValidUserType(*u.UserType) { + logger.Debug().Interface("user", u).Msg("invalid userType attribute") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid userType attribute, valid options are 'Member' or 'Guest'") + return + } + } else { + u.SetUserType("Member") + } + logger.Debug().Interface("user", u).Msg("calling create user on backend") if u, err = g.identityBackend.CreateUser(r.Context(), *u); err != nil { logger.Debug().Err(err).Msg("could not create user: backend error") @@ -652,6 +662,14 @@ func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) { features = append(features, events.UserFeature{Name: "displayname", Value: *name}) } + if changes.HasUserType() { + if !isValidUserType(*changes.UserType) { + logger.Debug().Interface("user", changes).Msg("invalid userType attribute") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid userType attribute, valid options are 'Member' or 'Guest'") + return + } + } + logger.Debug().Str("nameid", nameOrID).Interface("changes", *changes).Msg("calling update user on backend") u, err := g.identityBackend.UpdateUser(r.Context(), nameOrID, *changes) if err != nil { @@ -743,3 +761,15 @@ func sortUsers(req *godata.GoDataRequest, users []*libregraph.User) ([]*libregra } return users, nil } + +func isValidUserType(userType string) bool { + userType = strings.ToLower(userType) + + for _, value := range []string{"member", "guest"} { + if userType == value { + return true + } + } + + return false +} diff --git a/services/graph/pkg/service/v0/users_test.go b/services/graph/pkg/service/v0/users_test.go index a6f342fa0..95b50f6d6 100644 --- a/services/graph/pkg/service/v0/users_test.go +++ b/services/graph/pkg/service/v0/users_test.go @@ -588,6 +588,11 @@ var _ = Describe("Users", func() { assertHandleBadAttributes(user) }) + It("handles invalid userType", func() { + user.SetUserType("Clown") + assertHandleBadAttributes(user) + }) + It("creates a user", func() { roleService.On("AssignRoleToUser", mock.Anything, mock.Anything).Return(&settings.AssignRoleToUserResponse{}, nil) identityBackend.On("CreateUser", mock.Anything, mock.Anything).Return(func(ctx context.Context, user libregraph.User) *libregraph.User { @@ -602,6 +607,63 @@ var _ = Describe("Users", func() { svc.PostUser(rr, r) Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + createdUser := libregraph.User{} + err = json.Unmarshal(data, &createdUser) + Expect(err).ToNot(HaveOccurred()) + Expect(createdUser.GetUserType()).To(Equal("Member")) + }) + + It("creates a guest user", func() { + roleService.On("AssignRoleToUser", mock.Anything, mock.Anything).Return(&settings.AssignRoleToUserResponse{}, nil) + identityBackend.On("CreateUser", mock.Anything, mock.Anything).Return(func(ctx context.Context, user libregraph.User) *libregraph.User { + user.SetId("/users/user") + return &user + }, nil) + + user.SetUserType("Guest") + userJson, err := json.Marshal(user) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/users", bytes.NewBuffer(userJson)) + r = r.WithContext(revactx.ContextSetUser(ctx, currentUser)) + svc.PostUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + createdUser := libregraph.User{} + err = json.Unmarshal(data, &createdUser) + Expect(err).ToNot(HaveOccurred()) + Expect(createdUser.GetUserType()).To(Equal("Guest")) + }) + + It("creates a member user", func() { + roleService.On("AssignRoleToUser", mock.Anything, mock.Anything).Return(&settings.AssignRoleToUserResponse{}, nil) + identityBackend.On("CreateUser", mock.Anything, mock.Anything).Return(func(ctx context.Context, user libregraph.User) *libregraph.User { + user.SetId("/users/user") + return &user + }, nil) + + user.SetUserType("Member") + userJson, err := json.Marshal(user) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/users", bytes.NewBuffer(userJson)) + r = r.WithContext(revactx.ContextSetUser(ctx, currentUser)) + svc.PostUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + createdUser := libregraph.User{} + err = json.Unmarshal(data, &createdUser) + Expect(err).ToNot(HaveOccurred()) + Expect(createdUser.GetUserType()).To(Equal("Member")) }) Describe("Handling usernames with spaces", func() { @@ -768,9 +830,25 @@ var _ = Describe("Users", func() { Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) + It("handles invalid userType", func() { + user.SetUserType("Clown") + data, err := json.Marshal(user) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/users?$invalid=true", bytes.NewBuffer(data)) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", user.GetId()) + r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.PatchUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + It("updates attributes", func() { + user.SetUserType("Member") identityBackend.On("UpdateUser", mock.Anything, user.GetId(), mock.Anything).Return(user, nil) + user.SetUserType(("Member")) user.SetDisplayName("New Display Name") data, err := json.Marshal(user) Expect(err).ToNot(HaveOccurred()) @@ -788,6 +866,7 @@ var _ = Describe("Users", func() { updatedUser := libregraph.User{} err = json.Unmarshal(data, &updatedUser) Expect(err).ToNot(HaveOccurred()) + Expect(updatedUser.GetUserType()).To(Equal("Member")) Expect(updatedUser.GetDisplayName()).To(Equal("New Display Name")) }) })