graph: Add accountEnabled flag to ldap backend. (#5588)

* graph: Add accountEnabled flag to ldap backend.

* Add missing accountEnabled attribute to user listing.
This commit is contained in:
Daniel Swärd
2023-02-17 13:48:12 +01:00
committed by GitHub
parent 359509dcfe
commit fcf5783a1b
7 changed files with 234 additions and 54 deletions

View File

@@ -60,6 +60,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."`
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."`
GroupBaseDN string `yaml:"group_base_dn" env:"LDAP_GROUP_BASE_DN;GRAPH_LDAP_GROUP_BASE_DN" desc:"Search base DN for looking up LDAP groups."`
GroupSearchScope string `yaml:"group_search_scope" env:"LDAP_GROUP_SCOPE;GRAPH_LDAP_GROUP_SEARCH_SCOPE" desc:"LDAP search scope to use when looking up groups. Supported scopes are 'base', 'one' and 'sub'."`

View File

@@ -68,6 +68,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",
UserEnabledAttribute: "ownCloudUserEnabled",
GroupBaseDN: "ou=groups,o=libregraph-idm",
GroupSearchScope: "sub",
GroupFilter: "",

View File

@@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"github.com/CiscoM31/godata"
"github.com/go-ldap/ldap/v3"
@@ -46,12 +48,13 @@ type LDAP struct {
}
type userAttributeMap struct {
displayName string
id string
mail string
userName string
givenName string
surname string
displayName string
id string
mail string
userName string
givenName string
surname string
accountEnabled string
}
type ldapAttributeValues map[string][]string
@@ -62,12 +65,13 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LD
return nil, errors.New("invalid user attribute mappings")
}
uam := userAttributeMap{
displayName: config.UserDisplayNameAttribute,
id: config.UserIDAttribute,
mail: config.UserEmailAttribute,
userName: config.UserNameAttribute,
givenName: givenNameAttribute,
surname: surNameAttribute,
displayName: config.UserDisplayNameAttribute,
id: config.UserIDAttribute,
mail: config.UserEmailAttribute,
userName: config.UserNameAttribute,
accountEnabled: config.UserEnabledAttribute,
givenName: givenNameAttribute,
surname: surNameAttribute,
}
if config.GroupNameAttribute == "" || config.GroupIDAttribute == "" {
@@ -245,7 +249,17 @@ func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph.
updateNeeded = true
}
}
// TODO implement account disabled/enabled
if user.AccountEnabled != nil {
boolString := strings.ToUpper(strconv.FormatBool(*user.AccountEnabled))
ldapValue := e.GetEqualFoldAttributeValue(i.userAttributeMap.accountEnabled)
updateNeeded = true
if ldapValue != "" {
mr.Replace(i.userAttributeMap.accountEnabled, []string{boolString})
} else {
mr.Add(i.userAttributeMap.accountEnabled, []string{boolString})
}
}
if updateNeeded {
if err := i.conn.Modify(&mr); err != nil {
@@ -269,6 +283,7 @@ func (i *LDAP) getUserByDN(dn string) (*ldap.Entry, error) {
i.userAttributeMap.userName,
i.userAttributeMap.surname,
i.userAttributeMap.givenName,
i.userAttributeMap.accountEnabled,
}
filter := fmt.Sprintf("(objectClass=%s)", i.userObjectClass)
@@ -365,6 +380,7 @@ func (i *LDAP) getLDAPUserByFilter(filter string) (*ldap.Entry, error) {
i.userAttributeMap.userName,
i.userAttributeMap.surname,
i.userAttributeMap.givenName,
i.userAttributeMap.accountEnabled,
}
return i.searchLDAPEntryByFilter(i.userBaseDN, attrs, filter)
}
@@ -435,6 +451,7 @@ func (i *LDAP) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*lib
i.userAttributeMap.userName,
i.userAttributeMap.surname,
i.userAttributeMap.givenName,
i.userAttributeMap.accountEnabled,
},
nil,
)
@@ -581,6 +598,7 @@ func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User {
Id: &id,
GivenName: &givenName,
Surname: &surname,
AccountEnabled: booleanOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.accountEnabled)),
}
}
i.logger.Warn().Str("dn", e.DN).Msg("Invalid User. Missing username or id attribute")
@@ -592,13 +610,18 @@ func (i *LDAP) userToLDAPAttrValues(user libregraph.User) (map[string][]string,
i.userAttributeMap.displayName: {user.GetDisplayName()},
i.userAttributeMap.userName: {user.GetOnPremisesSamAccountName()},
i.userAttributeMap.mail: {user.GetMail()},
"objectClass": {"inetOrgPerson", "organizationalPerson", "person", "top"},
"objectClass": {"inetOrgPerson", "organizationalPerson", "person", "top", "ownCloudUser"},
"cn": {user.GetOnPremisesSamAccountName()},
}
if !i.useServerUUID {
attrs["owncloudUUID"] = []string{uuid.Must(uuid.NewV4()).String()}
attrs["objectClass"] = append(attrs["objectClass"], "owncloud")
}
if user.AccountEnabled != nil {
attrs[i.userAttributeMap.accountEnabled] = []string{
strings.ToUpper(strconv.FormatBool(*user.AccountEnabled)),
}
}
// inetOrgPerson requires "sn" to be set. Set it to the Username if
@@ -636,6 +659,7 @@ func (i *LDAP) getUserAttrTypes() []string {
"cn",
"owncloudUUID",
"userPassword",
i.userAttributeMap.accountEnabled,
}
}
@@ -666,6 +690,16 @@ func pointerOrNil(val string) *string {
return &val
}
func booleanOrNil(val string) *bool {
boolValue, err := strconv.ParseBool(val)
if err != nil {
return nil
}
return &boolValue
}
func stringToScope(scope string) (int, error) {
var s int
switch scope {

View File

@@ -275,7 +275,7 @@ func TestGetEducationClassMembers(t *testing.T) {
Scope: 0,
SizeLimit: 1,
Filter: "(objectClass=inetOrgPerson)",
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"},
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
Controls: []ldap.Control(nil),
}
lm.On("Search", user_sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil)

View File

@@ -21,6 +21,7 @@ var eduConfig = config.LDAP{
UserIDAttribute: "entryUUID",
UserEmailAttribute: "mail",
UserNameAttribute: "uid",
UserEnabledAttribute: "userEnabledAttribute",
GroupBaseDN: "ou=groups,dc=test",
GroupObjectClass: "groupOfNames",

View File

@@ -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"},
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
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"},
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
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"},
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
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"},
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
Controls: []ldap.Control(nil),
}

View File

@@ -30,6 +30,7 @@ var lconfig = config.LDAP{
UserIDAttribute: "entryUUID",
UserEmailAttribute: "mail",
UserNameAttribute: "uid",
UserEnabledAttribute: "userEnabledAttribute",
GroupBaseDN: "ou=groups,dc=test",
GroupObjectClass: "groupOfNames",
@@ -43,12 +44,13 @@ var lconfig = config.LDAP{
var userEntry = ldap.NewEntry("uid=user",
map[string][]string{
"uid": {"user"},
"displayname": {"DisplayName"},
"mail": {"user@example"},
"entryuuid": {"abcd-defg"},
"sn": {"surname"},
"givenname": {"givenName"},
"uid": {"user"},
"displayname": {"DisplayName"},
"mail": {"user@example"},
"entryuuid": {"abcd-defg"},
"sn": {"surname"},
"givenname": {"givenName"},
"userenabledattribute": {"TRUE"},
})
var invalidUserEntry = ldap.NewEntry("uid=user",
@@ -105,8 +107,9 @@ func TestCreateUser(t *testing.T) {
ar.Attribute(lconfig.UserEmailAttribute, []string{mail})
ar.Attribute("sn", []string{surname})
ar.Attribute("givenname", []string{givenName})
ar.Attribute("objectClass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"})
ar.Attribute("objectClass", []string{"inetOrgPerson", "organizationalPerson", "person", "top", "ownCloudUser"})
ar.Attribute("cn", []string{userName})
ar.Attribute(lconfig.UserEnabledAttribute, []string{"TRUE"})
l := &mocks.Client{}
l.On("Search", mock.Anything).
@@ -124,6 +127,7 @@ func TestCreateUser(t *testing.T) {
user.SetOnPremisesSamAccountName(userName)
user.SetSurname(surname)
user.SetGivenName(givenName)
user.SetAccountEnabled(true)
c := lconfig
c.UseServerUUID = true
@@ -136,6 +140,7 @@ func TestCreateUser(t *testing.T) {
assert.Equal(t, userName, newUser.GetOnPremisesSamAccountName())
assert.Equal(t, givenName, newUser.GetGivenName())
assert.Equal(t, surname, newUser.GetSurname())
assert.True(t, newUser.GetAccountEnabled())
}
func TestCreateUserModelFromLDAP(t *testing.T) {
@@ -285,6 +290,7 @@ func TestUpdateUser(t *testing.T) {
mail string
displayName string
onPremisesSamAccountName string
accountEnabled bool
}
type args struct {
nameOrID string
@@ -323,7 +329,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"},
[]string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
nil,
),
},
@@ -353,6 +359,7 @@ func TestUpdateUser(t *testing.T) {
mail: "testuser@example.org",
displayName: "testUser",
onPremisesSamAccountName: "testUser",
accountEnabled: true,
},
assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
return assert.Nil(t, err, args...)
@@ -366,7 +373,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"},
[]string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
nil,
),
},
@@ -380,24 +387,18 @@ func TestUpdateUser(t *testing.T) {
Name: "displayname",
Values: []string{"oldmail@example.org"},
},
},
},
{
DN: "uid=oldName",
Attributes: []*ldap.EntryAttribute{
{
Name: "entryUUID",
Values: []string{"testUser"},
},
},
},
{
DN: "uid=oldName",
Attributes: []*ldap.EntryAttribute{
{
Name: "mail",
Values: []string{"oldmail@example.org"},
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
},
},
},
},
@@ -416,7 +417,7 @@ func TestUpdateUser(t *testing.T) {
TimeLimit: 0,
TypesOnly: false,
Filter: "(objectClass=inetOrgPerson)",
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"},
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
Controls: []ldap.Control(nil),
},
},
@@ -442,6 +443,10 @@ func TestUpdateUser(t *testing.T) {
Name: lconfig.UserNameAttribute,
Values: []string{"testUser"},
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
},
},
},
},
@@ -483,6 +488,7 @@ func TestUpdateUser(t *testing.T) {
mail: "testuser@example.org",
displayName: "newName",
onPremisesSamAccountName: "testUser",
accountEnabled: true,
},
assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
return assert.Nil(t, err, args...)
@@ -496,7 +502,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"},
[]string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
nil,
),
},
@@ -510,24 +516,18 @@ func TestUpdateUser(t *testing.T) {
Name: "displayname",
Values: []string{"testUser"},
},
},
},
{
DN: "uid=oldName",
Attributes: []*ldap.EntryAttribute{
{
Name: "entryUUID",
Values: []string{"testUser"},
},
},
},
{
DN: "uid=oldName",
Attributes: []*ldap.EntryAttribute{
{
Name: "mail",
Values: []string{"testuser@example.org"},
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
},
},
},
},
@@ -546,7 +546,7 @@ func TestUpdateUser(t *testing.T) {
TimeLimit: 0,
TypesOnly: false,
Filter: "(objectClass=inetOrgPerson)",
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"},
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
Controls: []ldap.Control(nil),
},
},
@@ -572,6 +572,10 @@ func TestUpdateUser(t *testing.T) {
Name: lconfig.UserNameAttribute,
Values: []string{"testUser"},
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
},
},
},
},
@@ -613,6 +617,7 @@ func TestUpdateUser(t *testing.T) {
mail: "testuser@example.org",
displayName: "newName",
onPremisesSamAccountName: "newName",
accountEnabled: true,
},
assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
return assert.Nil(t, err, args...)
@@ -626,7 +631,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"},
[]string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
nil,
),
},
@@ -652,6 +657,10 @@ func TestUpdateUser(t *testing.T) {
Name: lconfig.UserNameAttribute,
Values: []string{"oldName"},
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
},
},
},
},
@@ -718,7 +727,7 @@ func TestUpdateUser(t *testing.T) {
TimeLimit: 0,
TypesOnly: false,
Filter: "(objectClass=inetOrgPerson)",
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname"},
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
Controls: []ldap.Control(nil),
},
},
@@ -744,6 +753,10 @@ func TestUpdateUser(t *testing.T) {
Name: lconfig.UserNameAttribute,
Values: []string{"newName"},
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
},
},
},
},
@@ -779,6 +792,135 @@ func TestUpdateUser(t *testing.T) {
},
},
},
{
name: "Test changing accountEnabled",
args: args{
nameOrID: "testUser",
userProps: userProps{
accountEnabled: false,
},
},
want: &userProps{
id: "testUser",
mail: "testuser@example.org",
displayName: "testUser",
onPremisesSamAccountName: "testUser",
accountEnabled: false,
},
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"},
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.UserEnabledAttribute,
Values: []string{"TRUE"},
},
},
},
},
},
nil,
},
},
{
funcName: "Search",
args: []interface{}{
&ldap.SearchRequest{
BaseDN: "uid=name",
Scope: 0,
DerefAliases: 0,
SizeLimit: 1,
TimeLimit: 0,
TypesOnly: false,
Filter: "(objectClass=inetOrgPerson)",
Attributes: []string{"displayname", "entryUUID", "mail", "uid", "sn", "givenname", "userEnabledAttribute"},
Controls: []ldap.Control(nil),
},
},
returns: []interface{}{
&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "uid=name",
Attributes: []*ldap.EntryAttribute{
{
Name: lconfig.UserIDAttribute,
Values: []string{"testUser"},
},
{
Name: lconfig.UserEmailAttribute,
Values: []string{"testuser@example.org"},
},
{
Name: lconfig.UserDisplayNameAttribute,
Values: []string{"testUser"},
},
{
Name: lconfig.UserNameAttribute,
Values: []string{"testUser"},
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"FALSE"},
},
},
},
},
},
nil,
},
},
{
funcName: "Modify",
args: []interface{}{
&ldap.ModifyRequest{
DN: "uid=name",
Changes: []ldap.Change{
{
Operation: 0x2,
Modification: ldap.PartialAttribute{
Type: lconfig.UserEnabledAttribute,
Vals: []string{"FALSE"},
},
},
},
Controls: []ldap.Control(nil),
},
},
returns: []interface{}{nil},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -805,6 +947,7 @@ func TestUpdateUser(t *testing.T) {
OnPremisesSamAccountName: &tt.want.onPremisesSamAccountName,
Surname: &emptyString,
GivenName: &emptyString,
AccountEnabled: &tt.want.accountEnabled,
}
}