diff --git a/services/graph/pkg/server/http/server.go b/services/graph/pkg/server/http/server.go index 027599665f..113ffac600 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 ffb174c661..36e3c3b7b4 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/language.go b/services/graph/pkg/service/v0/language.go deleted file mode 100644 index 3727c55a28..0000000000 --- a/services/graph/pkg/service/v0/language.go +++ /dev/null @@ -1,108 +0,0 @@ -package svc - -import ( - "github.com/CiscoM31/godata" - revactx "github.com/cs3org/reva/v2/pkg/ctx" - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" - "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" - "net/http" - "strings" -) - -// GetOwnLanguage returns the language of the current user. -func (g Graph) GetOwnLanguage(w http.ResponseWriter, r *http.Request) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - g.logger.Debug().Msg("Calling GetOwnLanguage") - sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/") - - odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query()) - if err != nil { - logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: query error") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) - return - } - - u, ok := revactx.ContextGetUser(r.Context()) - if !ok { - logger.Debug().Msg("could not get user: user not in context") - errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context") - return - } - - me, err := g.identityBackend.GetUser(r.Context(), u.GetId().GetOpaqueId(), odataReq) - if err != nil { - logger.Debug().Err(err).Interface("user", u).Msg("could not get user from backend") - errorcode.RenderError(w, r, err) - return - } - - // TODO: make sure that this actually returns the stored language - lang, ok := me.GetPreferredLanguageOk() - if !ok { - render.Status(r, http.StatusNotFound) - render.JSON(w, r, nil) - return - } - - render.Status(r, http.StatusOK) - render.JSON(w, r, lang) -} - -// SetOwnLanguage sets the language of the current user. -func (g Graph) SetOwnLanguage(w http.ResponseWriter, r *http.Request) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - g.logger.Debug().Msg("Calling SetOwnLanguage") - sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/") - - odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query()) - if err != nil { - logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: query error") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) - return - } - - u, ok := revactx.ContextGetUser(r.Context()) - if !ok { - logger.Debug().Msg("could not get user: user not in context") - errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context") - return - } - - me, err := g.identityBackend.GetUser(r.Context(), u.GetId().GetOpaqueId(), odataReq) - if err != nil { - logger.Debug().Err(err).Interface("user", u).Msg("could not get user from backend") - errorcode.RenderError(w, r, err) - return - } - - lang := chi.URLParam(r, "language") - me.SetPreferredLanguage(lang) - // TODO: persist this change - render.Status(r, http.StatusNoContent) -} - -func (g Graph) SetUserLanguage(w http.ResponseWriter, r *http.Request) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - g.logger.Debug().Msg("Calling SetUserLanguage") - sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/") - - odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query()) - if err != nil { - logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: query error") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) - return - } - - user, err := g.identityBackend.GetUser(r.Context(), chi.URLParam(r, "userID"), odataReq) - if err != nil { - logger.Debug().Err(err).Interface("user", user.GetId()).Msg("could not get user from backend") - errorcode.RenderError(w, r, err) - return - } - - lang := chi.URLParam(r, "language") - user.SetPreferredLanguage(lang) - // TODO: persist this change - render.Status(r, http.StatusNoContent) -} diff --git a/services/graph/pkg/service/v0/language_test.go b/services/graph/pkg/service/v0/language_test.go deleted file mode 100644 index ce046c3488..0000000000 --- a/services/graph/pkg/service/v0/language_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package svc_test - -import ( - libregraph "github.com/owncloud/libre-graph-api-go" - "net/http/httptest" - - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" - cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/owncloud/ocis/v2/ocis-pkg/shared" - "github.com/owncloud/ocis/v2/services/graph/mocks" - "github.com/owncloud/ocis/v2/services/graph/pkg/config" - "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" - "google.golang.org/grpc" -) - -var _ = Describe("Language", func() { - var ( - svc service.Service - //ctx context.Context - cfg *config.Config - gatewayClient *cs3mocks.GatewayAPIClient - gatewaySelector pool.Selectable[gateway.GatewayAPIClient] - eventsPublisher mocks.Publisher - identityBackend *identitymocks.Backend - - rr *httptest.ResponseRecorder - ) - - BeforeEach(func() { - eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil) - - pool.RemoveSelector("GatewaySelector" + "com.owncloud.api.gateway") - gatewayClient = &cs3mocks.GatewayAPIClient{} - gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient]( - "GatewaySelector", - "com.owncloud.api.gateway", - func(cc *grpc.ClientConn) gateway.GatewayAPIClient { - return gatewayClient - }, - ) - - identityBackend = &identitymocks.Backend{} - - rr = httptest.NewRecorder() - //ctx = context.Background() - - cfg = defaults.FullDefaultConfig() - cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests - cfg.TokenManager.JWTSecret = "loremipsum" - cfg.Commons = &shared.Commons{} - cfg.GRPCClientTLS = &shared.GRPCClientTLS{} - - svc, _ = service.NewService( - service.Config(cfg), - service.WithGatewaySelector(gatewaySelector), - service.EventsPublisher(&eventsPublisher), - service.WithIdentityBackend(identityBackend), - ) - - }) - - It("should return the language of the current user", func() { - user := libregraph.NewUser() - user.SetId("disallowed") - user.SetDisplayName("foobar") - user.SetPreferredLanguage("en-EN") - - r := httptest.NewRequest("GET", "/graph/v1.0/me/language", nil) - svc.(*service.Graph).GetOwnLanguage(rr, r) - Expect(rr.Code).To(Equal(200)) - Expect(rr.Body.String()).To(Equal("en-EN")) - - }) - - It("should set the language of the current user", func() { - r := httptest.NewRequest("PUT", "/graph/v1.0/me/language/en-EN", nil) - svc.(*service.Graph).SetOwnLanguage(rr, r) - Expect(rr.Code).To(Equal(204)) - svc.(*service.Graph).GetOwnLanguage(rr, r) - Expect(rr.Code).To(Equal(200)) - Expect(rr.Body.String()).To(Equal("en-EN")) - }) -}) diff --git a/services/graph/pkg/service/v0/option.go b/services/graph/pkg/service/v0/option.go index 9ed23fb4a1..f68633b48b 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 8a4728aed4..3012bbd729 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 { @@ -215,12 +216,9 @@ func NewService(opts ...Option) (Graph, error) { r.Route("/drives", func(r chi.Router) { r.Get("/", svc.GetDrives) }) - r.Route("/language", func(r chi.Router) { - r.Get("/", svc.GetOwnLanguage) - r.Post("/{language}", svc.SetOwnLanguage) - }) 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) @@ -231,7 +229,6 @@ func NewService(opts ...Option) (Graph, error) { r.Post("/exportPersonalData", svc.ExportPersonalData) r.With(requireAdmin).Delete("/", svc.DeleteUser) r.With(requireAdmin).Patch("/", svc.PatchUser) - r.With(requireAdmin).Patch("/language/{language}", svc.SetUserLanguage) if svc.roleService != nil { r.With(requireAdmin).Route("/appRoleAssignments", func(r chi.Router) { r.Get("/", svc.ListAppRoleAssignments) diff --git a/services/graph/pkg/service/v0/users.go b/services/graph/pkg/service/v0/users.go index 097e4fa654..c42b97f620 100644 --- a/services/graph/pkg/service/v0/users.go +++ b/services/graph/pkg/service/v0/users.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults" "net/http" "net/url" "reflect" @@ -22,10 +23,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 +88,16 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { } } + preferedLanguage, _, err := getUserLanguage(r.Context(), g.valueService) + if err != nil { + logger.Error().Err(err).Msg("could not get user language") + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, me) + 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 } @@ -475,10 +488,37 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) { } } + preferedLanguage, _, err := getUserLanguage(r.Context(), g.valueService) + if err != nil { + logger.Error().Err(err).Msg("could not get user language") + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, user) + return + } + + user.PreferredLanguage = &preferedLanguage + render.Status(r, http.StatusOK) render.JSON(w, r, user) } +// getUserLanguage returns the language of the user in the context. +func getUserLanguage(ctx context.Context, valueService settingssvc.ValueService) (string, string, error) { + gvr, err := valueService.GetValueByUniqueIdentifiers(ctx, &settingssvc.GetValueByUniqueIdentifiersRequest{ + AccountUuid: revactx.ContextMustGetUser(ctx).GetId().GetOpaqueId(), + 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 +639,23 @@ 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 +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 + } + g.patchUser(w, r, userID) +} + // 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 +663,12 @@ func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) { errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping user id failed") return } + g.patchUser(w, r, nameOrID) +} + +func (g Graph) patchUser(w http.ResponseWriter, r *http.Request, nameOrID string) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Debug().Msg("calling patch user") sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/") @@ -657,6 +715,42 @@ func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) { } } + preferredLanguage, ok := changes.GetPreferredLanguageOk() + if ok { + _, vID, err := getUserLanguage(r.Context(), g.valueService) + 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 + } + _, err = g.valueService.SaveValue(r.Context(), &settings.SaveValueRequest{ + Value: &settingsmsg.Value{ + Id: vID, + BundleId: defaults.BundleUUIDProfile, + SettingId: defaults.SettingUUIDProfileLanguage, + AccountUuid: nameOrID, + 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 +809,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,