diff --git a/changelog/unreleased/preferred-language.md b/changelog/unreleased/preferred-language.md new file mode 100644 index 00000000000..5bc03f56029 --- /dev/null +++ b/changelog/unreleased/preferred-language.md @@ -0,0 +1,7 @@ +Enhancement: Add preferred language to user settings + +We have added the preferred language to the libre-graph api & added endpoints for that to ocis. + +https://github.com/owncloud/ocis/pull/7720 +https://github.com/owncloud/ocis/issues/5455 +https://github.com/owncloud/libre-graph-api/pull/130 diff --git a/go.mod b/go.mod index 9fd29de3c4b..72afa1f60d2 100644 --- a/go.mod +++ b/go.mod @@ -67,7 +67,7 @@ require ( github.com/onsi/gomega v1.29.0 github.com/open-policy-agent/opa v0.51.0 github.com/orcaman/concurrent-map v1.0.0 - github.com/owncloud/libre-graph-api-go v1.0.5-0.20231107135330-011e9d4c45e3 + github.com/owncloud/libre-graph-api-go v1.0.5-0.20231113143725-09bf34dc9afb github.com/pkg/errors v0.9.1 github.com/pkg/xattr v0.4.9 github.com/prometheus/client_golang v1.17.0 diff --git a/go.sum b/go.sum index 791546b027e..9663d29f787 100644 --- a/go.sum +++ b/go.sum @@ -1774,8 +1774,8 @@ 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 v1.0.5-0.20231107135330-011e9d4c45e3 h1:eUE3kNgr8PwcXeUKFkuEuz1+4hfCCmq+rKYQzk0OxtY= -github.com/owncloud/libre-graph-api-go v1.0.5-0.20231107135330-011e9d4c45e3/go.mod h1:v2aAl5IwEI8t+GmcWvBd+bvJMYp9Vf1hekLuRf0UnEs= +github.com/owncloud/libre-graph-api-go v1.0.5-0.20231113143725-09bf34dc9afb h1:KFnmkGvHY+6k6IZ9I1w5Ia24VbALYms+Y6W7LrsUbsE= +github.com/owncloud/libre-graph-api-go v1.0.5-0.20231113143725-09bf34dc9afb/go.mod h1:v2aAl5IwEI8t+GmcWvBd+bvJMYp9Vf1hekLuRf0UnEs= 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= diff --git a/services/graph/mocks/value_service.go b/services/graph/mocks/value_service.go new file mode 100644 index 00000000000..1872da1de14 --- /dev/null +++ b/services/graph/mocks/value_service.go @@ -0,0 +1,164 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + context "context" + + client "go-micro.dev/v4/client" + + mock "github.com/stretchr/testify/mock" + + v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" +) + +// ValueService is an autogenerated mock type for the ValueService type +type ValueService struct { + mock.Mock +} + +// GetValue provides a mock function with given fields: ctx, in, opts +func (_m *ValueService) GetValue(ctx context.Context, in *v0.GetValueRequest, opts ...client.CallOption) (*v0.GetValueResponse, 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 *v0.GetValueResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v0.GetValueRequest, ...client.CallOption) (*v0.GetValueResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v0.GetValueRequest, ...client.CallOption) *v0.GetValueResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0.GetValueResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v0.GetValueRequest, ...client.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetValueByUniqueIdentifiers provides a mock function with given fields: ctx, in, opts +func (_m *ValueService) GetValueByUniqueIdentifiers(ctx context.Context, in *v0.GetValueByUniqueIdentifiersRequest, opts ...client.CallOption) (*v0.GetValueResponse, 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 *v0.GetValueResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v0.GetValueByUniqueIdentifiersRequest, ...client.CallOption) (*v0.GetValueResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v0.GetValueByUniqueIdentifiersRequest, ...client.CallOption) *v0.GetValueResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0.GetValueResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v0.GetValueByUniqueIdentifiersRequest, ...client.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListValues provides a mock function with given fields: ctx, in, opts +func (_m *ValueService) ListValues(ctx context.Context, in *v0.ListValuesRequest, opts ...client.CallOption) (*v0.ListValuesResponse, 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 *v0.ListValuesResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v0.ListValuesRequest, ...client.CallOption) (*v0.ListValuesResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v0.ListValuesRequest, ...client.CallOption) *v0.ListValuesResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0.ListValuesResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v0.ListValuesRequest, ...client.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SaveValue provides a mock function with given fields: ctx, in, opts +func (_m *ValueService) SaveValue(ctx context.Context, in *v0.SaveValueRequest, opts ...client.CallOption) (*v0.SaveValueResponse, 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 *v0.SaveValueResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v0.SaveValueRequest, ...client.CallOption) (*v0.SaveValueResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v0.SaveValueRequest, ...client.CallOption) *v0.SaveValueResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0.SaveValueResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v0.SaveValueRequest, ...client.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewValueService creates a new instance of ValueService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewValueService(t interface { + mock.TestingT + Cleanup(func()) +}) *ValueService { + mock := &ValueService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/services/graph/pkg/server/http/server.go b/services/graph/pkg/server/http/server.go index 027599665fc..113ffac6002 100644 --- a/services/graph/pkg/server/http/server.go +++ b/services/graph/pkg/server/http/server.go @@ -83,6 +83,7 @@ func Server(opts ...Option) (http.Service, error) { // how do we secure the api? var requireAdminMiddleware func(stdhttp.Handler) stdhttp.Handler var roleService svc.RoleService + var valueService settingssvc.ValueService var gatewaySelector pool.Selectable[gateway.GatewayAPIClient] grpcClient, err := grpc.NewClient(append(grpc.GetClientOptions(options.Config.GRPCClientTLS), grpc.WithTraceProvider(options.TraceProvider))...) if err != nil { @@ -95,6 +96,7 @@ func Server(opts ...Option) (http.Service, error) { account.JWTSecret(options.Config.TokenManager.JWTSecret), )) roleService = settingssvc.NewRoleService("com.owncloud.api.settings", grpcClient) + valueService = settingssvc.NewValueService("com.owncloud.api.settings", grpcClient) gatewaySelector, err = pool.GatewaySelector( options.Config.Reva.Address, append( @@ -133,6 +135,7 @@ func Server(opts ...Option) (http.Service, error) { svc.Middleware(middlewares...), svc.EventsPublisher(publisher), svc.WithRoleService(roleService), + svc.WithValueService(valueService), svc.WithRequireAdminMiddleware(requireAdminMiddleware), svc.WithGatewaySelector(gatewaySelector), svc.WithSearchService(searchsvc.NewSearchProviderService("com.owncloud.api.search", grpcClient)), diff --git a/services/graph/pkg/service/v0/graph.go b/services/graph/pkg/service/v0/graph.go index ffb174c6613..36e3c3b7b43 100644 --- a/services/graph/pkg/service/v0/graph.go +++ b/services/graph/pkg/service/v0/graph.go @@ -70,6 +70,7 @@ type Graph struct { gatewaySelector pool.Selectable[gateway.GatewayAPIClient] roleService RoleService permissionsService Permissions + valueService settingssvc.ValueService specialDriveItemsCache *ttlcache.Cache[string, interface{}] identityCache identity.IdentityCache eventsPublisher events.Publisher diff --git a/services/graph/pkg/service/v0/option.go b/services/graph/pkg/service/v0/option.go index 9ed23fb4a12..f68633b48be 100644 --- a/services/graph/pkg/service/v0/option.go +++ b/services/graph/pkg/service/v0/option.go @@ -31,6 +31,7 @@ type Options struct { IdentityEducationBackend identity.EducationBackend RoleService RoleService PermissionService Permissions + ValueService settingssvc.ValueService RoleManager *roles.Manager EventsPublisher events.Publisher SearchService searchsvc.SearchProviderService @@ -106,6 +107,13 @@ func WithRoleService(val RoleService) Option { } } +// WithValueService provides a function to set the ValueService option. +func WithValueService(val settingssvc.ValueService) Option { + return func(o *Options) { + o.ValueService = val + } +} + // WithSearchService provides a function to set the SearchService option. func WithSearchService(val searchsvc.SearchProviderService) Option { return func(o *Options) { diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index a02a25d62ca..3012bbd7298 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -146,6 +146,7 @@ func NewService(opts ...Option) (Graph, error) { keycloakClient: options.KeycloakClient, historyClient: options.EventHistoryClient, traceProvider: options.TraceProvider, + valueService: options.ValueService, } if err := setIdentityBackends(options, &svc); err != nil { @@ -212,9 +213,12 @@ func NewService(opts ...Option) (Graph, error) { r.Route("/me", func(r chi.Router) { r.Get("/", svc.GetMe) r.Get("/drive", svc.GetUserDrive) - r.Get("/drives", svc.GetDrives) + r.Route("/drives", func(r chi.Router) { + r.Get("/", svc.GetDrives) + }) r.Get("/drive/root/children", svc.GetRootDriveChildren) r.Post("/changePassword", svc.ChangeOwnPassword) + r.Patch("/", svc.PatchMe) }) r.Route("/users", func(r chi.Router) { r.With(requireAdmin).Get("/", svc.GetUsers) diff --git a/services/graph/pkg/service/v0/users.go b/services/graph/pkg/service/v0/users.go index 097e4fa654a..0a2ee154629 100644 --- a/services/graph/pkg/service/v0/users.go +++ b/services/graph/pkg/service/v0/users.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "github.com/google/uuid" + "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults" "net/http" "net/url" "reflect" @@ -22,10 +24,12 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" + settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/v2/services/graph/pkg/identity" "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" - settingssvc "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0" + ocissettingssvc "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0" "golang.org/x/exp/slices" ) @@ -85,6 +89,15 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { } } + preferedLanguage, _, err := getUserLanguage(r.Context(), g.valueService, me.GetId()) + if err != nil { + logger.Error().Err(err).Msg("could not get user language") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not get user language") + return + } + + me.PreferredLanguage = &preferedLanguage + render.Status(r, http.StatusOK) render.JSON(w, r, me) } @@ -319,10 +332,10 @@ func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) { // to all new users for now, as create Account request does not have any role field if _, err = g.roleService.AssignRoleToUser(r.Context(), &settings.AssignRoleToUserRequest{ AccountUuid: *u.Id, - RoleId: settingssvc.BundleUUIDRoleUser, + RoleId: ocissettingssvc.BundleUUIDRoleUser, }); err != nil { // log as error, admin eventually needs to do something - logger.Error().Err(err).Str("id", *u.Id).Str("role", settingssvc.BundleUUIDRoleUser).Msg("could not create user: role assignment failed") + logger.Error().Err(err).Str("id", *u.Id).Str("role", ocissettingssvc.BundleUUIDRoleUser).Msg("could not create user: role assignment failed") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "role assignment failed") return } @@ -479,6 +492,23 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, user) } +// getUserLanguage returns the language of the user in the context. +func getUserLanguage(ctx context.Context, valueService settingssvc.ValueService, userID string) (string, string, error) { + gvr, err := valueService.GetValueByUniqueIdentifiers(ctx, &settingssvc.GetValueByUniqueIdentifiersRequest{ + AccountUuid: userID, + SettingId: defaults.SettingUUIDProfileLanguage, + }) + if err != nil { + return "", "", err + } + + langVal := gvr.GetValue().GetValue().GetListValue().GetValues() + if len(langVal) > 0 && langVal[0] != nil { + return langVal[0].GetStringValue(), gvr.GetValue().GetValue().GetId(), nil + } + return "", "", errors.New("no language value found") +} + // DeleteUser implements the Service interface. func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) { logger := g.logger.SubloggerWithRequestID(r.Context()) @@ -599,11 +629,41 @@ func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) { render.NoContent(w, r) } +// PatchMe implements the Service Interface. Updates the specified attributes of the current user +func (g Graph) PatchMe(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Debug().Msg("calling patch me") + userID := revactx.ContextMustGetUser(r.Context()).GetId().GetOpaqueId() + if userID == "" { + logger.Debug().Msg("could not update user: missing user id") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing user id") + return + } + changes := libregraph.NewUser() + err := StrictJSONUnmarshal(r.Body, changes) + if err != nil { + logger.Debug().Err(err).Interface("body", r.Body).Msg("could not update user: invalid request body") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, + fmt.Sprintf("invalid request body: %s", err.Error())) + return + } + if _, ok := changes.GetDisplayNameOk(); ok { + logger.Info().Interface("user", changes).Msg("could not update user: user is not allowed to change own displayname") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "user is not allowed to change own displayname") + return + } + if _, ok := changes.GetMailOk(); ok { + logger.Info().Interface("user", changes).Msg("could not update user: user is not allowed to change own mail") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "user is not allowed to change own mail") + return + } + g.patchUser(w, r, userID, changes) +} + // PatchUser implements the Service Interface. Updates the specified attributes of an // ExistingUser func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) { logger := g.logger.SubloggerWithRequestID(r.Context()) - logger.Debug().Msg("calling patch user") nameOrID := chi.URLParam(r, "userID") nameOrID, err := url.PathUnescape(nameOrID) if err != nil { @@ -611,6 +671,26 @@ func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) { errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping user id failed") return } + changes := libregraph.NewUser() + err = StrictJSONUnmarshal(r.Body, changes) + if err != nil { + logger.Debug().Err(err).Interface("body", r.Body).Msg("could not update user: invalid request body") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, + fmt.Sprintf("invalid request body: %s", err.Error())) + return + } + if _, ok := changes.GetPreferredLanguageOk(); ok { + logger.Info().Interface("user", changes).Msg("could not update user: user is not allowed to change other users language") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "user is not allowed to change other users language") + return + } + + g.patchUser(w, r, nameOrID, changes) +} + +func (g Graph) patchUser(w http.ResponseWriter, r *http.Request, nameOrID string, changes *libregraph.User) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Debug().Msg("calling patch user") sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/") @@ -633,14 +713,6 @@ func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) { errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing user id") return } - changes := libregraph.NewUser() - err = StrictJSONUnmarshal(r.Body, changes) - if err != nil { - logger.Debug().Err(err).Interface("body", r.Body).Msg("could not update user: invalid request body") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, - fmt.Sprintf("invalid request body: %s", err.Error())) - return - } if reflect.ValueOf(*changes).IsZero() { logger.Debug().Interface("body", r.Body).Msg("ignoring empty request body") @@ -657,6 +729,47 @@ func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) { } } + preferredLanguage, ok := changes.GetPreferredLanguageOk() + if ok { + _, vID, err := getUserLanguage(r.Context(), g.valueService, oldUserValues.GetId()) + if err != nil { + logger.Error().Err(err).Msg("could not get user language") + tvID, err := uuid.NewUUID() + if err != nil { + logger.Error().Err(err).Msg("could not create user: error generating uuid") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "error generating uuid") + return + } + vID = tvID.String() + } + _, err = g.valueService.SaveValue(r.Context(), &settings.SaveValueRequest{ + Value: &settingsmsg.Value{ + Id: vID, + BundleId: defaults.BundleUUIDProfile, + SettingId: defaults.SettingUUIDProfileLanguage, + AccountUuid: oldUserValues.GetId(), + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_USER, + }, + Value: &settingsmsg.Value_ListValue{ + ListValue: &settingsmsg.ListValue{Values: []*settingsmsg.ListOptionValue{ + { + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: *preferredLanguage, + }, + }, + }}, + }, + }, + }) + if err != nil { + logger.Error().Err(err).Msg("could not update user: error saving language setting") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "error saving language setting") + return + } + + } + var features []events.UserFeature if mail, ok := changes.GetMailOk(); ok { if !isValidEmail(*mail) { @@ -715,6 +828,7 @@ func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) { errorcode.RenderError(w, r, err) return } + u.PreferredLanguage = preferredLanguage e := events.UserFeatureChanged{ UserID: nameOrID, diff --git a/services/graph/pkg/service/v0/users_test.go b/services/graph/pkg/service/v0/users_test.go index 8e67a5152d2..44f271e3ffc 100644 --- a/services/graph/pkg/service/v0/users_test.go +++ b/services/graph/pkg/service/v0/users_test.go @@ -13,14 +13,18 @@ import ( userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - revactx "github.com/cs3org/reva/v2/pkg/ctx" - "github.com/cs3org/reva/v2/pkg/rgrpc/status" - "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" - cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks" "github.com/go-chi/chi/v5" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/stretchr/testify/mock" + "go-micro.dev/v4/client" + "google.golang.org/grpc" + + revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/rgrpc/status" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks" "github.com/owncloud/ocis/v2/ocis-pkg/shared" settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" @@ -29,9 +33,6 @@ import ( "github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults" identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks" service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0" - "github.com/stretchr/testify/mock" - "go-micro.dev/v4/client" - "google.golang.org/grpc" ) type userList struct { @@ -47,6 +48,7 @@ var _ = Describe("Users", func() { gatewaySelector pool.Selectable[gateway.GatewayAPIClient] eventsPublisher mocks.Publisher roleService *mocks.RoleService + valueService *mocks.ValueService identityBackend *identitymocks.Backend rr *httptest.ResponseRecorder @@ -73,6 +75,7 @@ var _ = Describe("Users", func() { identityBackend = &identitymocks.Backend{} roleService = &mocks.RoleService{} + valueService = &mocks.ValueService{} rr = httptest.NewRecorder() ctx = context.Background() @@ -90,6 +93,7 @@ var _ = Describe("Users", func() { service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), service.WithRoleService(roleService), + service.WithValueService(valueService), ) }) @@ -102,6 +106,25 @@ var _ = Describe("Users", func() { }) It("gets the information", func() { + valueService.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything, mock.Anything). + Return(&settings.GetValueResponse{ + Value: &settingsmsg.ValueWithIdentifier{ + Value: &settingsmsg.Value{ + Value: &settingsmsg.Value_ListValue{ + ListValue: &settingsmsg.ListValue{ + Values: []*settingsmsg.ListOptionValue{ + { + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "it", + }, + }, + }, + }, + }, + }, + }, + }, nil) + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me", nil) r = r.WithContext(revactx.ContextSetUser(ctx, currentUser)) svc.GetMe(rr, r) @@ -117,7 +140,24 @@ var _ = Describe("Users", func() { }, } identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil) - + valueService.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything, mock.Anything). + Return(&settings.GetValueResponse{ + Value: &settingsmsg.ValueWithIdentifier{ + Value: &settingsmsg.Value{ + Value: &settingsmsg.Value_ListValue{ + ListValue: &settingsmsg.ListValue{ + Values: []*settingsmsg.ListOptionValue{ + { + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "it", + }, + }, + }, + }, + }, + }, + }, + }, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me?$expand=memberOf", nil) r = r.WithContext(revactx.ContextSetUser(ctx, currentUser)) svc.GetMe(rr, r) @@ -145,7 +185,24 @@ var _ = Describe("Users", func() { }, } roleService.On("ListRoleAssignments", mock.Anything, mock.Anything, mock.Anything).Return(&settings.ListRoleAssignmentsResponse{Assignments: assignments}, nil) - + valueService.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything, mock.Anything). + Return(&settings.GetValueResponse{ + Value: &settingsmsg.ValueWithIdentifier{ + Value: &settingsmsg.Value{ + Value: &settingsmsg.Value_ListValue{ + ListValue: &settingsmsg.ListValue{ + Values: []*settingsmsg.ListOptionValue{ + { + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "it", + }, + }, + }, + }, + }, + }, + }, + }, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me?$expand=appRoleAssignments", nil) r = r.WithContext(revactx.ContextSetUser(ctx, currentUser)) svc.GetMe(rr, r) @@ -413,6 +470,24 @@ var _ = Describe("Users", func() { user.SetId("user1") identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil) + valueService.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything, mock.Anything). + Return(&settings.GetValueResponse{ + Value: &settingsmsg.ValueWithIdentifier{ + Value: &settingsmsg.Value{ + Value: &settingsmsg.Value_ListValue{ + ListValue: &settingsmsg.ListValue{ + Values: []*settingsmsg.ListOptionValue{ + { + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "it", + }, + }, + }, + }, + }, + }, + }, + }, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/users", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("userID", *user.Id) @@ -455,7 +530,24 @@ var _ = Describe("Users", func() { }, }, }, nil) - + valueService.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything, mock.Anything). + Return(&settings.GetValueResponse{ + Value: &settingsmsg.ValueWithIdentifier{ + Value: &settingsmsg.Value{ + Value: &settingsmsg.Value_ListValue{ + ListValue: &settingsmsg.ListValue{ + Values: []*settingsmsg.ListOptionValue{ + { + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "it", + }, + }, + }, + }, + }, + }, + }, + }, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/users?$expand=drive", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("userID", *user.Id) @@ -490,7 +582,24 @@ var _ = Describe("Users", func() { }, }, }, nil) - + valueService.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything, mock.Anything). + Return(&settings.GetValueResponse{ + Value: &settingsmsg.ValueWithIdentifier{ + Value: &settingsmsg.Value{ + Value: &settingsmsg.Value_ListValue{ + ListValue: &settingsmsg.ListValue{ + Values: []*settingsmsg.ListOptionValue{ + { + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "it", + }, + }, + }, + }, + }, + }, + }, + }, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/users?$expand=drives", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("userID", *user.Id) @@ -521,7 +630,24 @@ var _ = Describe("Users", func() { }, } roleService.On("ListRoleAssignments", mock.Anything, mock.Anything, mock.Anything).Return(&settings.ListRoleAssignmentsResponse{Assignments: assignments}, nil) - + valueService.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything, mock.Anything). + Return(&settings.GetValueResponse{ + Value: &settingsmsg.ValueWithIdentifier{ + Value: &settingsmsg.Value{ + Value: &settingsmsg.Value_ListValue{ + ListValue: &settingsmsg.ListValue{ + Values: []*settingsmsg.ListOptionValue{ + { + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "it", + }, + }, + }, + }, + }, + }, + }, + }, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/users/user1?$expand=appRoleAssignments", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("userID", user.GetId()) diff --git a/services/settings/pkg/service/v0/service.go b/services/settings/pkg/service/v0/service.go index 48897b1a09e..9513be9d6e8 100644 --- a/services/settings/pkg/service/v0/service.go +++ b/services/settings/pkg/service/v0/service.go @@ -274,7 +274,6 @@ func (g Service) RemoveSettingFromBundle(ctx context.Context, req *settingssvc.R // SaveValue implements the ValueServiceHandler interface func (g Service) SaveValue(ctx context.Context, req *settingssvc.SaveValueRequest, res *settingssvc.SaveValueResponse) error { req.Value.AccountUuid = getValidatedAccountUUID(ctx, req.Value.AccountUuid) - if !g.isCurrentUser(ctx, req.Value.AccountUuid) { return merrors.Forbidden(g.id, "can't save value for another user") } diff --git a/vendor/github.com/owncloud/libre-graph-api-go/README.md b/vendor/github.com/owncloud/libre-graph-api-go/README.md index 9e4810624b4..00f539bd35f 100644 --- a/vendor/github.com/owncloud/libre-graph-api-go/README.md +++ b/vendor/github.com/owncloud/libre-graph-api-go/README.md @@ -134,6 +134,7 @@ Class | Method | HTTP request | Description *MeDriveRootChildrenApi* | [**HomeGetChildren**](docs/MeDriveRootChildrenApi.md#homegetchildren) | **Get** /v1.0/me/drive/root/children | Get children from drive *MeDrivesApi* | [**ListMyDrives**](docs/MeDrivesApi.md#listmydrives) | **Get** /v1.0/me/drives | Get all drives where the current user is a regular member of *MeUserApi* | [**GetOwnUser**](docs/MeUserApi.md#getownuser) | **Get** /v1.0/me | Get current user +*MeUserApi* | [**UpdateOwnUser**](docs/MeUserApi.md#updateownuser) | **Patch** /v1.0/me | Update the current user *RoleManagementApi* | [**GetPermissionRoleDefinition**](docs/RoleManagementApi.md#getpermissionroledefinition) | **Get** /v1beta1/roleManagement/permissions/roleDefinitions/{role-id} | Get unifiedRoleDefinition *RoleManagementApi* | [**ListPermissionRoleDefinitions**](docs/RoleManagementApi.md#listpermissionroledefinitions) | **Get** /v1beta1/roleManagement/permissions/roleDefinitions | List roleDefinitions *TagsApi* | [**AssignTags**](docs/TagsApi.md#assigntags) | **Put** /v1.0/extensions/org.libregraph/tags | Assign tags to a resource diff --git a/vendor/github.com/owncloud/libre-graph-api-go/api_me_user.go b/vendor/github.com/owncloud/libre-graph-api-go/api_me_user.go index 3d9820dfaf9..5613c7fc76b 100644 --- a/vendor/github.com/owncloud/libre-graph-api-go/api_me_user.go +++ b/vendor/github.com/owncloud/libre-graph-api-go/api_me_user.go @@ -135,3 +135,117 @@ func (a *MeUserApiService) GetOwnUserExecute(r ApiGetOwnUserRequest) (*User, *ht return localVarReturnValue, localVarHTTPResponse, nil } + +type ApiUpdateOwnUserRequest struct { + ctx context.Context + ApiService *MeUserApiService + user *User +} + +// New user values +func (r ApiUpdateOwnUserRequest) User(user User) ApiUpdateOwnUserRequest { + r.user = &user + return r +} + +func (r ApiUpdateOwnUserRequest) Execute() (*User, *http.Response, error) { + return r.ApiService.UpdateOwnUserExecute(r) +} + +/* +UpdateOwnUser Update the current user + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiUpdateOwnUserRequest +*/ +func (a *MeUserApiService) UpdateOwnUser(ctx context.Context) ApiUpdateOwnUserRequest { + return ApiUpdateOwnUserRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// @return User +func (a *MeUserApiService) UpdateOwnUserExecute(r ApiUpdateOwnUserRequest) (*User, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPatch + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *User + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MeUserApiService.UpdateOwnUser") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1.0/me" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.user + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v OdataError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/vendor/github.com/owncloud/libre-graph-api-go/model_user.go b/vendor/github.com/owncloud/libre-graph-api-go/model_user.go index b3170f2ea33..4a6262a73fe 100644 --- a/vendor/github.com/owncloud/libre-graph-api-go/model_user.go +++ b/vendor/github.com/owncloud/libre-graph-api-go/model_user.go @@ -45,6 +45,8 @@ type User struct { GivenName *string `json:"givenName,omitempty"` // The user`s type. This can be either \"Member\" for regular user, or \"Guest\" for guest users. UserType *string `json:"userType,omitempty"` + // Represents the users language setting, ISO-639-1 Code + PreferredLanguage *string `json:"preferredLanguage,omitempty"` } // NewUser instantiates a new User object @@ -512,6 +514,38 @@ func (o *User) SetUserType(v string) { o.UserType = &v } +// GetPreferredLanguage returns the PreferredLanguage field value if set, zero value otherwise. +func (o *User) GetPreferredLanguage() string { + if o == nil || IsNil(o.PreferredLanguage) { + var ret string + return ret + } + return *o.PreferredLanguage +} + +// GetPreferredLanguageOk returns a tuple with the PreferredLanguage field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *User) GetPreferredLanguageOk() (*string, bool) { + if o == nil || IsNil(o.PreferredLanguage) { + return nil, false + } + return o.PreferredLanguage, true +} + +// HasPreferredLanguage returns a boolean if a field has been set. +func (o *User) HasPreferredLanguage() bool { + if o != nil && !IsNil(o.PreferredLanguage) { + return true + } + + return false +} + +// SetPreferredLanguage gets a reference to the given string and assigns it to the PreferredLanguage field. +func (o *User) SetPreferredLanguage(v string) { + o.PreferredLanguage = &v +} + func (o User) MarshalJSON() ([]byte, error) { toSerialize, err := o.ToMap() if err != nil { @@ -564,6 +598,9 @@ func (o User) ToMap() (map[string]interface{}, error) { if !IsNil(o.UserType) { toSerialize["userType"] = o.UserType } + if !IsNil(o.PreferredLanguage) { + toSerialize["preferredLanguage"] = o.PreferredLanguage + } return toSerialize, nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 9775a1059cb..b7246cc7305 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1555,7 +1555,7 @@ github.com/opentracing/opentracing-go/log # github.com/orcaman/concurrent-map v1.0.0 ## explicit github.com/orcaman/concurrent-map -# github.com/owncloud/libre-graph-api-go v1.0.5-0.20231107135330-011e9d4c45e3 +# github.com/owncloud/libre-graph-api-go v1.0.5-0.20231113143725-09bf34dc9afb ## explicit; go 1.18 github.com/owncloud/libre-graph-api-go # github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c