From 5d6e361cffcf13d767f5a50cdd85b992c0676491 Mon Sep 17 00:00:00 2001 From: Alex Unger Date: Tue, 4 Jan 2022 23:12:49 +0100 Subject: [PATCH] Implement CreateUser support for the Graph LDAP backend This adds basic support for creating users via the GraphAPI LDAP backend. This currently just maintains the bare minimum Attributes for the inetOrgPerson objectclass. Co-authored-by: Ralf Haferkamp --- graph/pkg/config/defaultconfig.go | 4 +- graph/pkg/identity/backend.go | 3 + graph/pkg/identity/cs3.go | 5 ++ graph/pkg/identity/ldap.go | 92 +++++++++++++++++++++++++++- graph/pkg/identity/ldap/reconnect.go | 9 ++- graph/pkg/service/v0/service.go | 1 + graph/pkg/service/v0/users.go | 30 ++++++++- 7 files changed, 137 insertions(+), 7 deletions(-) diff --git a/graph/pkg/config/defaultconfig.go b/graph/pkg/config/defaultconfig.go index 3bb022786..acd24e838 100644 --- a/graph/pkg/config/defaultconfig.go +++ b/graph/pkg/config/defaultconfig.go @@ -33,13 +33,13 @@ func DefaultConfig() *Config { BindPassword: "", UserBaseDN: "ou=users,dc=ocis,dc=test", UserSearchScope: "sub", - UserFilter: "(objectClass=posixaccount)", + UserFilter: "(objectClass=inetOrgPerson)", 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", + UserIDAttribute: "entryUUID", GroupBaseDN: "ou=groups,dc=ocis,dc=test", GroupSearchScope: "sub", GroupFilter: "(objectclass=groupOfNames)", diff --git a/graph/pkg/identity/backend.go b/graph/pkg/identity/backend.go index f7b497251..d716009af 100644 --- a/graph/pkg/identity/backend.go +++ b/graph/pkg/identity/backend.go @@ -9,6 +9,9 @@ import ( ) type Backend interface { + // CreateUser creates a given user in the identity backend. + CreateUser(ctx context.Context, user libregraph.User) (*libregraph.User, error) + GetUser(ctx context.Context, nameOrId string) (*libregraph.User, error) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.User, error) diff --git a/graph/pkg/identity/cs3.go b/graph/pkg/identity/cs3.go index f76da144f..272bccfe6 100644 --- a/graph/pkg/identity/cs3.go +++ b/graph/pkg/identity/cs3.go @@ -20,6 +20,11 @@ type CS3 struct { Logger *log.Logger } +// CreateUser implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) CreateUser(ctx context.Context, user libregraph.User) (*libregraph.User, error) { + return nil, errorcode.New(errorcode.NotSupported, "not implemented") +} + func (i *CS3) GetUser(ctx context.Context, userID string) (*libregraph.User, error) { client, err := pool.GetGatewayServiceClient(i.Config.Address) if err != nil { diff --git a/graph/pkg/identity/ldap.go b/graph/pkg/identity/ldap.go index 5942c6f99..1becb2ca4 100644 --- a/graph/pkg/identity/ldap.go +++ b/graph/pkg/identity/ldap.go @@ -84,6 +84,95 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LD }, 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) { + + ar := ldap.AddRequest{ + DN: fmt.Sprintf("uid=%s,%s", *user.OnPremisesSamAccountName, i.userBaseDN), + Attributes: []ldap.Attribute{ + { + Type: "objectClass", + Vals: []string{"inetOrgPerson", "organizationalPerson", "person", "top"}, + }, + // inetOrgPerson requires "cn" + { + Type: "cn", + Vals: []string{*user.OnPremisesSamAccountName}, + }, + { + Type: i.userAttributeMap.mail, + Vals: []string{*user.Mail}, + }, + { + Type: i.userAttributeMap.userName, + Vals: []string{*user.OnPremisesSamAccountName}, + }, + { + Type: i.userAttributeMap.displayName, + Vals: []string{*user.DisplayName}, + }, + }, + } + + if user.PasswordProfile != nil && user.PasswordProfile.Password != nil { + // TODO? This relies to the LDAP server to properly hash the password. + // We might want to add support for the Password Modify LDAP Extended + // Operation for servers that implement it. (Or implement client-side + // hashing here. + ar.Attribute("userPassword", []string{*user.PasswordProfile.Password}) + } + + // 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 + } + ar.Attribute("sn", []string{sn}) + + if err := i.conn.Add(&ar); 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 +} + +func (i *LDAP) getUserByDN(dn string) (*ldap.Entry, error) { + searchRequest := ldap.NewSearchRequest( + dn, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, + "(objectclass=*)", + []string{ + i.userAttributeMap.displayName, + i.userAttributeMap.id, + i.userAttributeMap.mail, + i.userAttributeMap.userName, + }, + nil, + ) + + i.logger.Debug().Str("backend", "ldap").Str("dn", dn).Msg("Search user by DN") + 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") + return nil, errorcode.New(errorcode.ItemNotFound, err.Error()) + } + if len(res.Entries) == 0 { + return nil, errorcode.New(errorcode.ItemNotFound, "not found") + } + + return res.Entries[0], nil +} + func (i *LDAP) GetUser(ctx context.Context, userID string) (*libregraph.User, error) { i.logger.Debug().Str("backend", "ldap").Msg("GetUser") userID = ldap.EscapeFilter(userID) @@ -106,7 +195,8 @@ func (i *LDAP) GetUser(ctx context.Context, userID string) (*libregraph.User, er 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) + i.logger.Debug().Str("backend", "ldap").Err(lerr). + Str("user", userID).Msg("too many results searching for user") } } return nil, errorcode.New(errorcode.ItemNotFound, errmsg) diff --git a/graph/pkg/identity/ldap/reconnect.go b/graph/pkg/identity/ldap/reconnect.go index f929e57a5..bf5c7a1bd 100644 --- a/graph/pkg/identity/ldap/reconnect.go +++ b/graph/pkg/identity/ldap/reconnect.go @@ -162,8 +162,13 @@ func (c ConnWithReconnect) ExternalBind() error { return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented")) } -func (c ConnWithReconnect) Add(*ldap.AddRequest) error { - return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented")) +func (c ConnWithReconnect) Add(a *ldap.AddRequest) error { + conn, err := c.GetConnection() + if err != nil { + return err + } + + return conn.Add(a) } func (c ConnWithReconnect) Del(*ldap.DelRequest) error { diff --git a/graph/pkg/service/v0/service.go b/graph/pkg/service/v0/service.go index 36d0cf451..15fa77e43 100644 --- a/graph/pkg/service/v0/service.go +++ b/graph/pkg/service/v0/service.go @@ -67,6 +67,7 @@ func NewService(opts ...Option) Service { }) r.Route("/users", func(r chi.Router) { r.Get("/", svc.GetUsers) + r.Post("/", svc.PostUser) r.Route("/{userID}", func(r chi.Router) { r.Get("/", svc.GetUser) }) diff --git a/graph/pkg/service/v0/users.go b/graph/pkg/service/v0/users.go index c62337cbc..7cdd802e5 100644 --- a/graph/pkg/service/v0/users.go +++ b/graph/pkg/service/v0/users.go @@ -1,6 +1,7 @@ package svc import ( + "encoding/json" "errors" "net/http" "net/url" @@ -8,9 +9,9 @@ import ( revactx "github.com/cs3org/reva/pkg/ctx" "github.com/go-chi/chi/v5" "github.com/go-chi/render" + libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/graph/pkg/identity" "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" - //msgraph "github.com/owncloud/open-graph-api-go" // FIXME needs OnPremisesSamAccountName, OnPremisesDomainName and AdditionalData ) // GetMe implements the Service interface. @@ -32,7 +33,6 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { } // GetUsers implements the Service interface. -// TODO use cs3 api to look up user func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { users, err := g.identityBackend.GetUsers(r.Context(), r.URL.Query()) if err != nil { @@ -47,6 +47,28 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, &listResponse{Value: users}) } +func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) { + u := libregraph.NewUser() + err := json.NewDecoder(r.Body).Decode(u) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + + if isNilOrEmpty(u.DisplayName) || isNilOrEmpty(u.OnPremisesSamAccountName) || isNilOrEmpty(u.Mail) { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Missing Required Attribute") + return + } + + if u, err = g.identityBackend.CreateUser(r.Context(), *u); err != nil { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, u) +} + // GetUser implements the Service interface. func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "userID") @@ -74,3 +96,7 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusOK) render.JSON(w, r, user) } + +func isNilOrEmpty(s *string) bool { + return s == nil || *s == "" +}