Persist preferred language for user

Signed-off-by: Christian Richter <crichter@owncloud.com>
Co-authored-by: Julian Koberg <jkoberg@owncloud.com>
Co-authored-by: Michael Barz <mbarz@owncloud.com>
This commit is contained in:
Christian Richter
2023-11-13 13:20:36 +01:00
parent a3f037a53e
commit 174097214f
7 changed files with 113 additions and 206 deletions
+3
View File
@@ -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)),
+1
View File
@@ -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
-108
View File
@@ -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)
}
@@ -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"))
})
})
+8
View File
@@ -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) {
+2 -5
View File
@@ -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)
+99 -4
View File
@@ -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,