graph: Allow disabling user by adding to local group

* A new config option for disabling users with the options "none", "attribute" and "group".
* When set to "none", there will be no enabledAttribute returned in user info and trying to change enabledAttribute will return an error
* Disable/enable group name DN as config parameter
* Adding/removing users to specified group on user update
* Changing log level for service initialization failure to error
* Adding helper methods to check if user is enabled/disabled + tests

Fixes #5554
This commit is contained in:
Daniel Swärd
2023-02-21 09:08:01 +01:00
parent e3d9b810bd
commit ca746106d8
6 changed files with 867 additions and 48 deletions

View File

@@ -60,7 +60,7 @@ func Server(cfg *config.Config) *cli.Command {
)
if err != nil {
logger.Info().Err(err).Str("transport", "http").Msg("Failed to initialize server")
logger.Error().Err(err).Str("transport", "http").Msg("Failed to initialize server")
return err
}

View File

@@ -62,6 +62,8 @@ type LDAP struct {
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."`
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. Valid 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'."`
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

@@ -69,6 +69,8 @@ func DefaultConfig() *config.Config {
// ideally this needs to be constant for the lifetime of a users
UserIDAttribute: "owncloudUUID",
UserEnabledAttribute: "ownCloudUserEnabled",
DisableUserMechanism: "attribute",
LdapDisabledUsersGroupDN: "cn=DisabledUsersGroup,ou=groups,o=libregraph-idm",
GroupBaseDN: "ou=groups,o=libregraph-idm",
GroupSearchScope: "sub",
GroupFilter: "",

View File

@@ -20,10 +20,29 @@ import (
)
const (
givenNameAttribute = "givenname"
surNameAttribute = "sn"
_givenNameAttribute = "givenname"
_surNameAttribute = "sn"
_ldapGroupOfNamesAttribute = "(objectClass=groupOfNames)"
_ldapGroupMemberAttribute = "member"
)
// DisableUserMechanismType is used instead of directly using the string values from the configuration.
type DisableUserMechanismType int64
// The different DisableMechanism* constants are used for managing the enabling/disabling of users.
const (
DisableMechanismNone DisableUserMechanismType = iota
DisableMechanismAttribute
DisableMechanismGroup
)
var mechanismMap = map[string]DisableUserMechanismType{
"": DisableMechanismNone,
"none": DisableMechanismNone,
"attribute": DisableMechanismAttribute,
"group": DisableMechanismGroup,
}
type LDAP struct {
useServerUUID bool
writeEnabled bool
@@ -36,6 +55,9 @@ type LDAP struct {
userScope int
userAttributeMap userAttributeMap
disableUserMechanism DisableUserMechanismType
localUserDisableGroupDN string
groupBaseDN string
groupFilter string
groupObjectClass string
@@ -60,6 +82,17 @@ type userAttributeMap struct {
type ldapAttributeValues map[string][]string
// ParseDisableMechanismType checks that the configuration option for how to disable users is correct.
func ParseDisableMechanismType(disableMechanism string) (DisableUserMechanismType, error) {
disableMechanism = strings.ToLower(disableMechanism)
t, ok := mechanismMap[disableMechanism]
if !ok {
return -1, errors.New("invalid configuration option for disable user mechanism")
}
return t, nil
}
func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LDAP, error) {
if config.UserDisplayNameAttribute == "" || config.UserIDAttribute == "" ||
config.UserEmailAttribute == "" || config.UserNameAttribute == "" {
@@ -71,8 +104,8 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LD
mail: config.UserEmailAttribute,
userName: config.UserNameAttribute,
accountEnabled: config.UserEnabledAttribute,
givenName: givenNameAttribute,
surname: surNameAttribute,
givenName: _givenNameAttribute,
surname: _surNameAttribute,
}
if config.GroupNameAttribute == "" || config.GroupIDAttribute == "" {
@@ -81,7 +114,7 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LD
gam := groupAttributeMap{
name: config.GroupNameAttribute,
id: config.GroupIDAttribute,
member: "member",
member: _ldapGroupMemberAttribute,
memberSyntax: "dn",
}
@@ -100,24 +133,31 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LD
return nil, fmt.Errorf("error setting up education resource config: %w", err)
}
disableMechanismType, err := ParseDisableMechanismType(config.DisableUserMechanism)
if err != nil {
return nil, fmt.Errorf("error configuring disable user mechanism: %w", err)
}
return &LDAP{
useServerUUID: config.UseServerUUID,
usePwModifyExOp: config.UsePasswordModExOp,
userBaseDN: config.UserBaseDN,
userFilter: config.UserFilter,
userObjectClass: config.UserObjectClass,
userScope: userScope,
userAttributeMap: uam,
groupBaseDN: config.GroupBaseDN,
groupFilter: config.GroupFilter,
groupObjectClass: config.GroupObjectClass,
groupScope: groupScope,
groupAttributeMap: gam,
educationConfig: educationConfig,
logger: logger,
conn: lc,
writeEnabled: config.WriteEnabled,
refintEnabled: config.RefintEnabled,
useServerUUID: config.UseServerUUID,
usePwModifyExOp: config.UsePasswordModExOp,
userBaseDN: config.UserBaseDN,
userFilter: config.UserFilter,
userObjectClass: config.UserObjectClass,
userScope: userScope,
userAttributeMap: uam,
groupBaseDN: config.GroupBaseDN,
groupFilter: config.GroupFilter,
groupObjectClass: config.GroupObjectClass,
groupScope: groupScope,
groupAttributeMap: gam,
educationConfig: educationConfig,
disableUserMechanism: disableMechanismType,
localUserDisableGroupDN: config.LdapDisabledUsersGroupDN,
logger: logger,
conn: lc,
writeEnabled: config.WriteEnabled,
refintEnabled: config.RefintEnabled,
}, nil
}
@@ -131,6 +171,10 @@ func (i *LDAP) CreateUser(ctx context.Context, user libregraph.User) (*libregrap
return nil, ErrReadOnly
}
if user.AccountEnabled != nil && i.disableUserMechanism == DisableMechanismNone {
return nil, errors.New("accountEnabled option not compatible with backend disable user mechanism")
}
ar, err := i.userToAddRequest(user)
if err != nil {
return nil, err
@@ -254,14 +298,34 @@ func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph.
}
}
// Behavior of enabling/disabling of users depends on the "disableUserMechanism" config option:
//
// "attribute": For the upstream user management service which modifies accountEnabled on the user entry
// "group": Makes it possible for local admins to disable users by adding them to a special group
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})
switch i.disableUserMechanism {
case DisableMechanismNone:
return nil, errors.New("accountEnabled option not compatible with backend disable user mechanism")
case DisableMechanismAttribute:
boolString := strings.ToUpper(strconv.FormatBool(user.GetAccountEnabled()))
ldapValue := e.GetEqualFoldAttributeValue(i.userAttributeMap.accountEnabled)
if ldapValue != "" {
mr.Replace(i.userAttributeMap.accountEnabled, []string{boolString})
} else {
mr.Add(i.userAttributeMap.accountEnabled, []string{boolString})
}
updateNeeded = true
case DisableMechanismGroup:
if user.GetAccountEnabled() {
err = i.enableUser(logger, e.DN)
} else {
err = i.disableUser(logger, e.DN)
}
if err != nil {
return nil, err
}
updateNeeded = true
}
}
@@ -276,7 +340,16 @@ func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph.
if err != nil {
return nil, err
}
return i.createUserModelFromLDAP(e), nil
returnUser := i.createUserModelFromLDAP(e)
// To avoid an ldap lookup for group membership, set the enabled flag to same as input value
// since this would have been updated with group membership from the input anyway.
if user.AccountEnabled != nil && i.disableUserMechanism != DisableMechanismNone {
returnUser.AccountEnabled = user.AccountEnabled
}
return returnUser, nil
}
func (i *LDAP) getUserByDN(dn string) (*ldap.Entry, error) {
@@ -320,7 +393,7 @@ func (i *LDAP) getEntryByDN(dn string, attrs []string, filter string) (*ldap.Ent
Msg("getEntryByDN")
res, err := i.conn.Search(searchRequest)
if err != nil {
i.logger.Error().Err(err).Str("backend", "ldap").Str("dn", dn).Msg("Search user by DN failed")
i.logger.Error().Err(err).Str("backend", "ldap").Str("dn", dn).Msg("Search ldap by DN failed")
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
}
if len(res.Entries) == 0 {
@@ -404,6 +477,13 @@ func (i *LDAP) GetUser(ctx context.Context, nameOrID string, oreq *godata.GoData
return nil, ErrNotFound
}
if i.disableUserMechanism != DisableMechanismNone {
userEnabled, err := i.UserEnabled(e)
if err == nil {
u.AccountEnabled = &userEnabled
}
}
exp, err := GetExpandValues(oreq.Query)
if err != nil {
return nil, err
@@ -472,6 +552,10 @@ func (i *LDAP) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*lib
}
users := make([]*libregraph.User, 0, len(res.Entries))
usersEnabledState, err := i.usersEnabledState(res.Entries)
if err != nil {
return nil, err
}
for _, e := range res.Entries {
u := i.createUserModelFromLDAP(e)
@@ -479,6 +563,12 @@ func (i *LDAP) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*lib
if u == nil {
continue
}
if i.disableUserMechanism != DisableMechanismNone {
userEnabled := usersEnabledState[e.DN]
u.AccountEnabled = &userEnabled
}
if slices.Contains(exp, "memberOf") {
userGroups, err := i.getGroupsForUser(e.DN)
if err != nil {
@@ -802,3 +892,135 @@ func replaceDN(fullDN *ldap.DN, newDN string) (string, error) {
return newDN, nil
}
// CreateLDAPGroupByDN is a helper method specifically intended for creating a "system" group
// for managing locally disabled users on service startup
func (i *LDAP) CreateLDAPGroupByDN(dn string) error {
ar := ldap.NewAddRequest(dn, nil)
attrs := map[string][]string{
"objectClass": {"groupOfNames", "top"},
"member": {""},
}
for attrType, values := range attrs {
ar.Attribute(attrType, values)
}
return i.conn.Add(ar)
}
func (i *LDAP) disableUser(logger log.Logger, userDN string) (err error) {
group, err := i.getEntryByDN(i.localUserDisableGroupDN, []string{_ldapGroupMemberAttribute}, _ldapGroupOfNamesAttribute)
if err != nil {
return err
}
mr := ldap.ModifyRequest{DN: group.DN}
mr.Add(_ldapGroupMemberAttribute, []string{userDN})
err = i.conn.Modify(&mr)
var lerr *ldap.Error
if errors.As(err, &lerr) {
// If the user is already in the group, just log a message and return
if lerr.ResultCode == ldap.LDAPResultAttributeOrValueExists {
logger.Info().Msg("User already in group for disabled users")
return nil
}
}
return err
}
func (i *LDAP) enableUser(logger log.Logger, userDN string) (err error) {
group, err := i.getEntryByDN(i.localUserDisableGroupDN, []string{_ldapGroupMemberAttribute}, _ldapGroupOfNamesAttribute)
if err != nil {
return err
}
mr := ldap.ModifyRequest{DN: group.DN}
mr.Delete(_ldapGroupMemberAttribute, []string{userDN})
err = i.conn.Modify(&mr)
var lerr *ldap.Error
if errors.As(err, &lerr) {
// If the user is not in the group, just log a message and return
if lerr.ResultCode == ldap.LDAPResultNoSuchAttribute {
logger.Info().Msg("User was not in group for disabled users")
return nil
}
}
return err
}
func (i *LDAP) userEnabledByAttribute(user *ldap.Entry) bool {
enabledAttribute := booleanOrNil(user.GetEqualFoldAttributeValue(i.userAttributeMap.accountEnabled))
if enabledAttribute == nil {
return true
}
return *enabledAttribute
}
func (i *LDAP) usersEnabledStateFromGroup(users []string) (usersEnabledState map[string]bool, err error) {
group, err := i.getEntryByDN(i.localUserDisableGroupDN, []string{_ldapGroupMemberAttribute}, _ldapGroupOfNamesAttribute)
if err != nil {
return nil, err
}
usersEnabledState = make(map[string]bool, len(users))
for _, user := range users {
usersEnabledState[user] = true
}
for _, memberDN := range group.GetEqualFoldAttributeValues(_ldapGroupMemberAttribute) {
usersEnabledState[memberDN] = false
}
return usersEnabledState, err
}
// UserEnabled returns if a user is enabled. This can depend on a flag in the user entry or group membership
func (i *LDAP) UserEnabled(user *ldap.Entry) (bool, error) {
usersEnabledState, err := i.usersEnabledState([]*ldap.Entry{user})
if err != nil {
return false, err
}
return usersEnabledState[user.DN], nil
}
func (i *LDAP) usersEnabledState(users []*ldap.Entry) (usersEnabledState map[string]bool, err error) {
usersEnabledState = make(map[string]bool, len(users))
keys := make([]string, len(users))
for index, user := range users {
usersEnabledState[user.DN] = true
keys[index] = user.DN
}
switch i.disableUserMechanism {
case DisableMechanismAttribute:
for _, user := range users {
usersEnabledState[user.DN] = i.userEnabledByAttribute(user)
}
case DisableMechanismGroup:
userDisabledGroupState, err := i.usersEnabledStateFromGroup(keys)
if err != nil {
return nil, err
}
for _, user := range keys {
usersEnabledState[user] = userDisabledGroupState[user]
}
}
return usersEnabledState, nil
}

View File

@@ -21,6 +21,11 @@ func getMockedBackend(l ldap.Client, lc config.LDAP, logger *log.Logger) (*LDAP,
return NewLDAPBackend(l, lc, logger)
}
const (
disableUsersGroup = "cn=DisabledUsersGroup,ou=groups,o=testing"
groupSearchFilter = "(objectClass=groupOfNames)"
)
var lconfig = config.LDAP{
UserBaseDN: "ou=people,dc=test",
UserObjectClass: "inetOrgPerson",
@@ -31,6 +36,8 @@ var lconfig = config.LDAP{
UserEmailAttribute: "mail",
UserNameAttribute: "uid",
UserEnabledAttribute: "userEnabledAttribute",
LdapDisabledUsersGroupDN: disableUsersGroup,
DisableUserMechanism: "attribute",
GroupBaseDN: "ou=groups,dc=test",
GroupObjectClass: "groupOfNames",
@@ -285,16 +292,20 @@ func TestGetUsers(t *testing.T) {
}
func TestUpdateUser(t *testing.T) {
falseBool := false
trueBool := true
type userProps struct {
id string
mail string
displayName string
onPremisesSamAccountName string
accountEnabled bool
accountEnabled *bool
}
type args struct {
nameOrID string
userProps userProps
nameOrID string
userProps userProps
disableUserMechanism string
}
type mockInputs struct {
funcName string
@@ -359,7 +370,7 @@ func TestUpdateUser(t *testing.T) {
mail: "testuser@example.org",
displayName: "testUser",
onPremisesSamAccountName: "testUser",
accountEnabled: true,
accountEnabled: nil,
},
assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
return assert.Nil(t, err, args...)
@@ -397,7 +408,7 @@ func TestUpdateUser(t *testing.T) {
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
Values: []string{""},
},
},
},
@@ -445,7 +456,7 @@ func TestUpdateUser(t *testing.T) {
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
Values: []string{""},
},
},
},
@@ -488,7 +499,7 @@ func TestUpdateUser(t *testing.T) {
mail: "testuser@example.org",
displayName: "newName",
onPremisesSamAccountName: "testUser",
accountEnabled: true,
accountEnabled: nil,
},
assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
return assert.Nil(t, err, args...)
@@ -526,7 +537,7 @@ func TestUpdateUser(t *testing.T) {
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
Values: []string{""},
},
},
},
@@ -574,7 +585,7 @@ func TestUpdateUser(t *testing.T) {
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
Values: []string{""},
},
},
},
@@ -617,7 +628,7 @@ func TestUpdateUser(t *testing.T) {
mail: "testuser@example.org",
displayName: "newName",
onPremisesSamAccountName: "newName",
accountEnabled: true,
accountEnabled: nil,
},
assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
return assert.Nil(t, err, args...)
@@ -659,7 +670,7 @@ func TestUpdateUser(t *testing.T) {
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
Values: []string{""},
},
},
},
@@ -755,7 +766,7 @@ func TestUpdateUser(t *testing.T) {
},
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
Values: []string{""},
},
},
},
@@ -797,15 +808,16 @@ func TestUpdateUser(t *testing.T) {
args: args{
nameOrID: "testUser",
userProps: userProps{
accountEnabled: false,
accountEnabled: &falseBool,
},
disableUserMechanism: "attribute",
},
want: &userProps{
id: "testUser",
mail: "testuser@example.org",
displayName: "testUser",
onPremisesSamAccountName: "testUser",
accountEnabled: false,
accountEnabled: &falseBool,
},
assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
return assert.Nil(t, err, args...)
@@ -921,6 +933,338 @@ func TestUpdateUser(t *testing.T) {
},
},
},
{
name: "Test disabling user as local admin",
args: args{
nameOrID: "testUser",
userProps: userProps{
accountEnabled: &falseBool,
},
disableUserMechanism: "group",
},
want: &userProps{
id: "testUser",
mail: "testuser@example.org",
displayName: "testUser",
onPremisesSamAccountName: "testUser",
accountEnabled: &falseBool,
},
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: disableUsersGroup,
Scope: 0,
DerefAliases: 0,
SizeLimit: 1,
TimeLimit: 0,
TypesOnly: false,
Filter: groupSearchFilter,
Attributes: []string{"member"},
Controls: []ldap.Control(nil),
},
},
returns: []interface{}{
&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "uid=name",
Attributes: []*ldap.EntryAttribute{
{
Name: "member",
Values: []string{""},
},
},
},
},
},
nil,
},
},
{
funcName: "Modify",
args: []interface{}{
&ldap.ModifyRequest{
DN: "uid=name",
Changes: []ldap.Change{
{
Operation: ldap.AddAttribute,
Modification: ldap.PartialAttribute{
Type: "member",
Vals: []string{"uid=name"},
},
},
},
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"},
[]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"},
},
},
},
},
},
nil,
},
},
},
},
{
name: "Test enabling user as local admin",
args: args{
nameOrID: "testUser",
userProps: userProps{
accountEnabled: &trueBool,
},
disableUserMechanism: "group",
},
want: &userProps{
id: "testUser",
mail: "testuser@example.org",
displayName: "testUser",
onPremisesSamAccountName: "testUser",
accountEnabled: &trueBool,
},
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: disableUsersGroup,
Scope: 0,
DerefAliases: 0,
SizeLimit: 1,
TimeLimit: 0,
TypesOnly: false,
Filter: groupSearchFilter,
Attributes: []string{"member"},
Controls: []ldap.Control(nil),
},
},
returns: []interface{}{
&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "uid=name",
Attributes: []*ldap.EntryAttribute{
{
Name: "member",
Values: []string{""},
},
},
},
},
},
nil,
},
},
{
funcName: "Modify",
args: []interface{}{
&ldap.ModifyRequest{
DN: "uid=name",
Changes: []ldap.Change{
{
Operation: ldap.DeleteAttribute,
Modification: ldap.PartialAttribute{
Type: "member",
Vals: []string{"uid=name"},
},
},
},
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"},
[]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"},
},
},
},
},
},
nil,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -928,13 +1272,17 @@ func TestUpdateUser(t *testing.T) {
for _, mock := range tt.ldapMocks {
lm.On(mock.funcName, mock.args...).Return(mock.returns...)
}
i, _ := getMockedBackend(lm, lconfig, &logger)
ldapConfig := lconfig
ldapConfig.DisableUserMechanism = tt.args.disableUserMechanism
i, _ := getMockedBackend(lm, ldapConfig, &logger)
user := libregraph.User{
Id: &tt.args.userProps.id,
Mail: &tt.args.userProps.mail,
DisplayName: &tt.args.userProps.displayName,
OnPremisesSamAccountName: &tt.args.userProps.onPremisesSamAccountName,
AccountEnabled: tt.args.userProps.accountEnabled,
}
emptyString := ""
@@ -947,7 +1295,10 @@ func TestUpdateUser(t *testing.T) {
OnPremisesSamAccountName: &tt.want.onPremisesSamAccountName,
Surname: &emptyString,
GivenName: &emptyString,
AccountEnabled: &tt.want.accountEnabled,
}
if tt.want.accountEnabled != nil {
want.AccountEnabled = *&tt.want.accountEnabled
}
}
@@ -957,3 +1308,214 @@ func TestUpdateUser(t *testing.T) {
})
}
}
func TestUsersEnabledState(t *testing.T) {
aliceEnabled := ldap.Entry{
DN: "alice",
Attributes: []*ldap.EntryAttribute{
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"TRUE"},
},
},
}
bobDisabled := ldap.Entry{
DN: "bob",
Attributes: []*ldap.EntryAttribute{
{
Name: lconfig.UserEnabledAttribute,
Values: []string{"FALSE"},
},
},
}
carolImplicitlyEnabled := ldap.Entry{
DN: "carol",
Attributes: []*ldap.EntryAttribute{
{
Name: lconfig.UserEnabledAttribute,
},
},
}
type args struct {
usersToCheck []*ldap.Entry
expectedUsers map[string]bool
disableUserMechanism string
}
type mockInputs struct {
funcName string
args []interface{}
returns []interface{}
}
tests := []struct {
name string
args args
want map[string]bool
assertion assert.ErrorAssertionFunc
ldapMocks []mockInputs
}{
{
name: "Test no users",
args: args{
usersToCheck: []*ldap.Entry{},
expectedUsers: map[string]bool{},
disableUserMechanism: "attribute",
},
want: map[string]bool{},
assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
return assert.Nil(t, err, args...)
},
ldapMocks: []mockInputs{},
},
{
name: "Test attribute enabled users",
args: args{
usersToCheck: []*ldap.Entry{&aliceEnabled, &bobDisabled, &carolImplicitlyEnabled},
expectedUsers: map[string]bool{"alice": true, "bob": false, "carol": true},
disableUserMechanism: "attribute",
},
want: map[string]bool{"alice": true, "bob": false, "carol": true},
assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
return assert.Nil(t, err, args...)
},
ldapMocks: []mockInputs{},
},
{
name: "Test attribute enabled users not in disabled group",
args: args{
usersToCheck: []*ldap.Entry{&aliceEnabled, &bobDisabled, &carolImplicitlyEnabled},
expectedUsers: map[string]bool{"alice": true, "bob": false, "carol": true},
disableUserMechanism: "group",
},
want: map[string]bool{"alice": true, "bob": true, "carol": true},
assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
return assert.Nil(t, err, args...)
},
ldapMocks: []mockInputs{
{
funcName: "Search",
args: []interface{}{
ldap.NewSearchRequest(
disableUsersGroup,
ldap.ScopeBaseObject,
ldap.NeverDerefAliases, 1, 0, false,
groupSearchFilter,
[]string{"member"},
[]ldap.Control(nil),
),
},
returns: []interface{}{
&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "cn=DisabledGroup",
Attributes: []*ldap.EntryAttribute{
{
Name: "member",
Values: []string{""},
},
},
},
},
},
nil,
},
},
},
},
{
name: "Test attribute enabled users in disabled group",
args: args{
usersToCheck: []*ldap.Entry{&aliceEnabled, &bobDisabled, &carolImplicitlyEnabled},
expectedUsers: map[string]bool{"alice": true, "bob": false, "carol": true},
disableUserMechanism: "group",
},
want: map[string]bool{"alice": false, "bob": true, "carol": 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(
disableUsersGroup,
ldap.ScopeBaseObject,
ldap.NeverDerefAliases, 1, 0, false,
groupSearchFilter,
[]string{"member"},
[]ldap.Control(nil),
),
},
returns: []interface{}{
&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "cn=DisabledGroup",
Attributes: []*ldap.EntryAttribute{
{
Name: "member",
Values: []string{"alice", "carol"},
},
},
},
},
},
nil,
},
},
},
},
{
name: "Test local group disable when ldap is throwing error",
args: args{
usersToCheck: []*ldap.Entry{&aliceEnabled, &bobDisabled, &carolImplicitlyEnabled},
expectedUsers: map[string]bool{"alice": true, "bob": false, "carol": true},
disableUserMechanism: "group",
},
want: nil,
assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
return assert.NotNil(t, err, args...)
},
ldapMocks: []mockInputs{
{
funcName: "Search",
args: []interface{}{
ldap.NewSearchRequest(
disableUsersGroup,
ldap.ScopeBaseObject,
ldap.NeverDerefAliases, 1, 0, false,
groupSearchFilter,
[]string{"member"},
[]ldap.Control(nil),
),
},
returns: []interface{}{
nil,
&ldap.Error{
Err: fmt.Errorf("Very Problematic Problems"),
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lm := &mocks.Client{}
for _, mock := range tt.ldapMocks {
lm.On(mock.funcName, mock.args...).Return(mock.returns...)
}
ldapConfig := lconfig
ldapConfig.DisableUserMechanism = tt.args.disableUserMechanism
i, _ := getMockedBackend(lm, ldapConfig, &logger)
got, err := i.usersEnabledState(tt.args.usersToCheck)
tt.assertion(t, err)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -3,6 +3,7 @@ package svc
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net/http"
"os"
@@ -12,6 +13,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
ldapv3 "github.com/go-ldap/ldap/v3"
"github.com/jellydator/ttlcache/v3"
libregraph "github.com/owncloud/libre-graph-api-go"
ocisldap "github.com/owncloud/ocis/v2/ocis-pkg/ldap"
@@ -366,6 +368,35 @@ func setIdentityBackends(options Options, svc *Graph) error {
svc.identityEducationBackend = errEduBackend
}
}
disableMechanismType, err := identity.ParseDisableMechanismType(options.Config.Identity.LDAP.DisableUserMechanism)
if err != nil {
options.Logger.Error().Err(err).Msg("Error initializing LDAP Backend")
return err
}
if disableMechanismType == identity.DisableMechanismGroup {
options.Logger.Info().Msg("LocalUserDisable is true, will create group if not exists")
err := lb.CreateLDAPGroupByDN(options.Config.Identity.LDAP.LdapDisabledUsersGroupDN)
if err != nil {
isAnError := false
var lerr *ldapv3.Error
if errors.As(err, &lerr) {
if lerr.ResultCode != ldapv3.LDAPResultEntryAlreadyExists {
isAnError = true
}
} else {
isAnError = true
}
if isAnError {
msg := "error adding group for disabling users"
options.Logger.Error().Err(err).Str("local_user_disable", options.Config.Identity.LDAP.LdapDisabledUsersGroupDN).Msg(msg)
return err
}
}
}
default:
err := fmt.Errorf("unknown identity backend: '%s'", options.Config.Identity.Backend)
options.Logger.Err(err)