diff --git a/graph/pkg/config/config.go b/graph/pkg/config/config.go index 81a95d0d9a..ce39957d9b 100644 --- a/graph/pkg/config/config.go +++ b/graph/pkg/config/config.go @@ -52,8 +52,22 @@ type Spaces struct { DefaultQuota string `ocisConfig:"default_quota"` } +type LDAP struct { + URI string `ocisConfig:"uri"` + BindDN string `ocisConfig:"bind_dn"` + BindPassword string `ocisConfig:"bind_password"` + UserBaseDN string `ocisConfig:"user_base_dn"` + UserEmailAttribute string `ocisConfig:"user_mail_attribute"` + UserDisplayNameAttribute string `ocisConfig:"user_displayname_attribute"` + UserNameAttribute string `ocisConfig:"user_name_attribute"` + UserIDAttribute string `ocisConfig:"user_id_attribute"` + UserFilter string `ocisConfig:"user_filter"` + UserSearchScope string `ocisConfig:"user_search_scope"` +} + type Identity struct { Backend string `ocisConfig:"backend"` + LDAP LDAP `ocisConfig:"ldap"` } // Config combines all available configuration parts. @@ -110,6 +124,20 @@ func DefaultConfig() *Config { }, Identity: Identity{ Backend: "cs3", + LDAP: LDAP{ + URI: "ldap://localhost:9125", + BindDN: "", + BindPassword: "", + UserBaseDN: "ou=users,dc=ocis,dc=test", + UserEmailAttribute: "mail", + UserDisplayNameAttribute: "displayName", + UserNameAttribute: "uid", + // 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", + UserFilter: "(objectClass=posixaccount)", + UserSearchScope: "sub", + }, }, } } diff --git a/graph/pkg/config/mappings.go b/graph/pkg/config/mappings.go index 1daeaa39b6..a2a3a4d9ce 100644 --- a/graph/pkg/config/mappings.go +++ b/graph/pkg/config/mappings.go @@ -115,5 +115,45 @@ func structMappings(cfg *Config) []shared.EnvBinding { EnvVars: []string{"GRAPH_IDENTITY_BACKEND"}, Destination: &cfg.Identity.Backend, }, + { + EnvVars: []string{"GRAPH_LDAP_URI"}, + Destination: &cfg.Identity.LDAP.URI, + }, + { + EnvVars: []string{"GRAPH_LDAP_BIND_DN"}, + Destination: &cfg.Identity.LDAP.BindDN, + }, + { + EnvVars: []string{"GRAPH_LDAP_BIND_PASSWORD"}, + Destination: &cfg.Identity.LDAP.BindPassword, + }, + { + EnvVars: []string{"GRAPH_LDAP_USER_BASE_DN"}, + Destination: &cfg.Identity.LDAP.UserBaseDN, + }, + { + EnvVars: []string{"GRAPH_LDAP_USER_EMAIL_ATTRIBUTE"}, + Destination: &cfg.Identity.LDAP.UserEmailAttribute, + }, + { + EnvVars: []string{"GRAPH_LDAP_USER_DISPLAYNAME_ATTRIBUTE"}, + Destination: &cfg.Identity.LDAP.UserDisplayNameAttribute, + }, + { + EnvVars: []string{"GRAPH_LDAP_USER_NAME_ATTRIBUTE"}, + Destination: &cfg.Identity.LDAP.UserNameAttribute, + }, + { + EnvVars: []string{"GRAPH_LDAP_USER_UID_ATTRIBUTE"}, + Destination: &cfg.Identity.LDAP.UserIDAttribute, + }, + { + EnvVars: []string{"GRAPH_LDAP_USER_FILTER"}, + Destination: &cfg.Identity.LDAP.UserFilter, + }, + { + EnvVars: []string{"GRAPH_LDAP_USER_SCOPE"}, + Destination: &cfg.Identity.LDAP.UserSearchScope, + }, } } diff --git a/graph/pkg/identity/ldap.go b/graph/pkg/identity/ldap.go new file mode 100644 index 0000000000..e58da42856 --- /dev/null +++ b/graph/pkg/identity/ldap.go @@ -0,0 +1,157 @@ +package identity + +import ( + "context" + "fmt" + "net/url" + + "github.com/go-ldap/ldap/v3" + msgraph "github.com/yaegashi/msgraph.go/beta" + + "github.com/owncloud/ocis/graph/pkg/config" + ldaputil "github.com/owncloud/ocis/graph/pkg/identity/ldap" + "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" + "github.com/owncloud/ocis/ocis-pkg/log" +) + +type LDAP struct { + userBaseDN string + userFilter string + userScope int + userAttributeMap userAttributeMap + logger *log.Logger + conn *ldaputil.ConnWithReconnect +} + +type userAttributeMap struct { + displayName string + id string + mail string + userName string +} + +func NewLDAPBackend(config config.LDAP, logger *log.Logger) *LDAP { + conn := ldaputil.NewLDAPWithReconnect(logger, config.URI, config.BindDN, config.BindPassword) + uam := userAttributeMap{ + displayName: config.UserDisplayNameAttribute, + id: config.UserIDAttribute, + mail: config.UserEmailAttribute, + userName: config.UserNameAttribute, + } + + var userScope int + switch config.UserSearchScope { + case "sub": + userScope = ldap.ScopeWholeSubtree + case "one": + userScope = ldap.ScopeSingleLevel + case "base": + userScope = ldap.ScopeBaseObject + } + + return &LDAP{ + userBaseDN: config.UserBaseDN, + userFilter: config.UserFilter, + userScope: userScope, + userAttributeMap: uam, + logger: logger, + conn: &conn, + } +} + +func (i *LDAP) GetUser(ctx context.Context, userID string) (*msgraph.User, error) { + i.logger.Debug().Str("backend", "ldap").Msg("GetUser") + userID = ldap.EscapeFilter(userID) + searchRequest := ldap.NewSearchRequest( + i.userBaseDN, i.userScope, ldap.NeverDerefAliases, 1, 0, false, + fmt.Sprintf("(&%s(|(%s=%s)(%s=%s)))", i.userFilter, i.userAttributeMap.userName, userID, i.userAttributeMap.id, userID), + []string{ + i.userAttributeMap.displayName, + i.userAttributeMap.id, + i.userAttributeMap.mail, + i.userAttributeMap.userName, + }, + nil, + ) + i.logger.Debug().Str("backend", "ldap").Msgf("Search %s", i.userBaseDN) + res, err := i.conn.Search(searchRequest) + + if err != nil { + var errmsg string + if lerr, ok := err.(*ldap.Error); ok { + if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded { + errmsg = fmt.Sprintf("too many results searching for user '%s'", userID) + i.logger.Debug().Str("backend", "ldap").Err(lerr).Msg(errmsg) + } + } + return nil, errorcode.New(errorcode.ItemNotFound, errmsg) + } + if len(res.Entries) == 0 { + return nil, errorcode.New(errorcode.ItemNotFound, "not found") + } + + return i.createUserModelFromLDAP(res.Entries[0]), nil +} + +func (i *LDAP) GetUsers(ctx context.Context, queryParam url.Values) ([]*msgraph.User, error) { + i.logger.Debug().Str("backend", "ldap").Msg("GetUsers") + + search := queryParam.Get("search") + if search == "" { + search = queryParam.Get("$search") + } + userFilter := i.userFilter + if search != "" { + search = ldap.EscapeFilter(search) + userFilter = fmt.Sprintf( + "(&(%s)(|(%s=%s*)(%s=%s*)(%s=%s*)))", + userFilter, + i.userAttributeMap.userName, search, + i.userAttributeMap.mail, search, + i.userAttributeMap.displayName, search, + ) + } + 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, + }, + nil, + ) + i.logger.Debug().Str("backend", "ldap").Msgf("Search %s", i.userBaseDN) + res, err := i.conn.Search(searchRequest) + if err != nil { + return nil, errorcode.New(errorcode.ItemNotFound, err.Error()) + } + + users := make([]*msgraph.User, 0, len(res.Entries)) + + for _, e := range res.Entries { + users = append(users, i.createUserModelFromLDAP(e)) + } + return users, nil +} + +func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *msgraph.User { + return &msgraph.User{ + DisplayName: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.displayName)), + Mail: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.mail)), + OnPremisesSamAccountName: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.userName)), + DirectoryObject: msgraph.DirectoryObject{ + Entity: msgraph.Entity{ + ID: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.id)), + }, + }, + } +} + +func pointerOrNil(val string) *string { + if val == "" { + return nil + } + return &val +} diff --git a/graph/pkg/service/v0/service.go b/graph/pkg/service/v0/service.go index 562152d254..08a53b65b9 100644 --- a/graph/pkg/service/v0/service.go +++ b/graph/pkg/service/v0/service.go @@ -32,6 +32,8 @@ func NewService(opts ...Option) Service { Config: &options.Config.Reva, Logger: &options.Logger, } + case "ldap": + userBackend = identity.NewLDAPBackend(options.Config.Identity.LDAP, &options.Logger) default: options.Logger.Error().Msgf("Unknown Identity Backend: '%s'", options.Config.Identity.Backend) }