diff --git a/extensions/graph/Makefile b/extensions/graph/Makefile index df59c1a1d6c..d0e16854954 100644 --- a/extensions/graph/Makefile +++ b/extensions/graph/Makefile @@ -28,6 +28,7 @@ ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this $(MOCKERY) --dir pkg/service/v0 --case underscore --name GatewayClient $(MOCKERY) --dir pkg/service/v0 --case underscore --name HTTPClient $(MOCKERY) --dir pkg/service/v0 --case underscore --name Publisher + $(MOCKERY) --srcpkg github.com/go-ldap/ldap/v3 --case underscore --filename ldapclient.go --name Client .PHONY: ci-node-generate diff --git a/extensions/graph/mocks/gateway_client.go b/extensions/graph/mocks/gateway_client.go index c2ba9bc1f2f..aba88f1eb7a 100644 --- a/extensions/graph/mocks/gateway_client.go +++ b/extensions/graph/mocks/gateway_client.go @@ -18,6 +18,36 @@ type GatewayClient struct { mock.Mock } +// Authenticate provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) Authenticate(ctx context.Context, in *gatewayv1beta1.AuthenticateRequest, opts ...grpc.CallOption) (*gatewayv1beta1.AuthenticateResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *gatewayv1beta1.AuthenticateResponse + if rf, ok := ret.Get(0).(func(context.Context, *gatewayv1beta1.AuthenticateRequest, ...grpc.CallOption) *gatewayv1beta1.AuthenticateResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gatewayv1beta1.AuthenticateResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *gatewayv1beta1.AuthenticateRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateStorageSpace provides a mock function with given fields: ctx, in, opts func (_m *GatewayClient) CreateStorageSpace(ctx context.Context, in *providerv1beta1.CreateStorageSpaceRequest, opts ...grpc.CallOption) (*providerv1beta1.CreateStorageSpaceResponse, error) { _va := make([]interface{}, len(opts)) diff --git a/extensions/graph/mocks/ldapclient.go b/extensions/graph/mocks/ldapclient.go new file mode 100644 index 00000000000..f2c10eb7539 --- /dev/null +++ b/extensions/graph/mocks/ldapclient.go @@ -0,0 +1,294 @@ +// Code generated by mockery v2.10.4. DO NOT EDIT. + +package mocks + +import ( + ldap "github.com/go-ldap/ldap/v3" + mock "github.com/stretchr/testify/mock" + + time "time" + + tls "crypto/tls" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// Add provides a mock function with given fields: _a0 +func (_m *Client) Add(_a0 *ldap.AddRequest) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*ldap.AddRequest) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Bind provides a mock function with given fields: username, password +func (_m *Client) Bind(username string, password string) error { + ret := _m.Called(username, password) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(username, password) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Close provides a mock function with given fields: +func (_m *Client) Close() { + _m.Called() +} + +// Compare provides a mock function with given fields: dn, attribute, value +func (_m *Client) Compare(dn string, attribute string, value string) (bool, error) { + ret := _m.Called(dn, attribute, value) + + var r0 bool + if rf, ok := ret.Get(0).(func(string, string, string) bool); ok { + r0 = rf(dn, attribute, value) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, string) error); ok { + r1 = rf(dn, attribute, value) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Del provides a mock function with given fields: _a0 +func (_m *Client) Del(_a0 *ldap.DelRequest) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*ldap.DelRequest) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ExternalBind provides a mock function with given fields: +func (_m *Client) ExternalBind() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// IsClosing provides a mock function with given fields: +func (_m *Client) IsClosing() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Modify provides a mock function with given fields: _a0 +func (_m *Client) Modify(_a0 *ldap.ModifyRequest) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*ldap.ModifyRequest) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ModifyDN provides a mock function with given fields: _a0 +func (_m *Client) ModifyDN(_a0 *ldap.ModifyDNRequest) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*ldap.ModifyDNRequest) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ModifyWithResult provides a mock function with given fields: _a0 +func (_m *Client) ModifyWithResult(_a0 *ldap.ModifyRequest) (*ldap.ModifyResult, error) { + ret := _m.Called(_a0) + + var r0 *ldap.ModifyResult + if rf, ok := ret.Get(0).(func(*ldap.ModifyRequest) *ldap.ModifyResult); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ldap.ModifyResult) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*ldap.ModifyRequest) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PasswordModify provides a mock function with given fields: _a0 +func (_m *Client) PasswordModify(_a0 *ldap.PasswordModifyRequest) (*ldap.PasswordModifyResult, error) { + ret := _m.Called(_a0) + + var r0 *ldap.PasswordModifyResult + if rf, ok := ret.Get(0).(func(*ldap.PasswordModifyRequest) *ldap.PasswordModifyResult); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ldap.PasswordModifyResult) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*ldap.PasswordModifyRequest) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Search provides a mock function with given fields: _a0 +func (_m *Client) Search(_a0 *ldap.SearchRequest) (*ldap.SearchResult, error) { + ret := _m.Called(_a0) + + var r0 *ldap.SearchResult + if rf, ok := ret.Get(0).(func(*ldap.SearchRequest) *ldap.SearchResult); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ldap.SearchResult) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*ldap.SearchRequest) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SearchWithPaging provides a mock function with given fields: searchRequest, pagingSize +func (_m *Client) SearchWithPaging(searchRequest *ldap.SearchRequest, pagingSize uint32) (*ldap.SearchResult, error) { + ret := _m.Called(searchRequest, pagingSize) + + var r0 *ldap.SearchResult + if rf, ok := ret.Get(0).(func(*ldap.SearchRequest, uint32) *ldap.SearchResult); ok { + r0 = rf(searchRequest, pagingSize) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ldap.SearchResult) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*ldap.SearchRequest, uint32) error); ok { + r1 = rf(searchRequest, pagingSize) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetTimeout provides a mock function with given fields: _a0 +func (_m *Client) SetTimeout(_a0 time.Duration) { + _m.Called(_a0) +} + +// SimpleBind provides a mock function with given fields: _a0 +func (_m *Client) SimpleBind(_a0 *ldap.SimpleBindRequest) (*ldap.SimpleBindResult, error) { + ret := _m.Called(_a0) + + var r0 *ldap.SimpleBindResult + if rf, ok := ret.Get(0).(func(*ldap.SimpleBindRequest) *ldap.SimpleBindResult); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ldap.SimpleBindResult) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*ldap.SimpleBindRequest) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Start provides a mock function with given fields: +func (_m *Client) Start() { + _m.Called() +} + +// StartTLS provides a mock function with given fields: _a0 +func (_m *Client) StartTLS(_a0 *tls.Config) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*tls.Config) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UnauthenticatedBind provides a mock function with given fields: username +func (_m *Client) UnauthenticatedBind(username string) error { + ret := _m.Called(username) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(username) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/extensions/graph/pkg/service/v0/graph.go b/extensions/graph/pkg/service/v0/graph.go index 676c8d7b955..1b0ab42462c 100644 --- a/extensions/graph/pkg/service/v0/graph.go +++ b/extensions/graph/pkg/service/v0/graph.go @@ -17,12 +17,14 @@ import ( "google.golang.org/grpc" ) -//go:generate make generate +//go:generate make -C ../../.. generate // GatewayClient is the subset of the gateway.GatewayAPIClient that is being used to interact with the gateway type GatewayClient interface { //gateway.GatewayAPIClient + // Authenticates a user. + Authenticate(ctx context.Context, in *gateway.AuthenticateRequest, opts ...grpc.CallOption) (*gateway.AuthenticateResponse, error) // Returns the home path for the given authenticated user. // When a user has access to multiple storage providers, one of them is the home. GetHome(ctx context.Context, in *provider.GetHomeRequest, opts ...grpc.CallOption) (*provider.GetHomeResponse, error) diff --git a/extensions/graph/pkg/service/v0/instrument.go b/extensions/graph/pkg/service/v0/instrument.go index 0b31c66172d..7cad7f7c9c0 100644 --- a/extensions/graph/pkg/service/v0/instrument.go +++ b/extensions/graph/pkg/service/v0/instrument.go @@ -54,6 +54,11 @@ func (i instrument) PatchUser(w http.ResponseWriter, r *http.Request) { i.next.PatchUser(w, r) } +// ChangeOwnPassword implements the Service interface. +func (i instrument) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) { + i.next.ChangeOwnPassword(w, r) +} + // GetGroups implements the Service interface. func (i instrument) GetGroups(w http.ResponseWriter, r *http.Request) { i.next.GetGroups(w, r) diff --git a/extensions/graph/pkg/service/v0/logging.go b/extensions/graph/pkg/service/v0/logging.go index 6b28bf523bb..51213475d7e 100644 --- a/extensions/graph/pkg/service/v0/logging.go +++ b/extensions/graph/pkg/service/v0/logging.go @@ -54,6 +54,11 @@ func (l logging) PatchUser(w http.ResponseWriter, r *http.Request) { l.next.PatchUser(w, r) } +// ChangeOwnPassword implements the Service interface. +func (l logging) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) { + l.next.ChangeOwnPassword(w, r) +} + // GetGroups implements the Service interface. func (l logging) GetGroups(w http.ResponseWriter, r *http.Request) { l.next.GetGroups(w, r) diff --git a/extensions/graph/pkg/service/v0/option.go b/extensions/graph/pkg/service/v0/option.go index a5feb4e69d4..b8ebe47479c 100644 --- a/extensions/graph/pkg/service/v0/option.go +++ b/extensions/graph/pkg/service/v0/option.go @@ -5,6 +5,7 @@ import ( "github.com/cs3org/reva/v2/pkg/events" "github.com/owncloud/ocis/v2/extensions/graph/pkg/config" + "github.com/owncloud/ocis/v2/extensions/graph/pkg/identity" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/ocis-pkg/roles" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" @@ -19,6 +20,7 @@ type Options struct { Config *config.Config Middleware []func(http.Handler) http.Handler GatewayClient GatewayClient + IdentityBackend identity.Backend HTTPClient HTTPClient RoleService settingssvc.RoleService RoleManager *roles.Manager @@ -64,6 +66,13 @@ func WithGatewayClient(val GatewayClient) Option { } } +// WithIdentityBackend provides a function to set the IdentityBackend option. +func WithIdentityBackend(val identity.Backend) Option { + return func(o *Options) { + o.IdentityBackend = val + } +} + // WithHTTPClient provides a function to set the http client option. func WithHTTPClient(val HTTPClient) Option { return func(o *Options) { diff --git a/extensions/graph/pkg/service/v0/password.go b/extensions/graph/pkg/service/v0/password.go new file mode 100644 index 00000000000..e24011a6e82 --- /dev/null +++ b/extensions/graph/pkg/service/v0/password.go @@ -0,0 +1,88 @@ +package svc + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/CiscoM31/godata" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/go-chi/render" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/extensions/graph/pkg/service/v0/errorcode" +) + +// ChangeOwnPassword implements the Service interface. It allows the user to change +// its own password +func (g Graph) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u, ok := revactx.ContextGetUser(ctx) + if !ok { + g.logger.Error().Msg("user not in context") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "user not in context") + return + } + + sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/") + _, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query()) + if err != nil { + g.logger.Err(err).Interface("query", r.URL.Query()).Msg("query error") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + cpw := libregraph.NewPasswordChange() + err = json.NewDecoder(r.Body).Decode(cpw) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + + currentPw := cpw.GetCurrentPassword() + if currentPw == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "current password cannot be empty") + return + } + + newPw := cpw.GetNewPassword() + if newPw == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "new password cannot be empty") + return + } + + if newPw == currentPw { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "new password must be differnt from current password") + return + } + + authReq := &gateway.AuthenticateRequest{ + Type: "basic", + ClientId: u.Username, + ClientSecret: currentPw, + } + authRes, err := g.gatewayClient.Authenticate(r.Context(), authReq) + if err != nil { + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + if authRes.Status.Code != cs3rpc.Code_CODE_OK { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "password change failed") + return + } + + newPwProfile := libregraph.NewPasswordProfile() + newPwProfile.SetPassword(newPw) + changes := libregraph.NewUser() + changes.SetPasswordProfile(*newPwProfile) + _, err = g.identityBackend.UpdateUser(ctx, u.Id.OpaqueId, *changes) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "password change failed") + g.logger.Debug().Err(err).Str("userid", u.Id.OpaqueId).Msg("failed to update user password") + return + } + + render.Status(r, http.StatusNoContent) + render.NoContent(w, r) +} diff --git a/extensions/graph/pkg/service/v0/password_test.go b/extensions/graph/pkg/service/v0/password_test.go new file mode 100644 index 00000000000..9c7c06f7c82 --- /dev/null +++ b/extensions/graph/pkg/service/v0/password_test.go @@ -0,0 +1,159 @@ +package svc_test + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/rgrpc/status" + "github.com/go-ldap/ldap/v3" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/extensions/graph/mocks" + "github.com/owncloud/ocis/v2/extensions/graph/pkg/config" + "github.com/owncloud/ocis/v2/extensions/graph/pkg/config/defaults" + "github.com/owncloud/ocis/v2/extensions/graph/pkg/identity" + service "github.com/owncloud/ocis/v2/extensions/graph/pkg/service/v0" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/stretchr/testify/mock" +) + +type changePwTest struct { + desc string + currentpw string + newpw string + expected int +} + +var _ = Describe("Users changing their own password", func() { + var ( + svc service.Service + gatewayClient *mocks.GatewayClient + httpClient *mocks.HTTPClient + ldapClient *mocks.Client + ldapConfig config.LDAP + identityBackend identity.Backend + eventsPublisher mocks.Publisher + ctx context.Context + cfg *config.Config + user *userv1beta1.User + err error + ) + + JustBeforeEach(func() { + ctx = context.Background() + cfg = defaults.FullDefaultConfig() + cfg.TokenManager.JWTSecret = "loremipsum" + + gatewayClient = &mocks.GatewayClient{} + ldapClient = mockedLDAPClient() + + ldapConfig = config.LDAP{ + WriteEnabled: true, + UserDisplayNameAttribute: "displayName", + UserNameAttribute: "uid", + UserEmailAttribute: "mail", + UserIDAttribute: "ownclouduuid", + UserSearchScope: "sub", + GroupNameAttribute: "cn", + GroupIDAttribute: "ownclouduui", + GroupSearchScope: "sub", + } + loggger := log.NewLogger() + identityBackend, err = identity.NewLDAPBackend(ldapClient, ldapConfig, &loggger) + Expect(err).To(BeNil()) + + httpClient = &mocks.HTTPClient{} + eventsPublisher = mocks.Publisher{} + svc = service.NewService( + service.Config(cfg), + service.WithGatewayClient(gatewayClient), + service.WithIdentityBackend(identityBackend), + service.WithHTTPClient(httpClient), + service.EventsPublisher(&eventsPublisher), + ) + user = &userv1beta1.User{ + Id: &userv1beta1.UserId{ + OpaqueId: "user", + }, + } + ctx = revactx.ContextSetUser(ctx, user) + }) + + It("fails if no user in context", func() { + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/changePassword", nil) + rr := httptest.NewRecorder() + svc.ChangeOwnPassword(rr, r) + Expect(rr.Code).To(Equal(http.StatusInternalServerError)) + }) + + DescribeTable("changing the password", + func(current string, newpw string, authresult string, expected int) { + switch authresult { + case "error": + gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(nil, errors.New("fail")) + case "deny": + gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{ + Status: status.NewPermissionDenied(ctx, errors.New("wrong password"), "wrong password"), + Token: "authtoken", + }, nil) + default: + gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{ + Status: status.NewOK(ctx), + Token: "authtoken", + }, nil) + } + cpw := libregraph.NewPasswordChange() + cpw.SetCurrentPassword(current) + cpw.SetNewPassword(newpw) + body, _ := json.Marshal(cpw) + b := bytes.NewBuffer(body) + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/changePassword", b).WithContext(ctx) + rr := httptest.NewRecorder() + svc.ChangeOwnPassword(rr, r) + Expect(rr.Code).To(Equal(expected)) + }, + Entry("fails when current password is empty", "", "newpassword", "", http.StatusBadRequest), + Entry("fails when new password is empty", "currentpassword", "", "", http.StatusBadRequest), + Entry("fails when current and new password are equal", "password", "password", "", http.StatusBadRequest), + Entry("fails authentication with current password errors", "currentpassword", "newpassword", "error", http.StatusInternalServerError), + Entry("fails when current password is wrong", "currentpassword", "newpassword", "deny", http.StatusInternalServerError), + Entry("succeeds when current password is correct", "currentpassword", "newpassword", "", http.StatusNoContent), + ) +}) + +func mockedLDAPClient() *mocks.Client { + lm := &mocks.Client{} + + userEntry := ldap.NewEntry("uid=test", map[string][]string{ + "uid": {"test"}, + "displayName": {"test"}, + "mail": {"test@example.org"}, + }) + + lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + &ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, + nil) + + mr := ldap.NewModifyRequest("uid=test", nil) + mr.Changes = []ldap.Change{ + { + Operation: ldap.ReplaceAttribute, + Modification: ldap.PartialAttribute{ + Type: "userPassword", + Vals: []string{"newpassword"}, + }, + }, + } + lm.On("Modify", mr).Return(nil) + return lm +} diff --git a/extensions/graph/pkg/service/v0/service.go b/extensions/graph/pkg/service/v0/service.go index 4184a1fea57..cf328ce0503 100644 --- a/extensions/graph/pkg/service/v0/service.go +++ b/extensions/graph/pkg/service/v0/service.go @@ -35,6 +35,7 @@ type Service interface { PostUser(http.ResponseWriter, *http.Request) DeleteUser(http.ResponseWriter, *http.Request) PatchUser(http.ResponseWriter, *http.Request) + ChangeOwnPassword(http.ResponseWriter, *http.Request) GetGroups(http.ResponseWriter, *http.Request) GetGroup(http.ResponseWriter, *http.Request) @@ -55,46 +56,10 @@ func NewService(opts ...Option) Service { m := chi.NewMux() m.Use(options.Middleware...) - var backend identity.Backend - switch options.Config.Identity.Backend { - case "cs3": - backend = &identity.CS3{ - Config: options.Config.Reva, - Logger: &options.Logger, - } - case "ldap": - var err error - - var tlsConf *tls.Config - if options.Config.Identity.LDAP.Insecure { - tlsConf = &tls.Config{ - //nolint:gosec // We need the ability to run with "insecure" (dev/testing) - InsecureSkipVerify: options.Config.Identity.LDAP.Insecure, - } - } - - conn := ldap.NewLDAPWithReconnect(&options.Logger, - ldap.Config{ - URI: options.Config.Identity.LDAP.URI, - BindDN: options.Config.Identity.LDAP.BindDN, - BindPassword: options.Config.Identity.LDAP.BindPassword, - TLSConfig: tlsConf, - }, - ) - if backend, err = identity.NewLDAPBackend(conn, options.Config.Identity.LDAP, &options.Logger); err != nil { - options.Logger.Error().Msgf("Error initializing LDAP Backend: '%s'", err) - return nil - } - default: - options.Logger.Error().Msgf("Unknown Identity Backend: '%s'", options.Config.Identity.Backend) - return nil - } - svc := Graph{ config: options.Config, mux: m, logger: &options.Logger, - identityBackend: backend, spacePropertiesCache: ttlcache.NewCache(), eventsPublisher: options.EventsPublisher, } @@ -108,6 +73,44 @@ func NewService(opts ...Option) Service { } else { svc.gatewayClient = options.GatewayClient } + if options.IdentityBackend == nil { + switch options.Config.Identity.Backend { + case "cs3": + svc.identityBackend = &identity.CS3{ + Config: options.Config.Reva, + Logger: &options.Logger, + } + case "ldap": + var err error + + var tlsConf *tls.Config + if options.Config.Identity.LDAP.Insecure { + tlsConf = &tls.Config{ + //nolint:gosec // We need the ability to run with "insecure" (dev/testing) + InsecureSkipVerify: options.Config.Identity.LDAP.Insecure, + } + } + + conn := ldap.NewLDAPWithReconnect(&options.Logger, + ldap.Config{ + URI: options.Config.Identity.LDAP.URI, + BindDN: options.Config.Identity.LDAP.BindDN, + BindPassword: options.Config.Identity.LDAP.BindPassword, + TLSConfig: tlsConf, + }, + ) + if svc.identityBackend, err = identity.NewLDAPBackend(conn, options.Config.Identity.LDAP, &options.Logger); err != nil { + options.Logger.Error().Msgf("Error initializing LDAP Backend: '%s'", err) + return nil + } + default: + options.Logger.Error().Msgf("Unknown Identity Backend: '%s'", options.Config.Identity.Backend) + return nil + } + } else { + svc.identityBackend = options.IdentityBackend + } + if options.HTTPClient == nil { http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{ InsecureSkipVerify: options.Config.Spaces.Insecure, //nolint:gosec @@ -143,6 +146,7 @@ func NewService(opts ...Option) Service { r.Get("/", svc.GetMe) r.Get("/drives", svc.GetDrives) r.Get("/drive/root/children", svc.GetRootDriveChildren) + r.Post("/changePassword", svc.ChangeOwnPassword) }) r.Route("/users", func(r chi.Router) { r.With(requireAdmin).Get("/", svc.GetUsers) diff --git a/extensions/graph/pkg/service/v0/tracing.go b/extensions/graph/pkg/service/v0/tracing.go index 0d2581ba98d..d3a0167b463 100644 --- a/extensions/graph/pkg/service/v0/tracing.go +++ b/extensions/graph/pkg/service/v0/tracing.go @@ -50,6 +50,11 @@ func (t tracing) PatchUser(w http.ResponseWriter, r *http.Request) { t.next.PatchUser(w, r) } +// ChangeOwnPassword implements the Service interface. +func (t tracing) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) { + t.next.ChangeOwnPassword(w, r) +} + // GetGroups implements the Service interface. func (t tracing) GetGroups(w http.ResponseWriter, r *http.Request) { t.next.GetGroups(w, r) diff --git a/go.mod b/go.mod index f4aa9f18425..3e00736fabe 100644 --- a/go.mod +++ b/go.mod @@ -34,15 +34,12 @@ require ( github.com/gofrs/uuid v4.2.0+incompatible github.com/golang-jwt/jwt/v4 v4.4.1 github.com/golang/protobuf v1.5.2 - github.com/google/uuid v1.3.0 github.com/gookit/config/v2 v2.1.0 github.com/gorilla/mux v1.8.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0 - github.com/iancoleman/strcase v0.2.0 github.com/justinas/alice v1.2.0 github.com/libregraph/idm v0.3.1-0.20220315094434-e9a5cff3dd05 github.com/libregraph/lico v0.54.1-0.20220325072321-31efc3995d63 - github.com/mennanov/fieldmask-utils v0.5.0 github.com/mitchellh/mapstructure v1.5.0 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/nats-io/nats-server/v2 v2.8.2 @@ -157,6 +154,7 @@ require ( github.com/gomodule/redigo v1.8.8 // indirect github.com/google/go-cmp v0.5.7 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/gookit/goutil v0.5.0 // indirect github.com/gorilla/schema v1.2.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect @@ -171,6 +169,7 @@ require ( github.com/hashicorp/serf v0.9.6 // indirect github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect github.com/huandu/xstrings v1.3.2 // indirect + github.com/iancoleman/strcase v0.2.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect @@ -264,3 +263,6 @@ require ( // we need to use a fork to make the windows build pass replace github.com/pkg/xattr => github.com/micbar/xattr v0.4.6-0.20220215112335-88e74d648fb7 + +// remove once libre-graph-api is release with: https://github.com/owncloud/libre-graph-api/pull/39 +replace github.com/owncloud/libre-graph-api-go => github.com/rhafer/libre-graph-api-go v0.13.4-0.20220505155048-350c2e8fe3df diff --git a/go.sum b/go.sum index b982ad6ab6d..cb80270e522 100644 --- a/go.sum +++ b/go.sum @@ -883,8 +883,6 @@ github.com/maxymania/go-system v0.0.0-20170110133659-647cc364bf0b h1:Q53idHrTuQD github.com/maxymania/go-system v0.0.0-20170110133659-647cc364bf0b/go.mod h1:KirJrATYGbTyUwVR26xIkaipRqRcMRXBf8N5dacvGus= github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 h1:Z/i1e+gTZrmcGeZyWckaLfucYG6KYOXLWo4co8pZYNY= github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103/go.mod h1:o9YPB5aGP8ob35Vy6+vyq3P3bWe7NQWzf+JLiXCiMaE= -github.com/mennanov/fieldmask-utils v0.5.0 h1:8em4akN0NM3hmmrg8VbvOPfdS4SSBdbFd53m9VtfOg0= -github.com/mennanov/fieldmask-utils v0.5.0/go.mod h1:lah2lHczE2ff+7SqnNKpB+YzaO7M3h5iNO4LgPTJheM= github.com/micbar/xattr v0.4.6-0.20220215112335-88e74d648fb7 h1:M0R40eUlyqxMuZn3Knx4DJTwHE3TiPFzcWUA/BKtDMM= github.com/micbar/xattr v0.4.6-0.20220215112335-88e74d648fb7/go.mod h1:sBD3RAqlr8Q+RC3FutZcikpT8nyDrIEEBw2J744gVWs= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -1020,8 +1018,6 @@ github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35uk github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA= -github.com/owncloud/libre-graph-api-go v0.13.3 h1:jNtQ8QcT7AZTfhdVHDaqAOs2xaJTfqfkucM9GARBIrQ= -github.com/owncloud/libre-graph-api-go v0.13.3/go.mod h1:579sFrPP7aP24LZXGPopLfvE+hAka/2DYHk0+Ij+w+U= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -1103,6 +1099,8 @@ github.com/prometheus/statsd_exporter v0.22.4/go.mod h1:N4Z1+iSqc9rnxlT1N8Qn3l65 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rhafer/libre-graph-api-go v0.13.4-0.20220505155048-350c2e8fe3df h1:Lw9/oogPNnwpO0sNqnmMA0avfH+UmgmGCwucCuniDkk= +github.com/rhafer/libre-graph-api-go v0.13.4-0.20220505155048-350c2e8fe3df/go.mod h1:579sFrPP7aP24LZXGPopLfvE+hAka/2DYHk0+Ij+w+U= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -1835,7 +1833,6 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210524171403-669157292da3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=