From 840c9a7dddc9ed1868c84a4ada280f8a4dcc36a8 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Wed, 12 Jan 2022 16:28:37 +0100 Subject: [PATCH] Implement UpdateUser support for the GraphAPI backend --- graph/pkg/identity/backend.go | 3 ++ graph/pkg/identity/cs3.go | 5 +++ graph/pkg/identity/ldap.go | 51 ++++++++++++++++++++++++++++ graph/pkg/identity/ldap/reconnect.go | 27 +++++++++++---- graph/pkg/service/v0/instrument.go | 5 +++ graph/pkg/service/v0/logging.go | 5 +++ graph/pkg/service/v0/service.go | 2 ++ graph/pkg/service/v0/tracing.go | 5 +++ graph/pkg/service/v0/users.go | 35 +++++++++++++++++++ 9 files changed, 132 insertions(+), 6 deletions(-) diff --git a/graph/pkg/identity/backend.go b/graph/pkg/identity/backend.go index 1a71a7b5d0..d230a6b133 100644 --- a/graph/pkg/identity/backend.go +++ b/graph/pkg/identity/backend.go @@ -15,6 +15,9 @@ type Backend interface { // DeleteUser deletes a given user, identified by username or id, from the backend DeleteUser(ctx context.Context, nameOrId string) error + // UpdateUser applies changes to given user, identified by username or id + UpdateUser(ctx context.Context, nameOrId string, 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 5fe0117c88..231107d11d 100644 --- a/graph/pkg/identity/cs3.go +++ b/graph/pkg/identity/cs3.go @@ -30,6 +30,11 @@ func (i *CS3) DeleteUser(ctx context.Context, nameOrID string) error { return errorcode.New(errorcode.NotSupported, "not implemented") } +// UpdateUser implements the Backend Interface. It's currently not suported for the CS3 backend +func (i *CS3) UpdateUser(ctx context.Context, nameOrID string, 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 b9f9c65cfd..47433c7e61 100644 --- a/graph/pkg/identity/ldap.go +++ b/graph/pkg/identity/ldap.go @@ -160,6 +160,57 @@ func (i *LDAP) DeleteUser(ctx context.Context, nameOrID string) error { return nil } +// UpdateUser implements the Backend Interface. It's currently not suported for the CS3 backedn +func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph.User) (*libregraph.User, error) { + e, err := i.getLDAPUserByNameOrID(nameOrID) + if err != nil { + return nil, err + } + + // Don't allow updates of the ID + if user.Id != nil && *user.Id != "" { + if e.GetEqualFoldAttributeValue(i.userAttributeMap.id) != *user.Id { + return nil, errorcode.New(errorcode.NotAllowed, "changing the UserId is not allowed") + } + } + // TODO: In order to allow updating the user name we'd need to issue a ModRDN operation + // As we currently using uid as the naming Attribute for the user entries. (Do we even + // want to allow changing the user name?). For now just disallow it. + if user.OnPremisesSamAccountName != nil && *user.OnPremisesSamAccountName != "" { + if e.GetEqualFoldAttributeValue(i.userAttributeMap.userName) != *user.OnPremisesSamAccountName { + return nil, errorcode.New(errorcode.NotSupported, "changing the user name is currently not supported") + } + } + + mr := ldap.ModifyRequest{DN: e.DN} + if user.DisplayName != nil && *user.DisplayName != "" { + if e.GetEqualFoldAttributeValue(i.userAttributeMap.displayName) != *user.DisplayName { + mr.Replace(i.userAttributeMap.displayName, []string{*user.DisplayName}) + } + } + if user.Mail != nil && *user.Mail != "" { + if e.GetEqualFoldAttributeValue(i.userAttributeMap.mail) != *user.Mail { + mr.Replace(i.userAttributeMap.mail, []string{*user.Mail}) + } + } + if user.PasswordProfile != nil && user.PasswordProfile.Password != nil && *user.PasswordProfile.Password != "" { + // 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.Password}) + } + + 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 + } + 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, diff --git a/graph/pkg/identity/ldap/reconnect.go b/graph/pkg/identity/ldap/reconnect.go index 272a0043e9..b4e868846c 100644 --- a/graph/pkg/identity/ldap/reconnect.go +++ b/graph/pkg/identity/ldap/reconnect.go @@ -180,16 +180,31 @@ func (c ConnWithReconnect) Del(d *ldap.DelRequest) error { return conn.Del(d) } -func (c ConnWithReconnect) Modify(*ldap.ModifyRequest) error { - return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented")) +func (c ConnWithReconnect) Modify(m *ldap.ModifyRequest) error { + conn, err := c.GetConnection() + if err != nil { + return err + } + + return conn.Modify(m) } -func (c ConnWithReconnect) ModifyDN(*ldap.ModifyDNRequest) error { - return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented")) +func (c ConnWithReconnect) ModifyDN(m *ldap.ModifyDNRequest) error { + conn, err := c.GetConnection() + if err != nil { + return err + } + + return conn.ModifyDN(m) } -func (c ConnWithReconnect) ModifyWithResult(*ldap.ModifyRequest) (*ldap.ModifyResult, error) { - return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented")) +func (c ConnWithReconnect) ModifyWithResult(m *ldap.ModifyRequest) (*ldap.ModifyResult, error) { + conn, err := c.GetConnection() + if err != nil { + return nil, err + } + + return conn.ModifyWithResult(m) } func (c ConnWithReconnect) Compare(dn, attribute, value string) (bool, error) { diff --git a/graph/pkg/service/v0/instrument.go b/graph/pkg/service/v0/instrument.go index aeb9106f56..a0fb82542d 100644 --- a/graph/pkg/service/v0/instrument.go +++ b/graph/pkg/service/v0/instrument.go @@ -48,3 +48,8 @@ func (i instrument) PostUser(w http.ResponseWriter, r *http.Request) { func (i instrument) DeleteUser(w http.ResponseWriter, r *http.Request) { i.next.DeleteUser(w, r) } + +// PatchUser implements the Service interface. +func (i instrument) PatchUser(w http.ResponseWriter, r *http.Request) { + i.next.PatchUser(w, r) +} diff --git a/graph/pkg/service/v0/logging.go b/graph/pkg/service/v0/logging.go index 2c1a10e17a..d5ce3373aa 100644 --- a/graph/pkg/service/v0/logging.go +++ b/graph/pkg/service/v0/logging.go @@ -48,3 +48,8 @@ func (l logging) PostUser(w http.ResponseWriter, r *http.Request) { func (l logging) DeleteUser(w http.ResponseWriter, r *http.Request) { l.next.DeleteUser(w, r) } + +// PatchUser implements the Service interface. +func (l logging) PatchUser(w http.ResponseWriter, r *http.Request) { + l.next.PatchUser(w, r) +} diff --git a/graph/pkg/service/v0/service.go b/graph/pkg/service/v0/service.go index 59a2c2f30c..451cdd872a 100644 --- a/graph/pkg/service/v0/service.go +++ b/graph/pkg/service/v0/service.go @@ -20,6 +20,7 @@ type Service interface { GetUser(http.ResponseWriter, *http.Request) PostUser(http.ResponseWriter, *http.Request) DeleteUser(http.ResponseWriter, *http.Request) + PatchUser(http.ResponseWriter, *http.Request) } // NewService returns a service implementation for Service. @@ -73,6 +74,7 @@ func NewService(opts ...Option) Service { r.Route("/{userID}", func(r chi.Router) { r.Get("/", svc.GetUser) r.Delete("/", svc.DeleteUser) + r.Patch("/", svc.PatchUser) }) }) r.Route("/groups", func(r chi.Router) { diff --git a/graph/pkg/service/v0/tracing.go b/graph/pkg/service/v0/tracing.go index 27bbeb817b..c1e5442145 100644 --- a/graph/pkg/service/v0/tracing.go +++ b/graph/pkg/service/v0/tracing.go @@ -44,3 +44,8 @@ func (t tracing) PostUser(w http.ResponseWriter, r *http.Request) { func (t tracing) DeleteUser(w http.ResponseWriter, r *http.Request) { t.next.DeleteUser(w, r) } + +// PatchUser implements the Service interface. +func (t tracing) PatchUser(w http.ResponseWriter, r *http.Request) { + t.next.PatchUser(w, r) +} diff --git a/graph/pkg/service/v0/users.go b/graph/pkg/service/v0/users.go index 0593c6bcb3..4a0ae17b87 100644 --- a/graph/pkg/service/v0/users.go +++ b/graph/pkg/service/v0/users.go @@ -123,6 +123,41 @@ func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) { render.NoContent(w, r) } +// PatchUser implements the Service Interface. Updates the specified attributes of an +// ExistingUser +func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) { + nameOrID := chi.URLParam(r, "userID") + nameOrID, err := url.PathUnescape(nameOrID) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping user id failed") + } + + if nameOrID == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing user id") + return + } + changes := libregraph.NewUser() + err = json.NewDecoder(r.Body).Decode(changes) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + + u, err := g.identityBackend.UpdateUser(r.Context(), nameOrID, *changes) + if err != nil { + var errcode errorcode.Error + if errors.As(err, &errcode) { + errcode.Render(w, r) + } else { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + } + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, u) + +} + func isNilOrEmpty(s *string) bool { return s == nil || *s == "" }