mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-05 11:51:16 -06:00
By setting GRAPH_LDAP_GROUP_CREATE_BASE_DN a distinct subtree can be configured where new LDAP groups are created. That subtree needs to be subordinate to GRAPH_LDAP_GROUP_BASE_DN. All groups outside for GRAPH_LDAP_GROUP_CREATE_BASE_DN are considered read-only and only groups below that DN can be updated and deleted. This is introduced for a pretty specific usecase where most groups are managed in an external source (e.g. a read-only replica of an LDAP tree). But we still want to allow the local administrator to create groups in a writeable subtree attached to that replica.
1063 lines
31 KiB
Go
1063 lines
31 KiB
Go
package identity
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/CiscoM31/godata"
|
|
"github.com/go-ldap/ldap/v3"
|
|
"github.com/gofrs/uuid"
|
|
"github.com/libregraph/idm/pkg/ldapdn"
|
|
libregraph "github.com/owncloud/libre-graph-api-go"
|
|
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
|
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
|
|
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
const (
|
|
_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
|
|
refintEnabled bool
|
|
usePwModifyExOp bool
|
|
|
|
userBaseDN string
|
|
userFilter string
|
|
userObjectClass string
|
|
userScope int
|
|
userAttributeMap userAttributeMap
|
|
|
|
disableUserMechanism DisableUserMechanismType
|
|
localUserDisableGroupDN string
|
|
|
|
groupBaseDN string
|
|
groupCreateBaseDN string
|
|
groupFilter string
|
|
groupObjectClass string
|
|
groupScope int
|
|
groupAttributeMap groupAttributeMap
|
|
|
|
educationConfig educationConfig
|
|
|
|
logger *log.Logger
|
|
conn ldap.Client
|
|
}
|
|
|
|
type userAttributeMap struct {
|
|
displayName string
|
|
id string
|
|
mail string
|
|
userName string
|
|
givenName string
|
|
surname string
|
|
accountEnabled string
|
|
userType string
|
|
}
|
|
|
|
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 == "" {
|
|
return nil, errors.New("invalid user attribute mappings")
|
|
}
|
|
uam := userAttributeMap{
|
|
displayName: config.UserDisplayNameAttribute,
|
|
id: config.UserIDAttribute,
|
|
mail: config.UserEmailAttribute,
|
|
userName: config.UserNameAttribute,
|
|
accountEnabled: config.UserEnabledAttribute,
|
|
givenName: _givenNameAttribute,
|
|
surname: _surNameAttribute,
|
|
userType: config.UserTypeAttribute,
|
|
}
|
|
|
|
if config.GroupNameAttribute == "" || config.GroupIDAttribute == "" {
|
|
return nil, errors.New("invalid group attribute mappings")
|
|
}
|
|
gam := groupAttributeMap{
|
|
name: config.GroupNameAttribute,
|
|
id: config.GroupIDAttribute,
|
|
member: _ldapGroupMemberAttribute,
|
|
memberSyntax: "dn",
|
|
}
|
|
|
|
var userScope, groupScope int
|
|
var err error
|
|
if userScope, err = stringToScope(config.UserSearchScope); err != nil {
|
|
return nil, fmt.Errorf("error configuring user scope: %w", err)
|
|
}
|
|
|
|
if groupScope, err = stringToScope(config.GroupSearchScope); err != nil {
|
|
return nil, fmt.Errorf("error configuring group scope: %w", err)
|
|
}
|
|
|
|
var educationConfig educationConfig
|
|
if educationConfig, err = newEducationConfig(config); err != nil {
|
|
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,
|
|
groupCreateBaseDN: config.GroupCreateBaseDN,
|
|
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
|
|
}
|
|
|
|
// CreateUser implements the Backend Interface. It converts the libregraph.User into an
|
|
// LDAP User Entry (using the inetOrgPerson LDAP Objectclass) add adds that to the
|
|
// configured LDAP server
|
|
func (i *LDAP) CreateUser(ctx context.Context, user libregraph.User) (*libregraph.User, error) {
|
|
logger := i.logger.SubloggerWithRequestID(ctx)
|
|
logger.Debug().Str("backend", "ldap").Msg("CreateUser")
|
|
if !i.writeEnabled {
|
|
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
|
|
}
|
|
|
|
if err := i.conn.Add(ar); err != nil {
|
|
var lerr *ldap.Error
|
|
logger.Debug().Err(err).Msg("error adding user")
|
|
if errors.As(err, &lerr) {
|
|
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
|
|
err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if i.usePwModifyExOp && user.PasswordProfile != nil && user.PasswordProfile.Password != nil {
|
|
if err := i.updateUserPassowrd(ctx, ar.DN, user.PasswordProfile.GetPassword()); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Read back user from LDAP to get the generated UUID
|
|
e, err := i.getUserByDN(ar.DN)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return i.createUserModelFromLDAP(e), nil
|
|
}
|
|
|
|
// DeleteUser implements the Backend Interface. It permanently deletes a User identified
|
|
// by name or id from the LDAP server
|
|
func (i *LDAP) DeleteUser(ctx context.Context, nameOrID string) error {
|
|
logger := i.logger.SubloggerWithRequestID(ctx)
|
|
logger.Debug().Str("backend", "ldap").Msg("DeleteUser")
|
|
if !i.writeEnabled {
|
|
return ErrReadOnly
|
|
}
|
|
e, err := i.getLDAPUserByNameOrID(nameOrID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dr := ldap.DelRequest{DN: e.DN}
|
|
if err = i.conn.Del(&dr); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !i.refintEnabled {
|
|
// Find all the groups that this user was a member of and remove it from there
|
|
groupEntries, err := i.getLDAPGroupsByFilter(fmt.Sprintf("(%s=%s)", i.groupAttributeMap.member, e.DN), true, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, group := range groupEntries {
|
|
logger.Debug().Str("group", group.DN).Str("user", e.DN).Msg("Cleaning up group membership")
|
|
|
|
if mr, err := i.removeEntryByDNAndAttributeFromEntry(group, e.DN, i.groupAttributeMap.member); err == nil {
|
|
if err = i.conn.Modify(mr); err != nil {
|
|
// Errors when deleting the memberships are only logged as warnings but not returned
|
|
// to the user as we already successfully deleted the users itself
|
|
logger.Warn().Str("group", group.DN).Str("user", e.DN).Err(err).Msg("failed to remove member")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateUser implements the Backend Interface for the LDAP Backend
|
|
func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph.User) (*libregraph.User, error) {
|
|
logger := i.logger.SubloggerWithRequestID(ctx)
|
|
logger.Debug().Str("backend", "ldap").Msg("UpdateUser")
|
|
if !i.writeEnabled {
|
|
return nil, ErrReadOnly
|
|
}
|
|
e, err := i.getLDAPUserByNameOrID(nameOrID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var updateNeeded bool
|
|
|
|
// Don't allow updates of the ID
|
|
if user.GetId() != "" {
|
|
if e.GetEqualFoldAttributeValue(i.userAttributeMap.id) != user.GetId() {
|
|
return nil, errorcode.New(errorcode.NotAllowed, "changing the UserId is not allowed")
|
|
}
|
|
}
|
|
if user.GetOnPremisesSamAccountName() != "" {
|
|
if eu := e.GetEqualFoldAttributeValue(i.userAttributeMap.userName); eu != user.GetOnPremisesSamAccountName() {
|
|
e, err = i.changeUserName(ctx, e.DN, eu, user.GetOnPremisesSamAccountName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
mr := ldap.ModifyRequest{DN: e.DN}
|
|
properties := map[string]string{
|
|
i.userAttributeMap.displayName: user.GetDisplayName(),
|
|
i.userAttributeMap.mail: user.GetMail(),
|
|
i.userAttributeMap.surname: user.GetSurname(),
|
|
i.userAttributeMap.givenName: user.GetGivenName(),
|
|
i.userAttributeMap.userType: user.GetUserType(),
|
|
}
|
|
|
|
for attribute, value := range properties {
|
|
if value != "" {
|
|
if e.GetEqualFoldAttributeValue(attribute) != value {
|
|
mr.Replace(attribute, []string{value})
|
|
updateNeeded = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if user.PasswordProfile != nil && user.PasswordProfile.GetPassword() != "" {
|
|
if i.usePwModifyExOp {
|
|
if err := i.updateUserPassowrd(ctx, e.DN, user.PasswordProfile.GetPassword()); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// password are hashed server side there is no need to check if the new password
|
|
// is actually different from the old one.
|
|
mr.Replace("userPassword", []string{user.PasswordProfile.GetPassword()})
|
|
updateNeeded = true
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
un, err := i.updateAccountEnabledState(logger, user.GetAccountEnabled(), e, &mr)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if un {
|
|
updateNeeded = true
|
|
}
|
|
}
|
|
|
|
if updateNeeded {
|
|
if err := i.conn.Modify(&mr); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Read back user from LDAP to get the generated UUID
|
|
e, err = i.getUserByDN(e.DN)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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 == DisableMechanismGroup {
|
|
returnUser.AccountEnabled = user.AccountEnabled
|
|
}
|
|
|
|
return returnUser, nil
|
|
}
|
|
|
|
func (i *LDAP) getUserByDN(dn string) (*ldap.Entry, error) {
|
|
attrs := []string{
|
|
i.userAttributeMap.displayName,
|
|
i.userAttributeMap.id,
|
|
i.userAttributeMap.mail,
|
|
i.userAttributeMap.userName,
|
|
i.userAttributeMap.surname,
|
|
i.userAttributeMap.givenName,
|
|
i.userAttributeMap.accountEnabled,
|
|
i.userAttributeMap.userType,
|
|
}
|
|
|
|
filter := fmt.Sprintf("(objectClass=%s)", i.userObjectClass)
|
|
|
|
if i.userFilter != "" {
|
|
filter = fmt.Sprintf("(&%s(%s))", filter, i.userFilter)
|
|
}
|
|
|
|
return i.getEntryByDN(dn, attrs, filter)
|
|
}
|
|
|
|
func (i *LDAP) getEntryByDN(dn string, attrs []string, filter string) (*ldap.Entry, error) {
|
|
if filter == "" {
|
|
filter = "(objectclass=*)"
|
|
}
|
|
|
|
searchRequest := ldap.NewSearchRequest(
|
|
dn, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false,
|
|
filter,
|
|
attrs,
|
|
nil,
|
|
)
|
|
|
|
i.logger.Debug().Str("backend", "ldap").
|
|
Str("base", searchRequest.BaseDN).
|
|
Str("filter", searchRequest.Filter).
|
|
Int("scope", searchRequest.Scope).
|
|
Int("sizelimit", searchRequest.SizeLimit).
|
|
Interface("attributes", searchRequest.Attributes).
|
|
Msg("getEntryByDN")
|
|
res, err := i.conn.Search(searchRequest)
|
|
if err != nil {
|
|
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 {
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
return res.Entries[0], nil
|
|
}
|
|
|
|
func (i *LDAP) searchLDAPEntryByFilter(basedn string, attrs []string, filter string) (*ldap.Entry, error) {
|
|
if filter == "" {
|
|
filter = "(objectclass=*)"
|
|
}
|
|
|
|
searchRequest := ldap.NewSearchRequest(
|
|
basedn,
|
|
ldap.ScopeWholeSubtree,
|
|
ldap.NeverDerefAliases, 1, 0, false,
|
|
filter,
|
|
attrs,
|
|
nil,
|
|
)
|
|
|
|
i.logger.Debug().Str("backend", "ldap").
|
|
Str("base", searchRequest.BaseDN).
|
|
Str("filter", searchRequest.Filter).
|
|
Int("scope", searchRequest.Scope).
|
|
Int("sizelimit", searchRequest.SizeLimit).
|
|
Interface("attributes", searchRequest.Attributes).
|
|
Msg("getEntryByFilter")
|
|
res, err := i.conn.Search(searchRequest)
|
|
if err != nil {
|
|
i.logger.Error().Err(err).Str("backend", "ldap").Str("dn", basedn).Str("filter", filter).Msg("Search user by filter failed")
|
|
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
|
|
}
|
|
if len(res.Entries) == 0 {
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
return res.Entries[0], nil
|
|
}
|
|
|
|
func (i *LDAP) getLDAPUserByID(id string) (*ldap.Entry, error) {
|
|
id = ldap.EscapeFilter(id)
|
|
filter := fmt.Sprintf("(%s=%s)", i.userAttributeMap.id, id)
|
|
return i.getLDAPUserByFilter(filter)
|
|
}
|
|
|
|
func (i *LDAP) getLDAPUserByNameOrID(nameOrID string) (*ldap.Entry, error) {
|
|
nameOrID = ldap.EscapeFilter(nameOrID)
|
|
filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", i.userAttributeMap.userName, nameOrID, i.userAttributeMap.id, nameOrID)
|
|
return i.getLDAPUserByFilter(filter)
|
|
}
|
|
|
|
func (i *LDAP) getLDAPUserByFilter(filter string) (*ldap.Entry, error) {
|
|
filter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.userObjectClass, filter)
|
|
attrs := []string{
|
|
i.userAttributeMap.displayName,
|
|
i.userAttributeMap.id,
|
|
i.userAttributeMap.mail,
|
|
i.userAttributeMap.userName,
|
|
i.userAttributeMap.surname,
|
|
i.userAttributeMap.givenName,
|
|
i.userAttributeMap.accountEnabled,
|
|
i.userAttributeMap.userType,
|
|
}
|
|
return i.searchLDAPEntryByFilter(i.userBaseDN, attrs, filter)
|
|
}
|
|
|
|
// GetUser implements the Backend Interface.
|
|
func (i *LDAP) GetUser(ctx context.Context, nameOrID string, oreq *godata.GoDataRequest) (*libregraph.User, error) {
|
|
logger := i.logger.SubloggerWithRequestID(ctx)
|
|
logger.Debug().Str("backend", "ldap").Msg("GetUser")
|
|
|
|
e, err := i.getLDAPUserByNameOrID(nameOrID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
u := i.createUserModelFromLDAP(e)
|
|
if u == nil {
|
|
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
|
|
}
|
|
|
|
if slices.Contains(exp, "memberOf") {
|
|
userGroups, err := i.getGroupsForUser(e.DN)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u.MemberOf = i.groupsFromLDAPEntries(userGroups)
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// GetUsers implements the Backend Interface.
|
|
func (i *LDAP) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.User, error) {
|
|
logger := i.logger.SubloggerWithRequestID(ctx)
|
|
logger.Debug().Str("backend", "ldap").Msg("GetUsers")
|
|
|
|
search, err := GetSearchValues(oreq.Query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
exp, err := GetExpandValues(oreq.Query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var userFilter string
|
|
if search != "" {
|
|
search = ldap.EscapeFilter(search)
|
|
userFilter = fmt.Sprintf(
|
|
"(|(%s=%s*)(%s=%s*)(%s=%s*))",
|
|
i.userAttributeMap.userName, search,
|
|
i.userAttributeMap.mail, search,
|
|
i.userAttributeMap.displayName, search,
|
|
)
|
|
}
|
|
userFilter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.userObjectClass, userFilter)
|
|
searchRequest := ldap.NewSearchRequest(
|
|
i.userBaseDN, i.userScope, ldap.NeverDerefAliases, 0, 0, false,
|
|
userFilter,
|
|
[]string{
|
|
i.userAttributeMap.displayName,
|
|
i.userAttributeMap.id,
|
|
i.userAttributeMap.mail,
|
|
i.userAttributeMap.userName,
|
|
i.userAttributeMap.surname,
|
|
i.userAttributeMap.givenName,
|
|
i.userAttributeMap.accountEnabled,
|
|
i.userAttributeMap.userType,
|
|
},
|
|
nil,
|
|
)
|
|
logger.Debug().Str("backend", "ldap").
|
|
Str("base", searchRequest.BaseDN).
|
|
Str("filter", searchRequest.Filter).
|
|
Int("scope", searchRequest.Scope).
|
|
Int("sizelimit", searchRequest.SizeLimit).
|
|
Interface("attributes", searchRequest.Attributes).
|
|
Msg("GetUsers")
|
|
res, err := i.conn.Search(searchRequest)
|
|
if err != nil {
|
|
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
|
|
}
|
|
|
|
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)
|
|
// Skip invalid LDAP users
|
|
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 {
|
|
return nil, err
|
|
}
|
|
u.MemberOf = i.groupsFromLDAPEntries(userGroups)
|
|
}
|
|
users = append(users, u)
|
|
}
|
|
return users, nil
|
|
}
|
|
|
|
func (i *LDAP) changeUserName(ctx context.Context, dn, originalUserName, newUserName string) (*ldap.Entry, error) {
|
|
logger := i.logger.SubloggerWithRequestID(ctx)
|
|
|
|
attributeTypeAndValue := ldap.AttributeTypeAndValue{
|
|
Type: i.userAttributeMap.userName,
|
|
Value: newUserName,
|
|
}
|
|
newDNString := attributeTypeAndValue.String()
|
|
|
|
logger.Debug().Str("originalDN", dn).Str("newDN", newDNString).Msg("Modifying DN")
|
|
mrdn := ldap.NewModifyDNRequest(dn, newDNString, true, "")
|
|
|
|
if err := i.conn.ModifyDN(mrdn); err != nil {
|
|
var lerr *ldap.Error
|
|
logger.Debug().Str("originalDN", dn).Str("newDN", newDNString).Err(err).Msg("Failed to modify DN")
|
|
if errors.As(err, &lerr) {
|
|
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
|
|
err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
parsed, err := ldap.ParseDN(dn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newFullDN, err := replaceDN(parsed, newDNString)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
u, err := i.getUserByDN(newFullDN)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !i.refintEnabled {
|
|
groups, err := i.getGroupsForUser(dn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, g := range groups {
|
|
logger.Debug().Str("originalDN", dn).Str("newDN", u.DN).Str("group", g.DN).Msg("Changing member in group")
|
|
err = i.renameMemberInGroup(ctx, g, dn, u.DN)
|
|
// This could leave the groups in an inconsistent state, might be a good idea to
|
|
// add a defer that changes everything back on error. Ideally, this entire function
|
|
// should be atomic, but LDAP doesn't support that.
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
func (i *LDAP) renameMemberInGroup(ctx context.Context, group *ldap.Entry, oldMember, newMember string) error {
|
|
logger := i.logger.SubloggerWithRequestID(ctx)
|
|
logger.Debug().Str("oldMember", oldMember).Str("newMember", newMember).Msg("replacing group member")
|
|
mr := ldap.NewModifyRequest(group.DN, nil)
|
|
mr.Delete(i.groupAttributeMap.member, []string{oldMember})
|
|
mr.Add(i.groupAttributeMap.member, []string{newMember})
|
|
if err := i.conn.Modify(mr); err != nil {
|
|
var lerr *ldap.Error
|
|
if errors.As(err, &lerr) {
|
|
if lerr.ResultCode == ldap.LDAPResultNoSuchObject {
|
|
logger.Warn().Str("group", group.DN).Msg("Group no longer exists")
|
|
return nil
|
|
} else if lerr.ResultCode == ldap.LDAPResultNoSuchAttribute {
|
|
logger.Warn().
|
|
Str("oldMember", oldMember).
|
|
Str("newMember", newMember).
|
|
Str("groupDN", group.DN).
|
|
Msg("member attribute not found, this probably means that the server has refint enabled, please configure the OCIS to respect that.")
|
|
return nil
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *LDAP) updateUserPassowrd(ctx context.Context, dn, password string) error {
|
|
logger := i.logger.SubloggerWithRequestID(ctx)
|
|
logger.Debug().Str("backend", "ldap").Msg("updateUserPassowrd")
|
|
pwMod := ldap.PasswordModifyRequest{
|
|
UserIdentity: dn,
|
|
NewPassword: password,
|
|
}
|
|
// Note: We can ignore the result message here, as it were only relevant if we requested
|
|
// the server to generate a new Password
|
|
_, err := i.conn.PasswordModify(&pwMod)
|
|
if err != nil {
|
|
var lerr *ldap.Error
|
|
logger.Debug().Err(err).Msg("error setting password for user")
|
|
if errors.As(err, &lerr) {
|
|
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
|
|
err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
|
|
opsan := e.GetEqualFoldAttributeValue(i.userAttributeMap.userName)
|
|
id := e.GetEqualFoldAttributeValue(i.userAttributeMap.id)
|
|
givenName := e.GetEqualFoldAttributeValue(i.userAttributeMap.givenName)
|
|
surname := e.GetEqualFoldAttributeValue(i.userAttributeMap.surname)
|
|
|
|
if id != "" && opsan != "" {
|
|
return &libregraph.User{
|
|
DisplayName: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.displayName)),
|
|
Mail: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.mail)),
|
|
OnPremisesSamAccountName: &opsan,
|
|
Id: &id,
|
|
GivenName: &givenName,
|
|
Surname: &surname,
|
|
UserType: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.userType)),
|
|
AccountEnabled: booleanOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.accountEnabled)),
|
|
}
|
|
}
|
|
i.logger.Warn().Str("dn", e.DN).Msg("Invalid User. Missing username or id attribute")
|
|
return nil
|
|
}
|
|
|
|
func (i *LDAP) userToLDAPAttrValues(user libregraph.User) (map[string][]string, error) {
|
|
attrs := map[string][]string{
|
|
i.userAttributeMap.displayName: {user.GetDisplayName()},
|
|
i.userAttributeMap.userName: {user.GetOnPremisesSamAccountName()},
|
|
i.userAttributeMap.mail: {user.GetMail()},
|
|
"objectClass": {"inetOrgPerson", "organizationalPerson", "person", "top", "ownCloudUser"},
|
|
"cn": {user.GetOnPremisesSamAccountName()},
|
|
i.userAttributeMap.userType: {user.GetUserType()},
|
|
}
|
|
|
|
if !i.useServerUUID {
|
|
attrs["owncloudUUID"] = []string{uuid.Must(uuid.NewV4()).String()}
|
|
}
|
|
|
|
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
|
|
// Surname is not set in the Request
|
|
var sn string
|
|
if user.Surname != nil && *user.Surname != "" {
|
|
sn = *user.Surname
|
|
} else {
|
|
sn = *user.OnPremisesSamAccountName
|
|
}
|
|
attrs[i.userAttributeMap.surname] = []string{sn}
|
|
|
|
// When we get a givenName, we set the attribute.
|
|
if givenName := user.GetGivenName(); givenName != "" {
|
|
attrs[i.userAttributeMap.givenName] = []string{givenName}
|
|
}
|
|
|
|
if !i.usePwModifyExOp && user.PasswordProfile != nil && user.PasswordProfile.Password != nil {
|
|
// Depending on the LDAP server implementation this might cause the
|
|
// password to be stored in cleartext in the LDAP database. Using the
|
|
// "Password Modify LDAP Extended Operation" is recommended.
|
|
attrs["userPassword"] = []string{*user.PasswordProfile.Password}
|
|
}
|
|
return attrs, nil
|
|
}
|
|
|
|
func (i *LDAP) getUserAttrTypes() []string {
|
|
return []string{
|
|
i.userAttributeMap.displayName,
|
|
i.userAttributeMap.userName,
|
|
i.userAttributeMap.mail,
|
|
i.userAttributeMap.surname,
|
|
i.userAttributeMap.givenName,
|
|
"objectClass",
|
|
"cn",
|
|
"owncloudUUID",
|
|
"userPassword",
|
|
i.userAttributeMap.accountEnabled,
|
|
i.userAttributeMap.userType,
|
|
}
|
|
}
|
|
|
|
func (i *LDAP) getUserLDAPDN(user libregraph.User) string {
|
|
attributeTypeAndValue := ldap.AttributeTypeAndValue{
|
|
Type: "uid",
|
|
Value: *user.OnPremisesSamAccountName,
|
|
}
|
|
return fmt.Sprintf("%s,%s", attributeTypeAndValue.String(), i.userBaseDN)
|
|
}
|
|
|
|
func (i *LDAP) userToAddRequest(user libregraph.User) (*ldap.AddRequest, error) {
|
|
ar := ldap.NewAddRequest(i.getUserLDAPDN(user), nil)
|
|
|
|
attrMap, err := i.userToLDAPAttrValues(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, attrType := range i.getUserAttrTypes() {
|
|
if values, ok := attrMap[attrType]; ok {
|
|
ar.Attribute(attrType, values)
|
|
}
|
|
}
|
|
return ar, nil
|
|
}
|
|
|
|
func pointerOrNil(val string) *string {
|
|
if val == "" {
|
|
return nil
|
|
}
|
|
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 {
|
|
case "sub":
|
|
s = ldap.ScopeWholeSubtree
|
|
case "one":
|
|
s = ldap.ScopeSingleLevel
|
|
case "base":
|
|
s = ldap.ScopeBaseObject
|
|
default:
|
|
return 0, fmt.Errorf("invalid Scope '%s'", scope)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// removeEntryByDNAndAttributeFromEntry creates a request to remove a single member entry by attribute and DN from an ldap entry
|
|
func (i *LDAP) removeEntryByDNAndAttributeFromEntry(entry *ldap.Entry, dn string, attribute string) (*ldap.ModifyRequest, error) {
|
|
nOldDN, err := ldapdn.ParseNormalize(dn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries := entry.GetEqualFoldAttributeValues(attribute)
|
|
found := false
|
|
for _, entry := range entries {
|
|
if entry == "" {
|
|
continue
|
|
}
|
|
if nEntry, err := ldapdn.ParseNormalize(entry); err != nil {
|
|
// We couldn't parse the entry value as a DN. Let's keep it
|
|
// as it is but log a warning
|
|
i.logger.Warn().Str("entryDN", entry).Err(err).Msg("Couldn't parse DN")
|
|
continue
|
|
} else {
|
|
if nEntry == nOldDN {
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
i.logger.Debug().Str("backend", "ldap").Str("entry", entry.DN).Str("target", dn).
|
|
Msg("The target is not an entry in the attribute list")
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
mr := ldap.ModifyRequest{DN: entry.DN}
|
|
if len(entries) == 1 {
|
|
mr.Add(attribute, []string{""})
|
|
}
|
|
mr.Delete(attribute, []string{dn})
|
|
return &mr, nil
|
|
}
|
|
|
|
// expandLDAPAttributeEntries reads an attribute from an ldap entry and expands to users
|
|
func (i *LDAP) expandLDAPAttributeEntries(ctx context.Context, e *ldap.Entry, attribute string) ([]*ldap.Entry, error) {
|
|
logger := i.logger.SubloggerWithRequestID(ctx)
|
|
logger.Debug().Str("backend", "ldap").Msg("ExpandLDAPAttributeEntries")
|
|
result := []*ldap.Entry{}
|
|
|
|
for _, entryDN := range e.GetEqualFoldAttributeValues(attribute) {
|
|
if entryDN == "" {
|
|
continue
|
|
}
|
|
logger.Debug().Str("entryDN", entryDN).Msg("lookup")
|
|
ue, err := i.getUserByDN(entryDN)
|
|
if err != nil {
|
|
// Ignore errors when reading a specific entry fails, just log them and continue
|
|
logger.Debug().Err(err).Str("entry", entryDN).Msg("error reading attribute member entry")
|
|
continue
|
|
}
|
|
result = append(result, ue)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func replaceDN(fullDN *ldap.DN, newDN string) (string, error) {
|
|
if len(fullDN.RDNs) == 0 {
|
|
return "", fmt.Errorf("Can't operate on an empty dn")
|
|
}
|
|
|
|
if len(fullDN.RDNs) == 1 {
|
|
return newDN, nil
|
|
}
|
|
|
|
for _, part := range fullDN.RDNs[1:] {
|
|
newDN += "," + part.String()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
func (i *LDAP) updateAccountEnabledState(logger log.Logger, accountEnabled bool, e *ldap.Entry, mr *ldap.ModifyRequest) (updateNeeded bool, err error) {
|
|
switch i.disableUserMechanism {
|
|
case DisableMechanismNone:
|
|
err = errors.New("accountEnabled option not compatible with backend disable user mechanism")
|
|
case DisableMechanismAttribute:
|
|
boolString := strings.ToUpper(strconv.FormatBool(accountEnabled))
|
|
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 accountEnabled {
|
|
err = i.enableUser(logger, e.DN)
|
|
} else {
|
|
err = i.disableUser(logger, e.DN)
|
|
}
|
|
updateNeeded = true
|
|
}
|
|
|
|
return updateNeeded, err
|
|
}
|