From e23b8f412b4f273d111f415cc10fceb579801c3d Mon Sep 17 00:00:00 2001 From: Christian Richter Date: Thu, 8 May 2025 16:26:35 +0200 Subject: [PATCH 01/11] add skel for photos Signed-off-by: Christian Richter --- services/graph/pkg/service/v0/photo.go | 56 ++++++++++++++++++++++++ services/graph/pkg/service/v0/service.go | 2 + 2 files changed, 58 insertions(+) create mode 100644 services/graph/pkg/service/v0/photo.go diff --git a/services/graph/pkg/service/v0/photo.go b/services/graph/pkg/service/v0/photo.go new file mode 100644 index 0000000000..a3cafc299d --- /dev/null +++ b/services/graph/pkg/service/v0/photo.go @@ -0,0 +1,56 @@ +package svc + +import ( + "net/http" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/go-chi/render" + "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" +) + +// GetMePhoto implements the Service interface +func (g Graph) GetMePhoto(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Debug().Msg("GetMePhoto called") + 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 + } + g.GetPhoto(w, r, u.GetId()) +} + +// GetPhoto implements the Service interface +func (g Graph) GetPhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Debug().Msg("GetPhoto called") + + // TODO: use proper default return + render.Status(r, http.StatusNotFound) + render.JSON(w, r, nil) +} + +// UpdateMePhoto implements the Service interface +func (g Graph) UpdateMePhoto(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Debug().Msg("UpdateMePhoto called") + 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 + } + g.UpdatePhoto(w, r, u.GetId()) +} + +// UpdatePhoto implements the Service interface +func (g Graph) UpdatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Debug().Msg("UpdatePhoto called") + + // TODO: use proper default return + render.Status(r, http.StatusForbidden) + render.JSON(w, r, nil) +} diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 9881a9bdbd..24d84c48da 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -293,6 +293,8 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx }) r.Get("/drives", svc.GetDrives(APIVersion_1)) r.Post("/changePassword", svc.ChangeOwnPassword) + r.Get("/photo", svc.GetMePhoto) + r.Put("/photo", svc.UpdateMePhoto) }) r.Route("/users", func(r chi.Router) { r.Get("/", svc.GetUsers) From a24ead6d20125299da1c76222ef4fbcd97c920c9 Mon Sep 17 00:00:00 2001 From: Christian Richter Date: Thu, 8 May 2025 17:34:37 +0200 Subject: [PATCH 02/11] support getting photos for other users Signed-off-by: Christian Richter --- services/graph/pkg/service/v0/photo.go | 40 +++++++++++++++++++++--- services/graph/pkg/service/v0/service.go | 2 ++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/services/graph/pkg/service/v0/photo.go b/services/graph/pkg/service/v0/photo.go index a3cafc299d..7bbee839a1 100644 --- a/services/graph/pkg/service/v0/photo.go +++ b/services/graph/pkg/service/v0/photo.go @@ -2,8 +2,10 @@ package svc import ( "net/http" + "net/url" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" @@ -19,14 +21,28 @@ func (g Graph) GetMePhoto(w http.ResponseWriter, r *http.Request) { errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context") return } - g.GetPhoto(w, r, u.GetId()) + g.getPhoto(w, r, u.GetId()) } // GetPhoto implements the Service interface -func (g Graph) GetPhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { +func (g Graph) GetPhoto(w http.ResponseWriter, r *http.Request) { logger := g.logger.SubloggerWithRequestID(r.Context()) logger.Debug().Msg("GetPhoto called") - + userID, err := url.PathUnescape(chi.URLParam(r, "userID")) + if err != nil { + logger.Debug().Err(err).Str("userID", chi.URLParam(r, "userID")).Msg("could not get drive: unescaping drive id failed") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping user id failed") + return + } + g.getPhoto(w, r, &userpb.UserId{ + OpaqueId: userID, + }) +} + +func (g Graph) getPhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Debug().Msg("GetPhoto called") + // TODO: use proper default return render.Status(r, http.StatusNotFound) render.JSON(w, r, nil) @@ -42,11 +58,25 @@ func (g Graph) UpdateMePhoto(w http.ResponseWriter, r *http.Request) { errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context") return } - g.UpdatePhoto(w, r, u.GetId()) + g.updatePhoto(w, r, u.GetId()) } // UpdatePhoto implements the Service interface -func (g Graph) UpdatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { +func (g Graph) UpdatePhoto(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Debug().Msg("UpdatePhoto called") + userID, err := url.PathUnescape(chi.URLParam(r, "userID")) + if err != nil { + logger.Debug().Err(err).Str("userID", chi.URLParam(r, "userID")).Msg("could not get drive: unescaping drive id failed") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping user id failed") + return + } + g.updatePhoto(w, r, &userpb.UserId{ + OpaqueId: userID, + }) +} + +func (g Graph) updatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { logger := g.logger.SubloggerWithRequestID(r.Context()) logger.Debug().Msg("UpdatePhoto called") diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 24d84c48da..bdb86e4a0d 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -312,6 +312,8 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx r.Delete("/{appRoleAssignmentID}", svc.DeleteAppRoleAssignment) }) } + r.Get("/photo", svc.GetPhoto) + r.Put("/photo", svc.UpdatePhoto) }) }) r.Route("/groups", func(r chi.Router) { From 19c870425e8e46e8f1ea24f78fa38c988805bc70 Mon Sep 17 00:00:00 2001 From: Christian Richter Date: Fri, 9 May 2025 08:58:53 +0200 Subject: [PATCH 03/11] add missing patch route Signed-off-by: Christian Richter --- services/graph/pkg/service/v0/service.go | 1 + 1 file changed, 1 insertion(+) diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index bdb86e4a0d..89ad821bb7 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -295,6 +295,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx r.Post("/changePassword", svc.ChangeOwnPassword) r.Get("/photo", svc.GetMePhoto) r.Put("/photo", svc.UpdateMePhoto) + r.Patch("/photo", svc.UpdateMePhoto) }) r.Route("/users", func(r chi.Router) { r.Get("/", svc.GetUsers) From df93ea464917520f1c20f176a8ac455825774974 Mon Sep 17 00:00:00 2001 From: Christian Richter Date: Tue, 13 May 2025 16:02:41 +0200 Subject: [PATCH 04/11] begin implementation systemstorageclient Signed-off-by: Christian Richter --- .../systemstorageclient.go | 56 +++++++++++++++++++ services/graph/pkg/config/config.go | 13 +++++ .../pkg/config/defaults/defaultconfig.go | 11 ++++ services/graph/pkg/service/v0/graph.go | 2 + services/graph/pkg/service/v0/photo.go | 38 ++++++++++--- 5 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 pkg/systemstorageclient/systemstorageclient.go diff --git a/pkg/systemstorageclient/systemstorageclient.go b/pkg/systemstorageclient/systemstorageclient.go new file mode 100644 index 0000000000..bb87ee1394 --- /dev/null +++ b/pkg/systemstorageclient/systemstorageclient.go @@ -0,0 +1,56 @@ +package systemstorageclient + +import ( + "context" + "github.com/opencloud-eu/opencloud/pkg/log" + + "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" +) + +type SystemDataStorageClient struct { + mds *metadata.Storage +} + +func (s SystemDataStorageClient) SimpleDownload(ctx context.Context, userID, identifier string) ([]byte, error) { + //TODO implement me + panic("implement me") +} + +func (s SystemDataStorageClient) SimpleUpload(ctx context.Context, userID, identifier string, content []byte) error { + //TODO implement me + panic("implement me") +} + +func (s SystemDataStorageClient) Delete(ctx context.Context, userID, identifier string) error { + //TODO implement me + panic("implement me") +} + +func (s SystemDataStorageClient) ReadDir(ctx context.Context, userID, identifier string) ([]string, error) { + //TODO implement me + panic("implement me") +} + +func (s SystemDataStorageClient) MakeDirIfNotExist(ctx context.Context, userID, identifier string) error { + //TODO implement me + panic("implement me") +} + +func (s SystemDataStorageClient) Init(ctx context.Context, userID, identifier string) error { + //TODO implement me + panic("implement me") +} + +// NewProfileStorageClient creates a new ProfileStorageClient +func NewSystemStorageClient(nameSpace string, + logger *log.Logger, + gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey string) SystemDataStorageClient { + sdsci := SystemDataStorageClient{} + logger.Debug().Msg("NewSystemStorageClient called") + sdsc, err := metadata.NewCS3Storage(gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey) + if err != nil { + logger.Fatal().Err(err).Msg("could not create profile storage client") + } + sdsci.mds = &sdsc + return sdsci +} diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index 24491278dc..ce5d1a16d7 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -37,6 +37,8 @@ type Config struct { ServiceAccount ServiceAccount `yaml:"service_account"` Context context.Context `yaml:"-"` + + SystemStorageClient SystemStorageClient `yaml:"system_storage_client"` } type Spaces struct { @@ -153,3 +155,14 @@ type ServiceAccount struct { ServiceAccountID string `yaml:"service_account_id" env:"OC_SERVICE_ACCOUNT_ID;GRAPH_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"1.0.0"` ServiceAccountSecret string `yaml:"service_account_secret" env:"OC_SERVICE_ACCOUNT_SECRET;GRAPH_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"1.0.0"` } + +// SystemStorageClient configures the metadata store to use +type SystemStorageClient struct { + GatewayAddress string `yaml:"gateway_addr" env:"SETTINGS_STORAGE_GATEWAY_GRPC_ADDR;STORAGE_GATEWAY_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"1.0.0"` + StorageAddress string `yaml:"storage_addr" env:"SETTINGS_STORAGE_GRPC_ADDR;STORAGE_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"1.0.0"` + + SystemUserID string `yaml:"system_user_id" env:"OC_SYSTEM_USER_ID;SETTINGS_SYSTEM_USER_ID" desc:"ID of the OpenCloud STORAGE-SYSTEM system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"1.0.0"` + SystemUserIDP string `yaml:"system_user_idp" env:"OC_SYSTEM_USER_IDP;SETTINGS_SYSTEM_USER_IDP" desc:"IDP of the OpenCloud STORAGE-SYSTEM system user." introductionVersion:"1.0.0"` + SystemUserAPIKey string `yaml:"system_user_api_key" env:"OC_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user." introductionVersion:"1.0.0"` + Cache *Cache `yaml:"cache"` +} diff --git a/services/graph/pkg/config/defaults/defaultconfig.go b/services/graph/pkg/config/defaults/defaultconfig.go index 62985ccbe0..5c926c6ffc 100644 --- a/services/graph/pkg/config/defaults/defaultconfig.go +++ b/services/graph/pkg/config/defaults/defaultconfig.go @@ -125,6 +125,17 @@ func DefaultConfig() *config.Config { UnifiedRoles: config.UnifiedRoles{ AvailableRoles: nil, // will be populated with defaults in EnsureDefaults }, + SystemStorageClient: config.SystemStorageClient{ + GatewayAddress: "eu.opencloud.api.storage-system", + StorageAddress: "eu.opencloud.api.storage-system", + SystemUserIDP: "internal", + Cache: &config.Cache{ + Store: "memory", + Nodes: []string{"127.0.0.1:9233"}, + Database: "settings-cache", + TTL: time.Minute * 10, + }, + }, } } diff --git a/services/graph/pkg/service/v0/graph.go b/services/graph/pkg/service/v0/graph.go index dbcbef72bf..4cc1571637 100644 --- a/services/graph/pkg/service/v0/graph.go +++ b/services/graph/pkg/service/v0/graph.go @@ -3,6 +3,7 @@ package svc import ( "context" "errors" + "github.com/opencloud-eu/opencloud/pkg/systemstorageclient" "net/http" "net/url" "path" @@ -67,6 +68,7 @@ type Graph struct { keycloakClient keycloak.Client historyClient ehsvc.EventHistoryService traceProvider trace.TracerProvider + sdsc systemstorageclient.SystemDataStorageClient } // ServeHTTP implements the Service interface. diff --git a/services/graph/pkg/service/v0/photo.go b/services/graph/pkg/service/v0/photo.go index 7bbee839a1..6e18ddb6e2 100644 --- a/services/graph/pkg/service/v0/photo.go +++ b/services/graph/pkg/service/v0/photo.go @@ -1,14 +1,19 @@ package svc import ( - "net/http" - "net/url" - userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" "github.com/go-chi/chi/v5" "github.com/go-chi/render" + "github.com/opencloud-eu/opencloud/pkg/systemstorageclient" "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "net/http" + "net/url" +) + +var ( + namespace = "profilephoto" + identifier = "profilephoto" ) // GetMePhoto implements the Service interface @@ -42,10 +47,15 @@ func (g Graph) GetPhoto(w http.ResponseWriter, r *http.Request) { func (g Graph) getPhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { logger := g.logger.SubloggerWithRequestID(r.Context()) logger.Debug().Msg("GetPhoto called") + g.getSystemStorageClient() + photo, err := g.sdsc.SimpleDownload(r.Context(), u.GetOpaqueId(), identifier) + if err != nil { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, nil) + } + render.Status(r, http.StatusOK) + render.JSON(w, r, photo) - // TODO: use proper default return - render.Status(r, http.StatusNotFound) - render.JSON(w, r, nil) } // UpdateMePhoto implements the Service interface @@ -79,8 +89,22 @@ func (g Graph) UpdatePhoto(w http.ResponseWriter, r *http.Request) { func (g Graph) updatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { logger := g.logger.SubloggerWithRequestID(r.Context()) logger.Debug().Msg("UpdatePhoto called") - + g.getSystemStorageClient() + g.sdsc.SimpleUpload(r.Context(), u.GetOpaqueId(), identifier, []byte("test")) // TODO: use proper default return render.Status(r, http.StatusForbidden) render.JSON(w, r, nil) } + +func (g Graph) getSystemStorageClient() systemstorageclient.SystemDataStorageClient { + g.sdsc = systemstorageclient.NewSystemStorageClient( + namespace, + g.logger, + g.config.SystemStorageClient.GatewayAddress, + g.config.SystemStorageClient.StorageAddress, + g.config.SystemStorageClient.SystemUserID, + g.config.SystemStorageClient.SystemUserIDP, + g.config.SystemStorageClient.SystemUserAPIKey, + ) + return g.sdsc +} From 0f5855cef44227c8e93ad8a22af5578803483ba2 Mon Sep 17 00:00:00 2001 From: Christian Richter Date: Wed, 14 May 2025 11:12:24 +0200 Subject: [PATCH 05/11] pass upload data to storageclient Signed-off-by: Christian Richter --- .../systemstorageclient.go | 5 ++-- services/graph/pkg/service/v0/photo.go | 23 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/pkg/systemstorageclient/systemstorageclient.go b/pkg/systemstorageclient/systemstorageclient.go index bb87ee1394..826e722683 100644 --- a/pkg/systemstorageclient/systemstorageclient.go +++ b/pkg/systemstorageclient/systemstorageclient.go @@ -2,6 +2,7 @@ package systemstorageclient import ( "context" + "fmt" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" @@ -17,8 +18,8 @@ func (s SystemDataStorageClient) SimpleDownload(ctx context.Context, userID, ide } func (s SystemDataStorageClient) SimpleUpload(ctx context.Context, userID, identifier string, content []byte) error { - //TODO implement me - panic("implement me") + fmt.Println(content) + return nil } func (s SystemDataStorageClient) Delete(ctx context.Context, userID, identifier string) error { diff --git a/services/graph/pkg/service/v0/photo.go b/services/graph/pkg/service/v0/photo.go index 6e18ddb6e2..21d003376a 100644 --- a/services/graph/pkg/service/v0/photo.go +++ b/services/graph/pkg/service/v0/photo.go @@ -7,6 +7,7 @@ import ( "github.com/opencloud-eu/opencloud/pkg/systemstorageclient" "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "io" "net/http" "net/url" ) @@ -89,14 +90,30 @@ func (g Graph) UpdatePhoto(w http.ResponseWriter, r *http.Request) { func (g Graph) updatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { logger := g.logger.SubloggerWithRequestID(r.Context()) logger.Debug().Msg("UpdatePhoto called") + content, err := io.ReadAll(r.Body) + if err != nil { + logger.Debug().Err(err).Msg("could not read body") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "could not read body") + return + } + if len(content) == 0 { + logger.Debug().Msg("could not read body: empty body") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "empty body") + return + } g.getSystemStorageClient() - g.sdsc.SimpleUpload(r.Context(), u.GetOpaqueId(), identifier, []byte("test")) - // TODO: use proper default return - render.Status(r, http.StatusForbidden) + err = g.sdsc.SimpleUpload(r.Context(), u.GetOpaqueId(), identifier, content) + if err != nil { + logger.Debug().Err(err).Msg("could not upload photo") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not upload photo") + return + } + render.Status(r, http.StatusOK) render.JSON(w, r, nil) } func (g Graph) getSystemStorageClient() systemstorageclient.SystemDataStorageClient { + // TODO: this needs a check if the client is already initialized and if not, initialize it g.sdsc = systemstorageclient.NewSystemStorageClient( namespace, g.logger, From 9e7f4487ad895afa7717e5c7780298d368b6cdbe Mon Sep 17 00:00:00 2001 From: Christian Richter Date: Wed, 14 May 2025 12:38:57 +0200 Subject: [PATCH 06/11] fix wrong pointer Signed-off-by: Christian Richter --- pkg/systemstorageclient/systemstorageclient.go | 13 +++++++++---- services/graph/pkg/service/v0/photo.go | 4 +++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pkg/systemstorageclient/systemstorageclient.go b/pkg/systemstorageclient/systemstorageclient.go index 826e722683..87c9e6f254 100644 --- a/pkg/systemstorageclient/systemstorageclient.go +++ b/pkg/systemstorageclient/systemstorageclient.go @@ -9,7 +9,7 @@ import ( ) type SystemDataStorageClient struct { - mds *metadata.Storage + mds metadata.Storage } func (s SystemDataStorageClient) SimpleDownload(ctx context.Context, userID, identifier string) ([]byte, error) { @@ -18,7 +18,7 @@ func (s SystemDataStorageClient) SimpleDownload(ctx context.Context, userID, ide } func (s SystemDataStorageClient) SimpleUpload(ctx context.Context, userID, identifier string, content []byte) error { - fmt.Println(content) + s.mds.SimpleUpload(ctx, fmt.Sprintf("%s/%s", userID, identifier), content) return nil } @@ -43,15 +43,20 @@ func (s SystemDataStorageClient) Init(ctx context.Context, userID, identifier st } // NewProfileStorageClient creates a new ProfileStorageClient -func NewSystemStorageClient(nameSpace string, +func NewSystemStorageClient(scope, nameSpace string, logger *log.Logger, gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey string) SystemDataStorageClient { + + // scope: the scope the data should be persistet in (e.g. user) + // namespace: the namespace the data should be persistet in (e.g. profilephoto) + // results in the following path: //*//* + sdsci := SystemDataStorageClient{} logger.Debug().Msg("NewSystemStorageClient called") sdsc, err := metadata.NewCS3Storage(gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey) if err != nil { logger.Fatal().Err(err).Msg("could not create profile storage client") } - sdsci.mds = &sdsc + sdsci.mds = sdsc return sdsci } diff --git a/services/graph/pkg/service/v0/photo.go b/services/graph/pkg/service/v0/photo.go index 21d003376a..cbb814a972 100644 --- a/services/graph/pkg/service/v0/photo.go +++ b/services/graph/pkg/service/v0/photo.go @@ -14,6 +14,7 @@ import ( var ( namespace = "profilephoto" + scope = "user" identifier = "profilephoto" ) @@ -90,6 +91,7 @@ func (g Graph) UpdatePhoto(w http.ResponseWriter, r *http.Request) { func (g Graph) updatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { logger := g.logger.SubloggerWithRequestID(r.Context()) logger.Debug().Msg("UpdatePhoto called") + g.getSystemStorageClient() content, err := io.ReadAll(r.Body) if err != nil { logger.Debug().Err(err).Msg("could not read body") @@ -101,7 +103,6 @@ func (g Graph) updatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.Use errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "empty body") return } - g.getSystemStorageClient() err = g.sdsc.SimpleUpload(r.Context(), u.GetOpaqueId(), identifier, content) if err != nil { logger.Debug().Err(err).Msg("could not upload photo") @@ -115,6 +116,7 @@ func (g Graph) updatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.Use func (g Graph) getSystemStorageClient() systemstorageclient.SystemDataStorageClient { // TODO: this needs a check if the client is already initialized and if not, initialize it g.sdsc = systemstorageclient.NewSystemStorageClient( + scope, namespace, g.logger, g.config.SystemStorageClient.GatewayAddress, From 6e4cbf22301638051bd0b649b18da4ed622c976e Mon Sep 17 00:00:00 2001 From: Christian Richter Date: Wed, 14 May 2025 13:40:35 +0200 Subject: [PATCH 07/11] add new function Signed-off-by: Christian Richter --- .../systemstorageclient.go | 60 ++++++++++++------- services/graph/pkg/config/config.go | 8 +-- .../pkg/config/defaults/defaultconfig.go | 9 +++ services/graph/pkg/service/v0/photo.go | 6 +- 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/pkg/systemstorageclient/systemstorageclient.go b/pkg/systemstorageclient/systemstorageclient.go index 87c9e6f254..99b063e02d 100644 --- a/pkg/systemstorageclient/systemstorageclient.go +++ b/pkg/systemstorageclient/systemstorageclient.go @@ -2,61 +2,81 @@ package systemstorageclient import ( "context" - "fmt" - "github.com/opencloud-eu/opencloud/pkg/log" + "path" + "sync" + "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" ) +var ( + managerName = "systemdata" +) + type SystemDataStorageClient struct { mds metadata.Storage + l *sync.Mutex } -func (s SystemDataStorageClient) SimpleDownload(ctx context.Context, userID, identifier string) ([]byte, error) { +func (s *SystemDataStorageClient) SimpleDownload(ctx context.Context, userID, identifier string) ([]byte, error) { //TODO implement me panic("implement me") } -func (s SystemDataStorageClient) SimpleUpload(ctx context.Context, userID, identifier string, content []byte) error { - s.mds.SimpleUpload(ctx, fmt.Sprintf("%s/%s", userID, identifier), content) - return nil +func (s *SystemDataStorageClient) SimpleUpload(ctx context.Context, userID, identifier string, content []byte) error { + return s.mds.SimpleUpload(ctx, path.Join(userID, identifier), content) } -func (s SystemDataStorageClient) Delete(ctx context.Context, userID, identifier string) error { +func (s *SystemDataStorageClient) Delete(ctx context.Context, userID, identifier string) error { //TODO implement me panic("implement me") } -func (s SystemDataStorageClient) ReadDir(ctx context.Context, userID, identifier string) ([]string, error) { +func (s *SystemDataStorageClient) ReadDir(ctx context.Context, userID, identifier string) ([]string, error) { //TODO implement me panic("implement me") } -func (s SystemDataStorageClient) MakeDirIfNotExist(ctx context.Context, userID, identifier string) error { +func (s *SystemDataStorageClient) MakeDirIfNotExist(ctx context.Context, userID, identifier string) error { //TODO implement me panic("implement me") } -func (s SystemDataStorageClient) Init(ctx context.Context, userID, identifier string) error { - //TODO implement me - panic("implement me") +// New initialize the store once, later calls are noops +func (s *SystemDataStorageClient) New(ctx context.Context, + logger *log.Logger, + scope, namespace string, + gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey string, +) { + if s.mds != nil { + return + } + + s.l.Lock() + defer s.l.Unlock() + + if s.mds != nil { + return + } + + mds, err := metadata.NewCS3Storage(gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey) + if err != nil { + logger.Fatal().Err(err).Msg("could not create profile storage client") + } + s.mds = mds } // NewProfileStorageClient creates a new ProfileStorageClient func NewSystemStorageClient(scope, nameSpace string, logger *log.Logger, - gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey string) SystemDataStorageClient { + gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey string) *SystemDataStorageClient { // scope: the scope the data should be persistet in (e.g. user) // namespace: the namespace the data should be persistet in (e.g. profilephoto) // results in the following path: //*//* + // e.g. /user//profilephoto/profilephoto.jpg - sdsci := SystemDataStorageClient{} - logger.Debug().Msg("NewSystemStorageClient called") - sdsc, err := metadata.NewCS3Storage(gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey) - if err != nil { - logger.Fatal().Err(err).Msg("could not create profile storage client") - } - sdsci.mds = sdsc + sdsci := &SystemDataStorageClient{} + sdsci.New(context.TODO(), logger, scope, nameSpace, gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey) return sdsci } diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index ce5d1a16d7..3ae85a571c 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -158,11 +158,11 @@ type ServiceAccount struct { // SystemStorageClient configures the metadata store to use type SystemStorageClient struct { - GatewayAddress string `yaml:"gateway_addr" env:"SETTINGS_STORAGE_GATEWAY_GRPC_ADDR;STORAGE_GATEWAY_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"1.0.0"` - StorageAddress string `yaml:"storage_addr" env:"SETTINGS_STORAGE_GRPC_ADDR;STORAGE_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"1.0.0"` + GatewayAddress string `yaml:"gateway_addr" env:"GRAPH_STORAGE_GATEWAY_GRPC_ADDR;STORAGE_GATEWAY_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"1.0.0"` + StorageAddress string `yaml:"storage_addr" env:"GRAPH_STORAGE_GRPC_ADDR;STORAGE_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"1.0.0"` - SystemUserID string `yaml:"system_user_id" env:"OC_SYSTEM_USER_ID;SETTINGS_SYSTEM_USER_ID" desc:"ID of the OpenCloud STORAGE-SYSTEM system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"1.0.0"` - SystemUserIDP string `yaml:"system_user_idp" env:"OC_SYSTEM_USER_IDP;SETTINGS_SYSTEM_USER_IDP" desc:"IDP of the OpenCloud STORAGE-SYSTEM system user." introductionVersion:"1.0.0"` + SystemUserID string `yaml:"system_user_id" env:"OC_SYSTEM_USER_ID;GRAPH_SYSTEM_USER_ID" desc:"ID of the OpenCloud STORAGE-SYSTEM system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"1.0.0"` + SystemUserIDP string `yaml:"system_user_idp" env:"OC_SYSTEM_USER_IDP;GRAPH_SYSTEM_USER_IDP" desc:"IDP of the OpenCloud STORAGE-SYSTEM system user." introductionVersion:"1.0.0"` SystemUserAPIKey string `yaml:"system_user_api_key" env:"OC_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user." introductionVersion:"1.0.0"` Cache *Cache `yaml:"cache"` } diff --git a/services/graph/pkg/config/defaults/defaultconfig.go b/services/graph/pkg/config/defaults/defaultconfig.go index 5c926c6ffc..bc30304e28 100644 --- a/services/graph/pkg/config/defaults/defaultconfig.go +++ b/services/graph/pkg/config/defaults/defaultconfig.go @@ -202,6 +202,15 @@ func EnsureDefaults(cfg *config.Config) { cfg.UnifiedRoles.AvailableRoles = append(cfg.UnifiedRoles.AvailableRoles, definition.GetId()) } } + + if cfg.SystemStorageClient.SystemUserAPIKey == "" && cfg.Commons != nil && cfg.Commons.SystemUserAPIKey != "" { + cfg.SystemStorageClient.SystemUserAPIKey = cfg.Commons.SystemUserAPIKey + } + + if cfg.SystemStorageClient.SystemUserID == "" && cfg.Commons != nil && cfg.Commons.SystemUserID != "" { + cfg.SystemStorageClient.SystemUserID = cfg.Commons.SystemUserID + } + } // Sanitize sanitized the configuration diff --git a/services/graph/pkg/service/v0/photo.go b/services/graph/pkg/service/v0/photo.go index cbb814a972..0c46537f3b 100644 --- a/services/graph/pkg/service/v0/photo.go +++ b/services/graph/pkg/service/v0/photo.go @@ -91,7 +91,7 @@ func (g Graph) UpdatePhoto(w http.ResponseWriter, r *http.Request) { func (g Graph) updatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { logger := g.logger.SubloggerWithRequestID(r.Context()) logger.Debug().Msg("UpdatePhoto called") - g.getSystemStorageClient() + client := g.getSystemStorageClient() content, err := io.ReadAll(r.Body) if err != nil { logger.Debug().Err(err).Msg("could not read body") @@ -103,7 +103,7 @@ func (g Graph) updatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.Use errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "empty body") return } - err = g.sdsc.SimpleUpload(r.Context(), u.GetOpaqueId(), identifier, content) + err = client.SimpleUpload(r.Context(), u.GetOpaqueId(), identifier, content) if err != nil { logger.Debug().Err(err).Msg("could not upload photo") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not upload photo") @@ -115,7 +115,7 @@ func (g Graph) updatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.Use func (g Graph) getSystemStorageClient() systemstorageclient.SystemDataStorageClient { // TODO: this needs a check if the client is already initialized and if not, initialize it - g.sdsc = systemstorageclient.NewSystemStorageClient( + g.sdsc = *systemstorageclient.NewSystemStorageClient( scope, namespace, g.logger, From eccc900918f33a9cfd94defb71e3a3f2e4084459 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Wed, 14 May 2025 16:59:42 +0200 Subject: [PATCH 08/11] feature: add profile photos graph service and api --- .../systemstorageclient.go | 82 --------- services/graph/pkg/config/config.go | 17 +- .../pkg/config/defaults/defaultconfig.go | 16 +- .../v0/api_users_user_profile_photo.go | 163 ++++++++++++++++++ .../v0/api_users_user_profile_photo_test.go | 13 ++ services/graph/pkg/service/v0/graph.go | 2 - services/graph/pkg/service/v0/photo.go | 129 -------------- services/graph/pkg/service/v0/service.go | 33 +++- 8 files changed, 217 insertions(+), 238 deletions(-) delete mode 100644 pkg/systemstorageclient/systemstorageclient.go create mode 100644 services/graph/pkg/service/v0/api_users_user_profile_photo.go create mode 100644 services/graph/pkg/service/v0/api_users_user_profile_photo_test.go delete mode 100644 services/graph/pkg/service/v0/photo.go diff --git a/pkg/systemstorageclient/systemstorageclient.go b/pkg/systemstorageclient/systemstorageclient.go deleted file mode 100644 index 99b063e02d..0000000000 --- a/pkg/systemstorageclient/systemstorageclient.go +++ /dev/null @@ -1,82 +0,0 @@ -package systemstorageclient - -import ( - "context" - "path" - "sync" - - "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" -) - -var ( - managerName = "systemdata" -) - -type SystemDataStorageClient struct { - mds metadata.Storage - l *sync.Mutex -} - -func (s *SystemDataStorageClient) SimpleDownload(ctx context.Context, userID, identifier string) ([]byte, error) { - //TODO implement me - panic("implement me") -} - -func (s *SystemDataStorageClient) SimpleUpload(ctx context.Context, userID, identifier string, content []byte) error { - return s.mds.SimpleUpload(ctx, path.Join(userID, identifier), content) -} - -func (s *SystemDataStorageClient) Delete(ctx context.Context, userID, identifier string) error { - //TODO implement me - panic("implement me") -} - -func (s *SystemDataStorageClient) ReadDir(ctx context.Context, userID, identifier string) ([]string, error) { - //TODO implement me - panic("implement me") -} - -func (s *SystemDataStorageClient) MakeDirIfNotExist(ctx context.Context, userID, identifier string) error { - //TODO implement me - panic("implement me") -} - -// New initialize the store once, later calls are noops -func (s *SystemDataStorageClient) New(ctx context.Context, - logger *log.Logger, - scope, namespace string, - gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey string, -) { - if s.mds != nil { - return - } - - s.l.Lock() - defer s.l.Unlock() - - if s.mds != nil { - return - } - - mds, err := metadata.NewCS3Storage(gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey) - if err != nil { - logger.Fatal().Err(err).Msg("could not create profile storage client") - } - s.mds = mds -} - -// NewProfileStorageClient creates a new ProfileStorageClient -func NewSystemStorageClient(scope, nameSpace string, - logger *log.Logger, - gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey string) *SystemDataStorageClient { - - // scope: the scope the data should be persistet in (e.g. user) - // namespace: the namespace the data should be persistet in (e.g. profilephoto) - // results in the following path: //*//* - // e.g. /user//profilephoto/profilephoto.jpg - - sdsci := &SystemDataStorageClient{} - sdsci.New(context.TODO(), logger, scope, nameSpace, gatewayAddress, storageAddress, systemUserID, systemUserIDP, systemUserAPIKey) - return sdsci -} diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index 3ae85a571c..de8adbc67a 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -38,7 +38,7 @@ type Config struct { Context context.Context `yaml:"-"` - SystemStorageClient SystemStorageClient `yaml:"system_storage_client"` + Metadata Metadata `yaml:"metadata_config"` } type Spaces struct { @@ -156,13 +156,12 @@ type ServiceAccount struct { ServiceAccountSecret string `yaml:"service_account_secret" env:"OC_SERVICE_ACCOUNT_SECRET;GRAPH_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"1.0.0"` } -// SystemStorageClient configures the metadata store to use -type SystemStorageClient struct { - GatewayAddress string `yaml:"gateway_addr" env:"GRAPH_STORAGE_GATEWAY_GRPC_ADDR;STORAGE_GATEWAY_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"1.0.0"` - StorageAddress string `yaml:"storage_addr" env:"GRAPH_STORAGE_GRPC_ADDR;STORAGE_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"1.0.0"` +// Metadata configures the metadata store to use +type Metadata struct { + GatewayAddress string `yaml:"gateway_addr" env:"GRAPH_STORAGE_GATEWAY_GRPC_ADDR;STORAGE_GATEWAY_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"%%NEXT%%"` + StorageAddress string `yaml:"storage_addr" env:"GRAPH_STORAGE_GRPC_ADDR;STORAGE_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"%%NEXT%%"` - SystemUserID string `yaml:"system_user_id" env:"OC_SYSTEM_USER_ID;GRAPH_SYSTEM_USER_ID" desc:"ID of the OpenCloud STORAGE-SYSTEM system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"1.0.0"` - SystemUserIDP string `yaml:"system_user_idp" env:"OC_SYSTEM_USER_IDP;GRAPH_SYSTEM_USER_IDP" desc:"IDP of the OpenCloud STORAGE-SYSTEM system user." introductionVersion:"1.0.0"` - SystemUserAPIKey string `yaml:"system_user_api_key" env:"OC_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user." introductionVersion:"1.0.0"` - Cache *Cache `yaml:"cache"` + SystemUserID string `yaml:"system_user_id" env:"OC_SYSTEM_USER_ID;GRAPH_SYSTEM_USER_ID" desc:"ID of the OpenCloud STORAGE-SYSTEM system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"%%NEXT%%"` + SystemUserIDP string `yaml:"system_user_idp" env:"OC_SYSTEM_USER_IDP;GRAPH_SYSTEM_USER_IDP" desc:"IDP of the OpenCloud STORAGE-SYSTEM system user." introductionVersion:"%%NEXT%%"` + SystemUserAPIKey string `yaml:"system_user_api_key" env:"OC_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user." introductionVersion:"%%NEXT%%"` } diff --git a/services/graph/pkg/config/defaults/defaultconfig.go b/services/graph/pkg/config/defaults/defaultconfig.go index bc30304e28..4ed274c70a 100644 --- a/services/graph/pkg/config/defaults/defaultconfig.go +++ b/services/graph/pkg/config/defaults/defaultconfig.go @@ -125,16 +125,10 @@ func DefaultConfig() *config.Config { UnifiedRoles: config.UnifiedRoles{ AvailableRoles: nil, // will be populated with defaults in EnsureDefaults }, - SystemStorageClient: config.SystemStorageClient{ + Metadata: config.Metadata{ GatewayAddress: "eu.opencloud.api.storage-system", StorageAddress: "eu.opencloud.api.storage-system", SystemUserIDP: "internal", - Cache: &config.Cache{ - Store: "memory", - Nodes: []string{"127.0.0.1:9233"}, - Database: "settings-cache", - TTL: time.Minute * 10, - }, }, } } @@ -203,12 +197,12 @@ func EnsureDefaults(cfg *config.Config) { } } - if cfg.SystemStorageClient.SystemUserAPIKey == "" && cfg.Commons != nil && cfg.Commons.SystemUserAPIKey != "" { - cfg.SystemStorageClient.SystemUserAPIKey = cfg.Commons.SystemUserAPIKey + if cfg.Metadata.SystemUserAPIKey == "" && cfg.Commons != nil && cfg.Commons.SystemUserAPIKey != "" { + cfg.Metadata.SystemUserAPIKey = cfg.Commons.SystemUserAPIKey } - if cfg.SystemStorageClient.SystemUserID == "" && cfg.Commons != nil && cfg.Commons.SystemUserID != "" { - cfg.SystemStorageClient.SystemUserID = cfg.Commons.SystemUserID + if cfg.Metadata.SystemUserID == "" && cfg.Commons != nil && cfg.Commons.SystemUserID != "" { + cfg.Metadata.SystemUserID = cfg.Commons.SystemUserID } } diff --git a/services/graph/pkg/service/v0/api_users_user_profile_photo.go b/services/graph/pkg/service/v0/api_users_user_profile_photo.go new file mode 100644 index 0000000000..3f4b233c89 --- /dev/null +++ b/services/graph/pkg/service/v0/api_users_user_profile_photo.go @@ -0,0 +1,163 @@ +package svc + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/go-chi/render" + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" +) + +type ( + // UsersUserProfilePhotoProvider is the interface that defines the methods for the user profile photo service + UsersUserProfilePhotoProvider interface { + // GetPhoto retrieves the requested photo + GetPhoto(ctx context.Context, id string) ([]byte, error) + + // UpdatePhoto retrieves the requested photo + UpdatePhoto(ctx context.Context, id string, rc io.Reader) error + + // DeletePhoto deletes the requested photo + DeletePhoto(ctx context.Context, id string) error + } +) + +var ( + // profilePhotoSpaceID is the space ID for the profile photo + profilePhotoSpaceID = "f2bdd61a-da7c-49fc-8203-0558109d1b4f" + + // ErrNoBytes is returned when no bytes are found + ErrNoBytes = errors.New("no bytes") + + // ErrNoUser is returned when no user is found + ErrNoUser = errors.New("no user found") +) + +// UsersUserProfilePhotoService is the implementation of the UsersUserProfilePhotoProvider interface +type UsersUserProfilePhotoService struct { + storage metadata.Storage +} + +// NewUsersUserProfilePhotoService creates a new UsersUserProfilePhotoService +func NewUsersUserProfilePhotoService(storage metadata.Storage) (UsersUserProfilePhotoService, error) { + if err := storage.Init(context.Background(), profilePhotoSpaceID); err != nil { + return UsersUserProfilePhotoService{}, err + } + + return UsersUserProfilePhotoService{ + storage: storage, + }, nil +} + +// GetPhoto retrieves the requested photo +func (s UsersUserProfilePhotoService) GetPhoto(ctx context.Context, id string) ([]byte, error) { + photo, err := s.storage.SimpleDownload(ctx, id) + if err != nil { + return nil, err + } + + return photo, nil +} + +// DeletePhoto deletes the requested photo +func (s UsersUserProfilePhotoService) DeletePhoto(ctx context.Context, id string) error { + return s.storage.Delete(ctx, id) +} + +// UpdatePhoto updates the requested photo +func (s UsersUserProfilePhotoService) UpdatePhoto(ctx context.Context, id string, rc io.Reader) error { + photo, err := io.ReadAll(rc) + if err != nil { + return err + } + + if len(photo) == 0 { + return ErrNoBytes + } + + return s.storage.SimpleUpload(ctx, id, photo) +} + +// UsersUserProfilePhotoApi contains all photo related api endpoints +type UsersUserProfilePhotoApi struct { + logger log.Logger + usersUserProfilePhotoService UsersUserProfilePhotoProvider +} + +// NewUsersUserProfilePhotoApi creates a new UsersUserProfilePhotoApi +func NewUsersUserProfilePhotoApi(usersUserProfilePhotoService UsersUserProfilePhotoProvider, logger log.Logger) (UsersUserProfilePhotoApi, error) { + return UsersUserProfilePhotoApi{ + logger: log.Logger{Logger: logger.With().Str("graph api", "UsersUserProfilePhotoApi").Logger()}, + usersUserProfilePhotoService: usersUserProfilePhotoService, + }, nil +} + +// GetProfilePhoto provides the requested photo +func (api UsersUserProfilePhotoApi) GetProfilePhoto(w http.ResponseWriter, r *http.Request) { + id, ok := api.getUserID(w, r) + if !ok { + return + } + + photo, err := api.usersUserProfilePhotoService.GetPhoto(r.Context(), id) + if err != nil { + api.logger.Debug().Err(err) + errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to get photo") + return + } + + render.Status(r, http.StatusOK) + _, _ = w.Write(photo) +} + +// UpsertProfilePhoto updates or inserts (initial create) the requested photo +func (api UsersUserProfilePhotoApi) UpsertProfilePhoto(w http.ResponseWriter, r *http.Request) { + id, ok := api.getUserID(w, r) + if !ok { + return + } + + if err := api.usersUserProfilePhotoService.UpdatePhoto(r.Context(), id, r.Body); err != nil { + api.logger.Debug().Err(err) + errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to update photo") + return + } + defer func() { + _ = r.Body.Close() + }() + + render.Status(r, http.StatusOK) +} + +// DeleteProfilePhoto deletes the requested photo +func (api UsersUserProfilePhotoApi) DeleteProfilePhoto(w http.ResponseWriter, r *http.Request) { + id, ok := api.getUserID(w, r) + if !ok { + return + } + + if err := api.usersUserProfilePhotoService.DeletePhoto(r.Context(), id); err != nil { + api.logger.Debug().Err(err) + errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to delete photo") + return + } + + render.Status(r, http.StatusOK) +} + +func (api UsersUserProfilePhotoApi) getUserID(w http.ResponseWriter, r *http.Request) (string, bool) { + u, ok := revactx.ContextGetUser(r.Context()) + if !ok { + api.logger.Debug().Msg(ErrNoUser.Error()) + errorcode.GeneralException.Render(w, r, http.StatusMethodNotAllowed, ErrNoUser.Error()) + return "", false + } + + return u.GetId().GetOpaqueId(), true +} diff --git a/services/graph/pkg/service/v0/api_users_user_profile_photo_test.go b/services/graph/pkg/service/v0/api_users_user_profile_photo_test.go new file mode 100644 index 0000000000..a51c3b2b56 --- /dev/null +++ b/services/graph/pkg/service/v0/api_users_user_profile_photo_test.go @@ -0,0 +1,13 @@ +package svc_test + +import ( + "testing" +) + +func TestNewUsersUserProfilePhotoApi(t *testing.T) { + panic("add UsersUserProfilePhotoApi tests") +} + +func TestNewUsersUserProfilePhotoService(t *testing.T) { + panic("add UsersUserProfilePhotoService tests") +} diff --git a/services/graph/pkg/service/v0/graph.go b/services/graph/pkg/service/v0/graph.go index 4cc1571637..dbcbef72bf 100644 --- a/services/graph/pkg/service/v0/graph.go +++ b/services/graph/pkg/service/v0/graph.go @@ -3,7 +3,6 @@ package svc import ( "context" "errors" - "github.com/opencloud-eu/opencloud/pkg/systemstorageclient" "net/http" "net/url" "path" @@ -68,7 +67,6 @@ type Graph struct { keycloakClient keycloak.Client historyClient ehsvc.EventHistoryService traceProvider trace.TracerProvider - sdsc systemstorageclient.SystemDataStorageClient } // ServeHTTP implements the Service interface. diff --git a/services/graph/pkg/service/v0/photo.go b/services/graph/pkg/service/v0/photo.go deleted file mode 100644 index 0c46537f3b..0000000000 --- a/services/graph/pkg/service/v0/photo.go +++ /dev/null @@ -1,129 +0,0 @@ -package svc - -import ( - userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" - "github.com/opencloud-eu/opencloud/pkg/systemstorageclient" - "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" - revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" - "io" - "net/http" - "net/url" -) - -var ( - namespace = "profilephoto" - scope = "user" - identifier = "profilephoto" -) - -// GetMePhoto implements the Service interface -func (g Graph) GetMePhoto(w http.ResponseWriter, r *http.Request) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - logger.Debug().Msg("GetMePhoto called") - 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 - } - g.getPhoto(w, r, u.GetId()) -} - -// GetPhoto implements the Service interface -func (g Graph) GetPhoto(w http.ResponseWriter, r *http.Request) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - logger.Debug().Msg("GetPhoto called") - userID, err := url.PathUnescape(chi.URLParam(r, "userID")) - if err != nil { - logger.Debug().Err(err).Str("userID", chi.URLParam(r, "userID")).Msg("could not get drive: unescaping drive id failed") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping user id failed") - return - } - g.getPhoto(w, r, &userpb.UserId{ - OpaqueId: userID, - }) -} - -func (g Graph) getPhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - logger.Debug().Msg("GetPhoto called") - g.getSystemStorageClient() - photo, err := g.sdsc.SimpleDownload(r.Context(), u.GetOpaqueId(), identifier) - if err != nil { - render.Status(r, http.StatusNotFound) - render.JSON(w, r, nil) - } - render.Status(r, http.StatusOK) - render.JSON(w, r, photo) - -} - -// UpdateMePhoto implements the Service interface -func (g Graph) UpdateMePhoto(w http.ResponseWriter, r *http.Request) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - logger.Debug().Msg("UpdateMePhoto called") - 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 - } - g.updatePhoto(w, r, u.GetId()) -} - -// UpdatePhoto implements the Service interface -func (g Graph) UpdatePhoto(w http.ResponseWriter, r *http.Request) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - logger.Debug().Msg("UpdatePhoto called") - userID, err := url.PathUnescape(chi.URLParam(r, "userID")) - if err != nil { - logger.Debug().Err(err).Str("userID", chi.URLParam(r, "userID")).Msg("could not get drive: unescaping drive id failed") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping user id failed") - return - } - g.updatePhoto(w, r, &userpb.UserId{ - OpaqueId: userID, - }) -} - -func (g Graph) updatePhoto(w http.ResponseWriter, r *http.Request, u *userpb.UserId) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - logger.Debug().Msg("UpdatePhoto called") - client := g.getSystemStorageClient() - content, err := io.ReadAll(r.Body) - if err != nil { - logger.Debug().Err(err).Msg("could not read body") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "could not read body") - return - } - if len(content) == 0 { - logger.Debug().Msg("could not read body: empty body") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "empty body") - return - } - err = client.SimpleUpload(r.Context(), u.GetOpaqueId(), identifier, content) - if err != nil { - logger.Debug().Err(err).Msg("could not upload photo") - errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not upload photo") - return - } - render.Status(r, http.StatusOK) - render.JSON(w, r, nil) -} - -func (g Graph) getSystemStorageClient() systemstorageclient.SystemDataStorageClient { - // TODO: this needs a check if the client is already initialized and if not, initialize it - g.sdsc = *systemstorageclient.NewSystemStorageClient( - scope, - namespace, - g.logger, - g.config.SystemStorageClient.GatewayAddress, - g.config.SystemStorageClient.StorageAddress, - g.config.SystemStorageClient.SystemUserID, - g.config.SystemStorageClient.SystemUserIDP, - g.config.SystemStorageClient.SystemUserAPIKey, - ) - return g.sdsc -} diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 89ad821bb7..919f07aa6e 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -19,6 +19,7 @@ import ( "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" "github.com/opencloud-eu/reva/v2/pkg/store" "github.com/opencloud-eu/reva/v2/pkg/utils" @@ -225,6 +226,27 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx return svc, err } + storage, err := metadata.NewCS3Storage( + options.Config.Metadata.GatewayAddress, + options.Config.Metadata.StorageAddress, + options.Config.Metadata.SystemUserID, + options.Config.Metadata.SystemUserIDP, + options.Config.Metadata.SystemUserAPIKey, + ) + if err != nil { + return svc, err + } + + usersUserProfilePhotoService, err := NewUsersUserProfilePhotoService(storage) + if err != nil { + return svc, err + } + + usersUserProfilePhotoApi, err := NewUsersUserProfilePhotoApi(usersUserProfilePhotoService, options.Logger) + if err != nil { + return svc, err + } + m.Route(options.Config.HTTP.Root, func(r chi.Router) { r.Use(middleware.StripSlashes) @@ -293,9 +315,12 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx }) r.Get("/drives", svc.GetDrives(APIVersion_1)) r.Post("/changePassword", svc.ChangeOwnPassword) - r.Get("/photo", svc.GetMePhoto) - r.Put("/photo", svc.UpdateMePhoto) - r.Patch("/photo", svc.UpdateMePhoto) + r.Route("/photo", func(r chi.Router) { + r.Get("/", usersUserProfilePhotoApi.GetProfilePhoto) + r.Put("/", usersUserProfilePhotoApi.UpsertProfilePhoto) + r.Patch("/", usersUserProfilePhotoApi.UpsertProfilePhoto) + r.Delete("/", usersUserProfilePhotoApi.DeleteProfilePhoto) + }) }) r.Route("/users", func(r chi.Router) { r.Get("/", svc.GetUsers) @@ -313,8 +338,6 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx r.Delete("/{appRoleAssignmentID}", svc.DeleteAppRoleAssignment) }) } - r.Get("/photo", svc.GetPhoto) - r.Put("/photo", svc.UpdatePhoto) }) }) r.Route("/groups", func(r chi.Router) { From 250400639a47341ac8e23ef5367faa81a45bdefb Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Tue, 20 May 2025 14:27:21 +0200 Subject: [PATCH 09/11] enhancement: refine the profile photo service and introduce httpDataProviders which allows reusing the endpoints --- services/graph/.mockery.yaml | 1 + .../users_user_profile_photo_provider.go | 191 ++++++++++++++++++ .../v0/api_users_user_profile_photo.go | 109 +++++----- .../v0/api_users_user_profile_photo_test.go | 115 ++++++++++- services/graph/pkg/service/v0/data.go | 42 ++++ services/graph/pkg/service/v0/service.go | 111 +++++----- 6 files changed, 452 insertions(+), 117 deletions(-) create mode 100644 services/graph/mocks/users_user_profile_photo_provider.go create mode 100644 services/graph/pkg/service/v0/data.go diff --git a/services/graph/.mockery.yaml b/services/graph/.mockery.yaml index 163e6e23ce..551e991a83 100644 --- a/services/graph/.mockery.yaml +++ b/services/graph/.mockery.yaml @@ -16,6 +16,7 @@ packages: HTTPClient: Permissions: RoleService: + UsersUserProfilePhotoProvider: github.com/opencloud-eu/reva/v2/pkg/events: config: dir: "mocks" diff --git a/services/graph/mocks/users_user_profile_photo_provider.go b/services/graph/mocks/users_user_profile_photo_provider.go new file mode 100644 index 0000000000..89c17e305b --- /dev/null +++ b/services/graph/mocks/users_user_profile_photo_provider.go @@ -0,0 +1,191 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + context "context" + io "io" + + mock "github.com/stretchr/testify/mock" +) + +// UsersUserProfilePhotoProvider is an autogenerated mock type for the UsersUserProfilePhotoProvider type +type UsersUserProfilePhotoProvider struct { + mock.Mock +} + +type UsersUserProfilePhotoProvider_Expecter struct { + mock *mock.Mock +} + +func (_m *UsersUserProfilePhotoProvider) EXPECT() *UsersUserProfilePhotoProvider_Expecter { + return &UsersUserProfilePhotoProvider_Expecter{mock: &_m.Mock} +} + +// DeletePhoto provides a mock function with given fields: ctx, id +func (_m *UsersUserProfilePhotoProvider) DeletePhoto(ctx context.Context, id string) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeletePhoto") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UsersUserProfilePhotoProvider_DeletePhoto_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePhoto' +type UsersUserProfilePhotoProvider_DeletePhoto_Call struct { + *mock.Call +} + +// DeletePhoto is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *UsersUserProfilePhotoProvider_Expecter) DeletePhoto(ctx interface{}, id interface{}) *UsersUserProfilePhotoProvider_DeletePhoto_Call { + return &UsersUserProfilePhotoProvider_DeletePhoto_Call{Call: _e.mock.On("DeletePhoto", ctx, id)} +} + +func (_c *UsersUserProfilePhotoProvider_DeletePhoto_Call) Run(run func(ctx context.Context, id string)) *UsersUserProfilePhotoProvider_DeletePhoto_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *UsersUserProfilePhotoProvider_DeletePhoto_Call) Return(_a0 error) *UsersUserProfilePhotoProvider_DeletePhoto_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UsersUserProfilePhotoProvider_DeletePhoto_Call) RunAndReturn(run func(context.Context, string) error) *UsersUserProfilePhotoProvider_DeletePhoto_Call { + _c.Call.Return(run) + return _c +} + +// GetPhoto provides a mock function with given fields: ctx, id +func (_m *UsersUserProfilePhotoProvider) GetPhoto(ctx context.Context, id string) ([]byte, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetPhoto") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]byte, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []byte); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UsersUserProfilePhotoProvider_GetPhoto_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPhoto' +type UsersUserProfilePhotoProvider_GetPhoto_Call struct { + *mock.Call +} + +// GetPhoto is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *UsersUserProfilePhotoProvider_Expecter) GetPhoto(ctx interface{}, id interface{}) *UsersUserProfilePhotoProvider_GetPhoto_Call { + return &UsersUserProfilePhotoProvider_GetPhoto_Call{Call: _e.mock.On("GetPhoto", ctx, id)} +} + +func (_c *UsersUserProfilePhotoProvider_GetPhoto_Call) Run(run func(ctx context.Context, id string)) *UsersUserProfilePhotoProvider_GetPhoto_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *UsersUserProfilePhotoProvider_GetPhoto_Call) Return(_a0 []byte, _a1 error) *UsersUserProfilePhotoProvider_GetPhoto_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UsersUserProfilePhotoProvider_GetPhoto_Call) RunAndReturn(run func(context.Context, string) ([]byte, error)) *UsersUserProfilePhotoProvider_GetPhoto_Call { + _c.Call.Return(run) + return _c +} + +// UpdatePhoto provides a mock function with given fields: ctx, id, rc +func (_m *UsersUserProfilePhotoProvider) UpdatePhoto(ctx context.Context, id string, rc io.Reader) error { + ret := _m.Called(ctx, id, rc) + + if len(ret) == 0 { + panic("no return value specified for UpdatePhoto") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, io.Reader) error); ok { + r0 = rf(ctx, id, rc) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UsersUserProfilePhotoProvider_UpdatePhoto_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdatePhoto' +type UsersUserProfilePhotoProvider_UpdatePhoto_Call struct { + *mock.Call +} + +// UpdatePhoto is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - rc io.Reader +func (_e *UsersUserProfilePhotoProvider_Expecter) UpdatePhoto(ctx interface{}, id interface{}, rc interface{}) *UsersUserProfilePhotoProvider_UpdatePhoto_Call { + return &UsersUserProfilePhotoProvider_UpdatePhoto_Call{Call: _e.mock.On("UpdatePhoto", ctx, id, rc)} +} + +func (_c *UsersUserProfilePhotoProvider_UpdatePhoto_Call) Run(run func(ctx context.Context, id string, rc io.Reader)) *UsersUserProfilePhotoProvider_UpdatePhoto_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(io.Reader)) + }) + return _c +} + +func (_c *UsersUserProfilePhotoProvider_UpdatePhoto_Call) Return(_a0 error) *UsersUserProfilePhotoProvider_UpdatePhoto_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UsersUserProfilePhotoProvider_UpdatePhoto_Call) RunAndReturn(run func(context.Context, string, io.Reader) error) *UsersUserProfilePhotoProvider_UpdatePhoto_Call { + _c.Call.Return(run) + return _c +} + +// NewUsersUserProfilePhotoProvider creates a new instance of UsersUserProfilePhotoProvider. 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 NewUsersUserProfilePhotoProvider(t interface { + mock.TestingT + Cleanup(func()) +}) *UsersUserProfilePhotoProvider { + mock := &UsersUserProfilePhotoProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/services/graph/pkg/service/v0/api_users_user_profile_photo.go b/services/graph/pkg/service/v0/api_users_user_profile_photo.go index 3f4b233c89..3cd43c1e1f 100644 --- a/services/graph/pkg/service/v0/api_users_user_profile_photo.go +++ b/services/graph/pkg/service/v0/api_users_user_profile_photo.go @@ -7,7 +7,6 @@ import ( "net/http" "github.com/go-chi/render" - revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" "github.com/opencloud-eu/opencloud/pkg/log" @@ -21,7 +20,7 @@ type ( GetPhoto(ctx context.Context, id string) ([]byte, error) // UpdatePhoto retrieves the requested photo - UpdatePhoto(ctx context.Context, id string, rc io.Reader) error + UpdatePhoto(ctx context.Context, id string, r io.Reader) error // DeletePhoto deletes the requested photo DeletePhoto(ctx context.Context, id string) error @@ -34,9 +33,6 @@ var ( // ErrNoBytes is returned when no bytes are found ErrNoBytes = errors.New("no bytes") - - // ErrNoUser is returned when no user is found - ErrNoUser = errors.New("no user found") ) // UsersUserProfilePhotoService is the implementation of the UsersUserProfilePhotoProvider interface @@ -71,8 +67,8 @@ func (s UsersUserProfilePhotoService) DeletePhoto(ctx context.Context, id string } // UpdatePhoto updates the requested photo -func (s UsersUserProfilePhotoService) UpdatePhoto(ctx context.Context, id string, rc io.Reader) error { - photo, err := io.ReadAll(rc) +func (s UsersUserProfilePhotoService) UpdatePhoto(ctx context.Context, id string, r io.Reader) error { + photo, err := io.ReadAll(r) if err != nil { return err } @@ -98,66 +94,61 @@ func NewUsersUserProfilePhotoApi(usersUserProfilePhotoService UsersUserProfilePh }, nil } -// GetProfilePhoto provides the requested photo -func (api UsersUserProfilePhotoApi) GetProfilePhoto(w http.ResponseWriter, r *http.Request) { - id, ok := api.getUserID(w, r) - if !ok { - return - } +// GetProfilePhoto creates a handler which renders the corresponding photo +func (api UsersUserProfilePhotoApi) GetProfilePhoto(h HTTPDataHandler[string]) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + v, ok := h(w, r) + if !ok { + return + } - photo, err := api.usersUserProfilePhotoService.GetPhoto(r.Context(), id) - if err != nil { - api.logger.Debug().Err(err) - errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to get photo") - return - } + photo, err := api.usersUserProfilePhotoService.GetPhoto(r.Context(), v) + if err != nil { + api.logger.Debug().Err(err) + errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to get photo") + return + } - render.Status(r, http.StatusOK) - _, _ = w.Write(photo) + render.Status(r, http.StatusOK) + _, _ = w.Write(photo) + } } -// UpsertProfilePhoto updates or inserts (initial create) the requested photo -func (api UsersUserProfilePhotoApi) UpsertProfilePhoto(w http.ResponseWriter, r *http.Request) { - id, ok := api.getUserID(w, r) - if !ok { - return - } +// UpsertProfilePhoto creates a handler which updates or creates the corresponding photo +func (api UsersUserProfilePhotoApi) UpsertProfilePhoto(h HTTPDataHandler[string]) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + v, ok := h(w, r) + if !ok { + return + } - if err := api.usersUserProfilePhotoService.UpdatePhoto(r.Context(), id, r.Body); err != nil { - api.logger.Debug().Err(err) - errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to update photo") - return - } - defer func() { - _ = r.Body.Close() - }() + if err := api.usersUserProfilePhotoService.UpdatePhoto(r.Context(), v, r.Body); err != nil { + api.logger.Debug().Err(err) + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "failed to update photo") + return + } + defer func() { + _ = r.Body.Close() + }() - render.Status(r, http.StatusOK) + render.Status(r, http.StatusOK) + } } -// DeleteProfilePhoto deletes the requested photo -func (api UsersUserProfilePhotoApi) DeleteProfilePhoto(w http.ResponseWriter, r *http.Request) { - id, ok := api.getUserID(w, r) - if !ok { - return - } +// DeleteProfilePhoto creates a handler which deletes the corresponding photo +func (api UsersUserProfilePhotoApi) DeleteProfilePhoto(h HTTPDataHandler[string]) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + v, ok := h(w, r) + if !ok { + return + } - if err := api.usersUserProfilePhotoService.DeletePhoto(r.Context(), id); err != nil { - api.logger.Debug().Err(err) - errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to delete photo") - return - } + if err := api.usersUserProfilePhotoService.DeletePhoto(r.Context(), v); err != nil { + api.logger.Debug().Err(err) + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "failed to delete photo") + return + } - render.Status(r, http.StatusOK) -} - -func (api UsersUserProfilePhotoApi) getUserID(w http.ResponseWriter, r *http.Request) (string, bool) { - u, ok := revactx.ContextGetUser(r.Context()) - if !ok { - api.logger.Debug().Msg(ErrNoUser.Error()) - errorcode.GeneralException.Render(w, r, http.StatusMethodNotAllowed, ErrNoUser.Error()) - return "", false - } - - return u.GetId().GetOpaqueId(), true + render.Status(r, http.StatusOK) + } } diff --git a/services/graph/pkg/service/v0/api_users_user_profile_photo_test.go b/services/graph/pkg/service/v0/api_users_user_profile_photo_test.go index a51c3b2b56..cf38bcddfa 100644 --- a/services/graph/pkg/service/v0/api_users_user_profile_photo_test.go +++ b/services/graph/pkg/service/v0/api_users_user_profile_photo_test.go @@ -1,13 +1,118 @@ package svc_test import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/graph/mocks" + svc "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" ) -func TestNewUsersUserProfilePhotoApi(t *testing.T) { - panic("add UsersUserProfilePhotoApi tests") -} +func TestUsersUserProfilePhotoApi(t *testing.T) { + var ( + usersUserProfilePhotoProvider = mocks.NewUsersUserProfilePhotoProvider(t) + dummyDataProvider = func(w http.ResponseWriter, r *http.Request) (string, bool) { + return "123", true + } + ) -func TestNewUsersUserProfilePhotoService(t *testing.T) { - panic("add UsersUserProfilePhotoService tests") + api, err := svc.NewUsersUserProfilePhotoApi(usersUserProfilePhotoProvider, log.NopLogger()) + assert.NoError(t, err) + + t.Run("GetProfilePhoto", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/", nil) + ep := api.GetProfilePhoto(dummyDataProvider) + + t.Run("fails if photo provider errors", func(t *testing.T) { + w := httptest.NewRecorder() + + usersUserProfilePhotoProvider.EXPECT().GetPhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) ([]byte, error) { + return nil, errors.New("any") + }).Once() + + ep.ServeHTTP(w, r) + + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("successfully returns the requested photo", func(t *testing.T) { + w := httptest.NewRecorder() + + usersUserProfilePhotoProvider.EXPECT().GetPhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) ([]byte, error) { + return []byte("photo"), nil + }).Once() + + ep.ServeHTTP(w, r) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "photo", w.Body.String()) + }) + }) + + t.Run("DeleteProfilePhoto", func(t *testing.T) { + r := httptest.NewRequest(http.MethodDelete, "/", nil) + ep := api.DeleteProfilePhoto(dummyDataProvider) + + t.Run("fails if photo provider errors", func(t *testing.T) { + w := httptest.NewRecorder() + + usersUserProfilePhotoProvider.EXPECT().DeletePhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) error { + return errors.New("any") + }).Once() + + ep.ServeHTTP(w, r) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + }) + + t.Run("successfully deletes the requested photo", func(t *testing.T) { + w := httptest.NewRecorder() + + usersUserProfilePhotoProvider.EXPECT().DeletePhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) error { + return nil + }).Once() + + ep.ServeHTTP(w, r) + + assert.Equal(t, http.StatusOK, w.Code) + }) + }) + + t.Run("UpsertProfilePhoto", func(t *testing.T) { + r := httptest.NewRequest(http.MethodPut, "/", strings.NewReader("body")) + ep := api.UpsertProfilePhoto(dummyDataProvider) + + t.Run("fails if photo provider errors", func(t *testing.T) { + w := httptest.NewRecorder() + + usersUserProfilePhotoProvider.EXPECT().UpdatePhoto(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string, r io.Reader) error { + return errors.New("any") + }).Once() + + ep.ServeHTTP(w, r) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + }) + + t.Run("successfully upserts the photo", func(t *testing.T) { + w := httptest.NewRecorder() + + usersUserProfilePhotoProvider.EXPECT().UpdatePhoto(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string, r io.Reader) error { + return nil + }).Once() + + ep.ServeHTTP(w, r) + + assert.Equal(t, http.StatusOK, w.Code) + }) + }) } diff --git a/services/graph/pkg/service/v0/data.go b/services/graph/pkg/service/v0/data.go new file mode 100644 index 0000000000..5738183f44 --- /dev/null +++ b/services/graph/pkg/service/v0/data.go @@ -0,0 +1,42 @@ +package svc + +import ( + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/go-chi/chi/v5" + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + + "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" +) + +// HTTPDataHandler returns data from the request, it should exit early and return false in the case of any error +type HTTPDataHandler[T any] func(w http.ResponseWriter, r *http.Request) (T, bool) + +var ( + // ErrNoUser is returned when no user is found + ErrNoUser = errors.New("no user found") +) + +// GetUserIDFromCTX extracts the user from the request +func GetUserIDFromCTX(w http.ResponseWriter, r *http.Request) (string, bool) { + u, ok := revactx.ContextGetUser(r.Context()) + if !ok { + errorcode.GeneralException.Render(w, r, http.StatusMethodNotAllowed, ErrNoUser.Error()) + } + + return u.GetId().GetOpaqueId(), ok +} + +func GetSlugValue(key string) HTTPDataHandler[string] { + return func(w http.ResponseWriter, r *http.Request) (string, bool) { + v, err := url.PathUnescape(chi.URLParam(r, key)) + if err != nil { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, fmt.Sprintf(`failed to get slug: "%s"`, key)) + } + + return v, err == nil + } +} diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 919f07aa6e..7236c98b9d 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -144,14 +144,57 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx identity.IdentityCacheWithGroupsTTL(time.Duration(options.Config.Spaces.GroupsCacheTTL)), ) + storage, err := metadata.NewCS3Storage( + options.Config.Metadata.GatewayAddress, + options.Config.Metadata.StorageAddress, + options.Config.Metadata.SystemUserID, + options.Config.Metadata.SystemUserIDP, + options.Config.Metadata.SystemUserAPIKey, + ) + if err != nil { + return Graph{}, err + } + + baseGraphService := BaseGraphService{ + logger: &options.Logger, + identityCache: identityCache, + gatewaySelector: options.GatewaySelector, + config: options.Config, + availableRoles: unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(options.Config.UnifiedRoles.AvailableRoles...)), + } + + drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector) + if err != nil { + return Graph{}, err + } + + drivesDriveItemApi, err := NewDrivesDriveItemApi(drivesDriveItemService, baseGraphService, options.Logger) + if err != nil { + return Graph{}, err + } + + driveItemPermissionsService, err := NewDriveItemPermissionsService(options.Logger, options.GatewaySelector, identityCache, options.Config) + if err != nil { + return Graph{}, err + } + + driveItemPermissionsApi, err := NewDriveItemPermissionsApi(driveItemPermissionsService, options.Logger, options.Config) + if err != nil { + return Graph{}, err + } + + usersUserProfilePhotoService, err := NewUsersUserProfilePhotoService(storage) + if err != nil { + return Graph{}, err + } + + usersUserProfilePhotoApi, err := NewUsersUserProfilePhotoApi(usersUserProfilePhotoService, options.Logger) + if err != nil { + return Graph{}, err + } + svc := Graph{ - BaseGraphService: BaseGraphService{ - logger: &options.Logger, - identityCache: identityCache, - gatewaySelector: options.GatewaySelector, - config: options.Config, - availableRoles: unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(options.Config.UnifiedRoles.AvailableRoles...)), - }, + BaseGraphService: baseGraphService, mux: m, specialDriveItemsCache: spacePropertiesCache, eventsPublisher: options.EventsPublisher, @@ -206,47 +249,6 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx requireAdmin = options.RequireAdminMiddleware } - drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector) - if err != nil { - return svc, err - } - - drivesDriveItemApi, err := NewDrivesDriveItemApi(drivesDriveItemService, svc.BaseGraphService, options.Logger) - if err != nil { - return svc, err - } - - driveItemPermissionsService, err := NewDriveItemPermissionsService(options.Logger, options.GatewaySelector, identityCache, options.Config) - if err != nil { - return svc, err - } - - driveItemPermissionsApi, err := NewDriveItemPermissionsApi(driveItemPermissionsService, options.Logger, options.Config) - if err != nil { - return svc, err - } - - storage, err := metadata.NewCS3Storage( - options.Config.Metadata.GatewayAddress, - options.Config.Metadata.StorageAddress, - options.Config.Metadata.SystemUserID, - options.Config.Metadata.SystemUserIDP, - options.Config.Metadata.SystemUserAPIKey, - ) - if err != nil { - return svc, err - } - - usersUserProfilePhotoService, err := NewUsersUserProfilePhotoService(storage) - if err != nil { - return svc, err - } - - usersUserProfilePhotoApi, err := NewUsersUserProfilePhotoApi(usersUserProfilePhotoService, options.Logger) - if err != nil { - return svc, err - } - m.Route(options.Config.HTTP.Root, func(r chi.Router) { r.Use(middleware.StripSlashes) @@ -315,11 +317,11 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx }) r.Get("/drives", svc.GetDrives(APIVersion_1)) r.Post("/changePassword", svc.ChangeOwnPassword) - r.Route("/photo", func(r chi.Router) { - r.Get("/", usersUserProfilePhotoApi.GetProfilePhoto) - r.Put("/", usersUserProfilePhotoApi.UpsertProfilePhoto) - r.Patch("/", usersUserProfilePhotoApi.UpsertProfilePhoto) - r.Delete("/", usersUserProfilePhotoApi.DeleteProfilePhoto) + r.Route("/photo/$value", func(r chi.Router) { + r.Get("/", usersUserProfilePhotoApi.GetProfilePhoto(GetUserIDFromCTX)) + r.Put("/", usersUserProfilePhotoApi.UpsertProfilePhoto(GetUserIDFromCTX)) + r.Patch("/", usersUserProfilePhotoApi.UpsertProfilePhoto(GetUserIDFromCTX)) + r.Delete("/", usersUserProfilePhotoApi.DeleteProfilePhoto(GetUserIDFromCTX)) }) }) r.Route("/users", func(r chi.Router) { @@ -329,6 +331,9 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx r.Get("/", svc.GetUser) r.Get("/drive", svc.GetUserDrive) r.Post("/exportPersonalData", svc.ExportPersonalData) + r.Route("/photo/$value", func(r chi.Router) { + r.Get("/", usersUserProfilePhotoApi.GetProfilePhoto(GetSlugValue("userID"))) + }) r.With(requireAdmin).Delete("/", svc.DeleteUser) r.With(requireAdmin).Patch("/", svc.PatchUser) if svc.roleService != nil { From 6b7c004d0b4d20c6ac02ae813aa1c8dc3cb3f5d3 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Wed, 21 May 2025 13:51:51 +0200 Subject: [PATCH 10/11] fix(tests): fix nil pointer errors caused by the introduction of cs3 metadata storage --- services/graph/.mockery.yaml | 5 + services/graph/mocks/storage.go | 732 ++++++++++++++++++ .../users_user_profile_photo_provider.go | 16 +- services/graph/pkg/server/http/server.go | 13 + .../graph/pkg/service/v0/application_test.go | 19 +- .../pkg/service/v0/approleassignments_test.go | 21 +- .../graph/pkg/service/v0/driveitems_test.go | 8 +- .../pkg/service/v0/educationclasses_test.go | 29 +- .../pkg/service/v0/educationschools_test.go | 12 +- .../pkg/service/v0/educationuser_test.go | 23 +- services/graph/pkg/service/v0/graph_test.go | 11 +- services/graph/pkg/service/v0/groups_test.go | 28 +- services/graph/pkg/service/v0/option.go | 16 +- .../graph/pkg/service/v0/password_test.go | 29 +- services/graph/pkg/service/v0/service.go | 14 +- .../graph/pkg/service/v0/sharedbyme_test.go | 26 +- .../graph/pkg/service/v0/sharedwithme_test.go | 12 +- services/graph/pkg/service/v0/users_test.go | 26 +- 18 files changed, 948 insertions(+), 92 deletions(-) create mode 100644 services/graph/mocks/storage.go diff --git a/services/graph/.mockery.yaml b/services/graph/.mockery.yaml index 551e991a83..fcfd8a989b 100644 --- a/services/graph/.mockery.yaml +++ b/services/graph/.mockery.yaml @@ -22,6 +22,11 @@ packages: dir: "mocks" interfaces: Publisher: + github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata: + config: + dir: "mocks" + interfaces: + Storage: github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool: config: dir: "mocks" diff --git a/services/graph/mocks/storage.go b/services/graph/mocks/storage.go new file mode 100644 index 0000000000..62fdc1fcae --- /dev/null +++ b/services/graph/mocks/storage.go @@ -0,0 +1,732 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + context "context" + + metadata "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" + mock "github.com/stretchr/testify/mock" + + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" +) + +// Storage is an autogenerated mock type for the Storage type +type Storage struct { + mock.Mock +} + +type Storage_Expecter struct { + mock *mock.Mock +} + +func (_m *Storage) EXPECT() *Storage_Expecter { + return &Storage_Expecter{mock: &_m.Mock} +} + +// Backend provides a mock function with no fields +func (_m *Storage) Backend() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Backend") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Storage_Backend_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Backend' +type Storage_Backend_Call struct { + *mock.Call +} + +// Backend is a helper method to define mock.On call +func (_e *Storage_Expecter) Backend() *Storage_Backend_Call { + return &Storage_Backend_Call{Call: _e.mock.On("Backend")} +} + +func (_c *Storage_Backend_Call) Run(run func()) *Storage_Backend_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Storage_Backend_Call) Return(_a0 string) *Storage_Backend_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Storage_Backend_Call) RunAndReturn(run func() string) *Storage_Backend_Call { + _c.Call.Return(run) + return _c +} + +// CreateSymlink provides a mock function with given fields: ctx, oldname, newname +func (_m *Storage) CreateSymlink(ctx context.Context, oldname string, newname string) error { + ret := _m.Called(ctx, oldname, newname) + + if len(ret) == 0 { + panic("no return value specified for CreateSymlink") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, oldname, newname) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Storage_CreateSymlink_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateSymlink' +type Storage_CreateSymlink_Call struct { + *mock.Call +} + +// CreateSymlink is a helper method to define mock.On call +// - ctx context.Context +// - oldname string +// - newname string +func (_e *Storage_Expecter) CreateSymlink(ctx interface{}, oldname interface{}, newname interface{}) *Storage_CreateSymlink_Call { + return &Storage_CreateSymlink_Call{Call: _e.mock.On("CreateSymlink", ctx, oldname, newname)} +} + +func (_c *Storage_CreateSymlink_Call) Run(run func(ctx context.Context, oldname string, newname string)) *Storage_CreateSymlink_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *Storage_CreateSymlink_Call) Return(_a0 error) *Storage_CreateSymlink_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Storage_CreateSymlink_Call) RunAndReturn(run func(context.Context, string, string) error) *Storage_CreateSymlink_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function with given fields: ctx, path +func (_m *Storage) Delete(ctx context.Context, path string) error { + ret := _m.Called(ctx, path) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, path) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Storage_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type Storage_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - path string +func (_e *Storage_Expecter) Delete(ctx interface{}, path interface{}) *Storage_Delete_Call { + return &Storage_Delete_Call{Call: _e.mock.On("Delete", ctx, path)} +} + +func (_c *Storage_Delete_Call) Run(run func(ctx context.Context, path string)) *Storage_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Storage_Delete_Call) Return(_a0 error) *Storage_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Storage_Delete_Call) RunAndReturn(run func(context.Context, string) error) *Storage_Delete_Call { + _c.Call.Return(run) + return _c +} + +// Download provides a mock function with given fields: ctx, req +func (_m *Storage) Download(ctx context.Context, req metadata.DownloadRequest) (*metadata.DownloadResponse, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for Download") + } + + var r0 *metadata.DownloadResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, metadata.DownloadRequest) (*metadata.DownloadResponse, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, metadata.DownloadRequest) *metadata.DownloadResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*metadata.DownloadResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, metadata.DownloadRequest) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Storage_Download_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Download' +type Storage_Download_Call struct { + *mock.Call +} + +// Download is a helper method to define mock.On call +// - ctx context.Context +// - req metadata.DownloadRequest +func (_e *Storage_Expecter) Download(ctx interface{}, req interface{}) *Storage_Download_Call { + return &Storage_Download_Call{Call: _e.mock.On("Download", ctx, req)} +} + +func (_c *Storage_Download_Call) Run(run func(ctx context.Context, req metadata.DownloadRequest)) *Storage_Download_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(metadata.DownloadRequest)) + }) + return _c +} + +func (_c *Storage_Download_Call) Return(_a0 *metadata.DownloadResponse, _a1 error) *Storage_Download_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Storage_Download_Call) RunAndReturn(run func(context.Context, metadata.DownloadRequest) (*metadata.DownloadResponse, error)) *Storage_Download_Call { + _c.Call.Return(run) + return _c +} + +// Init provides a mock function with given fields: ctx, name +func (_m *Storage) Init(ctx context.Context, name string) error { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for Init") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Storage_Init_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Init' +type Storage_Init_Call struct { + *mock.Call +} + +// Init is a helper method to define mock.On call +// - ctx context.Context +// - name string +func (_e *Storage_Expecter) Init(ctx interface{}, name interface{}) *Storage_Init_Call { + return &Storage_Init_Call{Call: _e.mock.On("Init", ctx, name)} +} + +func (_c *Storage_Init_Call) Run(run func(ctx context.Context, name string)) *Storage_Init_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Storage_Init_Call) Return(err error) *Storage_Init_Call { + _c.Call.Return(err) + return _c +} + +func (_c *Storage_Init_Call) RunAndReturn(run func(context.Context, string) error) *Storage_Init_Call { + _c.Call.Return(run) + return _c +} + +// ListDir provides a mock function with given fields: ctx, path +func (_m *Storage) ListDir(ctx context.Context, path string) ([]*providerv1beta1.ResourceInfo, error) { + ret := _m.Called(ctx, path) + + if len(ret) == 0 { + panic("no return value specified for ListDir") + } + + var r0 []*providerv1beta1.ResourceInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]*providerv1beta1.ResourceInfo, error)); ok { + return rf(ctx, path) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []*providerv1beta1.ResourceInfo); ok { + r0 = rf(ctx, path) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*providerv1beta1.ResourceInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, path) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Storage_ListDir_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListDir' +type Storage_ListDir_Call struct { + *mock.Call +} + +// ListDir is a helper method to define mock.On call +// - ctx context.Context +// - path string +func (_e *Storage_Expecter) ListDir(ctx interface{}, path interface{}) *Storage_ListDir_Call { + return &Storage_ListDir_Call{Call: _e.mock.On("ListDir", ctx, path)} +} + +func (_c *Storage_ListDir_Call) Run(run func(ctx context.Context, path string)) *Storage_ListDir_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Storage_ListDir_Call) Return(_a0 []*providerv1beta1.ResourceInfo, _a1 error) *Storage_ListDir_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Storage_ListDir_Call) RunAndReturn(run func(context.Context, string) ([]*providerv1beta1.ResourceInfo, error)) *Storage_ListDir_Call { + _c.Call.Return(run) + return _c +} + +// MakeDirIfNotExist provides a mock function with given fields: ctx, name +func (_m *Storage) MakeDirIfNotExist(ctx context.Context, name string) error { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for MakeDirIfNotExist") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Storage_MakeDirIfNotExist_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MakeDirIfNotExist' +type Storage_MakeDirIfNotExist_Call struct { + *mock.Call +} + +// MakeDirIfNotExist is a helper method to define mock.On call +// - ctx context.Context +// - name string +func (_e *Storage_Expecter) MakeDirIfNotExist(ctx interface{}, name interface{}) *Storage_MakeDirIfNotExist_Call { + return &Storage_MakeDirIfNotExist_Call{Call: _e.mock.On("MakeDirIfNotExist", ctx, name)} +} + +func (_c *Storage_MakeDirIfNotExist_Call) Run(run func(ctx context.Context, name string)) *Storage_MakeDirIfNotExist_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Storage_MakeDirIfNotExist_Call) Return(_a0 error) *Storage_MakeDirIfNotExist_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Storage_MakeDirIfNotExist_Call) RunAndReturn(run func(context.Context, string) error) *Storage_MakeDirIfNotExist_Call { + _c.Call.Return(run) + return _c +} + +// ReadDir provides a mock function with given fields: ctx, path +func (_m *Storage) ReadDir(ctx context.Context, path string) ([]string, error) { + ret := _m.Called(ctx, path) + + if len(ret) == 0 { + panic("no return value specified for ReadDir") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { + return rf(ctx, path) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []string); ok { + r0 = rf(ctx, path) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, path) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Storage_ReadDir_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadDir' +type Storage_ReadDir_Call struct { + *mock.Call +} + +// ReadDir is a helper method to define mock.On call +// - ctx context.Context +// - path string +func (_e *Storage_Expecter) ReadDir(ctx interface{}, path interface{}) *Storage_ReadDir_Call { + return &Storage_ReadDir_Call{Call: _e.mock.On("ReadDir", ctx, path)} +} + +func (_c *Storage_ReadDir_Call) Run(run func(ctx context.Context, path string)) *Storage_ReadDir_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Storage_ReadDir_Call) Return(_a0 []string, _a1 error) *Storage_ReadDir_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Storage_ReadDir_Call) RunAndReturn(run func(context.Context, string) ([]string, error)) *Storage_ReadDir_Call { + _c.Call.Return(run) + return _c +} + +// ResolveSymlink provides a mock function with given fields: ctx, name +func (_m *Storage) ResolveSymlink(ctx context.Context, name string) (string, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for ResolveSymlink") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { + r0 = rf(ctx, name) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Storage_ResolveSymlink_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ResolveSymlink' +type Storage_ResolveSymlink_Call struct { + *mock.Call +} + +// ResolveSymlink is a helper method to define mock.On call +// - ctx context.Context +// - name string +func (_e *Storage_Expecter) ResolveSymlink(ctx interface{}, name interface{}) *Storage_ResolveSymlink_Call { + return &Storage_ResolveSymlink_Call{Call: _e.mock.On("ResolveSymlink", ctx, name)} +} + +func (_c *Storage_ResolveSymlink_Call) Run(run func(ctx context.Context, name string)) *Storage_ResolveSymlink_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Storage_ResolveSymlink_Call) Return(_a0 string, _a1 error) *Storage_ResolveSymlink_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Storage_ResolveSymlink_Call) RunAndReturn(run func(context.Context, string) (string, error)) *Storage_ResolveSymlink_Call { + _c.Call.Return(run) + return _c +} + +// SimpleDownload provides a mock function with given fields: ctx, path +func (_m *Storage) SimpleDownload(ctx context.Context, path string) ([]byte, error) { + ret := _m.Called(ctx, path) + + if len(ret) == 0 { + panic("no return value specified for SimpleDownload") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]byte, error)); ok { + return rf(ctx, path) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []byte); ok { + r0 = rf(ctx, path) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, path) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Storage_SimpleDownload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SimpleDownload' +type Storage_SimpleDownload_Call struct { + *mock.Call +} + +// SimpleDownload is a helper method to define mock.On call +// - ctx context.Context +// - path string +func (_e *Storage_Expecter) SimpleDownload(ctx interface{}, path interface{}) *Storage_SimpleDownload_Call { + return &Storage_SimpleDownload_Call{Call: _e.mock.On("SimpleDownload", ctx, path)} +} + +func (_c *Storage_SimpleDownload_Call) Run(run func(ctx context.Context, path string)) *Storage_SimpleDownload_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Storage_SimpleDownload_Call) Return(_a0 []byte, _a1 error) *Storage_SimpleDownload_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Storage_SimpleDownload_Call) RunAndReturn(run func(context.Context, string) ([]byte, error)) *Storage_SimpleDownload_Call { + _c.Call.Return(run) + return _c +} + +// SimpleUpload provides a mock function with given fields: ctx, uploadpath, content +func (_m *Storage) SimpleUpload(ctx context.Context, uploadpath string, content []byte) error { + ret := _m.Called(ctx, uploadpath, content) + + if len(ret) == 0 { + panic("no return value specified for SimpleUpload") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, []byte) error); ok { + r0 = rf(ctx, uploadpath, content) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Storage_SimpleUpload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SimpleUpload' +type Storage_SimpleUpload_Call struct { + *mock.Call +} + +// SimpleUpload is a helper method to define mock.On call +// - ctx context.Context +// - uploadpath string +// - content []byte +func (_e *Storage_Expecter) SimpleUpload(ctx interface{}, uploadpath interface{}, content interface{}) *Storage_SimpleUpload_Call { + return &Storage_SimpleUpload_Call{Call: _e.mock.On("SimpleUpload", ctx, uploadpath, content)} +} + +func (_c *Storage_SimpleUpload_Call) Run(run func(ctx context.Context, uploadpath string, content []byte)) *Storage_SimpleUpload_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].([]byte)) + }) + return _c +} + +func (_c *Storage_SimpleUpload_Call) Return(_a0 error) *Storage_SimpleUpload_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Storage_SimpleUpload_Call) RunAndReturn(run func(context.Context, string, []byte) error) *Storage_SimpleUpload_Call { + _c.Call.Return(run) + return _c +} + +// Stat provides a mock function with given fields: ctx, path +func (_m *Storage) Stat(ctx context.Context, path string) (*providerv1beta1.ResourceInfo, error) { + ret := _m.Called(ctx, path) + + if len(ret) == 0 { + panic("no return value specified for Stat") + } + + var r0 *providerv1beta1.ResourceInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*providerv1beta1.ResourceInfo, error)); ok { + return rf(ctx, path) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *providerv1beta1.ResourceInfo); ok { + r0 = rf(ctx, path) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.ResourceInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, path) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Storage_Stat_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stat' +type Storage_Stat_Call struct { + *mock.Call +} + +// Stat is a helper method to define mock.On call +// - ctx context.Context +// - path string +func (_e *Storage_Expecter) Stat(ctx interface{}, path interface{}) *Storage_Stat_Call { + return &Storage_Stat_Call{Call: _e.mock.On("Stat", ctx, path)} +} + +func (_c *Storage_Stat_Call) Run(run func(ctx context.Context, path string)) *Storage_Stat_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Storage_Stat_Call) Return(_a0 *providerv1beta1.ResourceInfo, _a1 error) *Storage_Stat_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Storage_Stat_Call) RunAndReturn(run func(context.Context, string) (*providerv1beta1.ResourceInfo, error)) *Storage_Stat_Call { + _c.Call.Return(run) + return _c +} + +// Upload provides a mock function with given fields: ctx, req +func (_m *Storage) Upload(ctx context.Context, req metadata.UploadRequest) (*metadata.UploadResponse, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for Upload") + } + + var r0 *metadata.UploadResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, metadata.UploadRequest) (*metadata.UploadResponse, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, metadata.UploadRequest) *metadata.UploadResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*metadata.UploadResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, metadata.UploadRequest) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Storage_Upload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upload' +type Storage_Upload_Call struct { + *mock.Call +} + +// Upload is a helper method to define mock.On call +// - ctx context.Context +// - req metadata.UploadRequest +func (_e *Storage_Expecter) Upload(ctx interface{}, req interface{}) *Storage_Upload_Call { + return &Storage_Upload_Call{Call: _e.mock.On("Upload", ctx, req)} +} + +func (_c *Storage_Upload_Call) Run(run func(ctx context.Context, req metadata.UploadRequest)) *Storage_Upload_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(metadata.UploadRequest)) + }) + return _c +} + +func (_c *Storage_Upload_Call) Return(_a0 *metadata.UploadResponse, _a1 error) *Storage_Upload_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Storage_Upload_Call) RunAndReturn(run func(context.Context, metadata.UploadRequest) (*metadata.UploadResponse, error)) *Storage_Upload_Call { + _c.Call.Return(run) + return _c +} + +// NewStorage creates a new instance of Storage. 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 NewStorage(t interface { + mock.TestingT + Cleanup(func()) +}) *Storage { + mock := &Storage{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/services/graph/mocks/users_user_profile_photo_provider.go b/services/graph/mocks/users_user_profile_photo_provider.go index 89c17e305b..faeb8f1a8d 100644 --- a/services/graph/mocks/users_user_profile_photo_provider.go +++ b/services/graph/mocks/users_user_profile_photo_provider.go @@ -128,9 +128,9 @@ func (_c *UsersUserProfilePhotoProvider_GetPhoto_Call) RunAndReturn(run func(con return _c } -// UpdatePhoto provides a mock function with given fields: ctx, id, rc -func (_m *UsersUserProfilePhotoProvider) UpdatePhoto(ctx context.Context, id string, rc io.Reader) error { - ret := _m.Called(ctx, id, rc) +// UpdatePhoto provides a mock function with given fields: ctx, id, r +func (_m *UsersUserProfilePhotoProvider) UpdatePhoto(ctx context.Context, id string, r io.Reader) error { + ret := _m.Called(ctx, id, r) if len(ret) == 0 { panic("no return value specified for UpdatePhoto") @@ -138,7 +138,7 @@ func (_m *UsersUserProfilePhotoProvider) UpdatePhoto(ctx context.Context, id str var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, io.Reader) error); ok { - r0 = rf(ctx, id, rc) + r0 = rf(ctx, id, r) } else { r0 = ret.Error(0) } @@ -154,12 +154,12 @@ type UsersUserProfilePhotoProvider_UpdatePhoto_Call struct { // UpdatePhoto is a helper method to define mock.On call // - ctx context.Context // - id string -// - rc io.Reader -func (_e *UsersUserProfilePhotoProvider_Expecter) UpdatePhoto(ctx interface{}, id interface{}, rc interface{}) *UsersUserProfilePhotoProvider_UpdatePhoto_Call { - return &UsersUserProfilePhotoProvider_UpdatePhoto_Call{Call: _e.mock.On("UpdatePhoto", ctx, id, rc)} +// - r io.Reader +func (_e *UsersUserProfilePhotoProvider_Expecter) UpdatePhoto(ctx interface{}, id interface{}, r interface{}) *UsersUserProfilePhotoProvider_UpdatePhoto_Call { + return &UsersUserProfilePhotoProvider_UpdatePhoto_Call{Call: _e.mock.On("UpdatePhoto", ctx, id, r)} } -func (_c *UsersUserProfilePhotoProvider_UpdatePhoto_Call) Run(run func(ctx context.Context, id string, rc io.Reader)) *UsersUserProfilePhotoProvider_UpdatePhoto_Call { +func (_c *UsersUserProfilePhotoProvider_UpdatePhoto_Call) Run(run func(ctx context.Context, id string, r io.Reader)) *UsersUserProfilePhotoProvider_UpdatePhoto_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(io.Reader)) }) diff --git a/services/graph/pkg/server/http/server.go b/services/graph/pkg/server/http/server.go index 91ca248088..98fb696bf2 100644 --- a/services/graph/pkg/server/http/server.go +++ b/services/graph/pkg/server/http/server.go @@ -8,6 +8,7 @@ import ( chimiddleware "github.com/go-chi/chi/v5/middleware" "github.com/opencloud-eu/reva/v2/pkg/events/stream" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" "github.com/pkg/errors" "go-micro.dev/v4" "go-micro.dev/v4/events" @@ -128,9 +129,21 @@ func Server(opts ...Option) (http.Service, error) { hClient := ehsvc.NewEventHistoryService("eu.opencloud.api.eventhistory", grpcClient) + storage, err := metadata.NewCS3Storage( + options.Config.Metadata.GatewayAddress, + options.Config.Metadata.StorageAddress, + options.Config.Metadata.SystemUserID, + options.Config.Metadata.SystemUserIDP, + options.Config.Metadata.SystemUserAPIKey, + ) + if err != nil { + return http.Service{}, fmt.Errorf("could not initialize metadata storage: %w", err) + } + var handle svc.Service handle, err = svc.NewService( svc.Context(options.Context), + svc.MetadataStorage(storage), svc.Logger(options.Logger), svc.Config(options.Config), svc.Middleware(middlewares...), diff --git a/services/graph/pkg/service/v0/application_test.go b/services/graph/pkg/service/v0/application_test.go index 0e53238fc4..47acb9f93d 100644 --- a/services/graph/pkg/service/v0/application_test.go +++ b/services/graph/pkg/service/v0/application_test.go @@ -11,6 +11,12 @@ import ( "github.com/go-chi/chi/v5" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + libregraph "github.com/opencloud-eu/libre-graph-api-go" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" + "github.com/stretchr/testify/mock" + "google.golang.org/grpc" + "github.com/opencloud-eu/opencloud/pkg/shared" settingsmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/settings/v0" settings "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0" @@ -19,11 +25,6 @@ import ( "github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults" identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks" service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" - libregraph "github.com/opencloud-eu/libre-graph-api-go" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc" ) type applicationList struct { @@ -70,13 +71,19 @@ var _ = Describe("Applications", func() { cfg.GRPCClientTLS = &shared.GRPCClientTLS{} cfg.Application.ID = "some-application-ID" - svc, _ = service.NewService( + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), service.WithRoleService(roleService), ) + Expect(err).ToNot(HaveOccurred()) }) Describe("ListApplications", func() { diff --git a/services/graph/pkg/service/v0/approleassignments_test.go b/services/graph/pkg/service/v0/approleassignments_test.go index 4bcefe63b5..29c94b2abb 100644 --- a/services/graph/pkg/service/v0/approleassignments_test.go +++ b/services/graph/pkg/service/v0/approleassignments_test.go @@ -14,6 +14,13 @@ import ( "github.com/golang/protobuf/ptypes/empty" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + libregraph "github.com/opencloud-eu/libre-graph-api-go" + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" + "github.com/stretchr/testify/mock" + "google.golang.org/grpc" + "github.com/opencloud-eu/opencloud/pkg/shared" settingsmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/settings/v0" settings "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0" @@ -22,12 +29,6 @@ import ( "github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults" identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks" service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" - revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" - libregraph "github.com/opencloud-eu/libre-graph-api-go" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc" ) type assignmentList struct { @@ -80,13 +81,19 @@ var _ = Describe("AppRoleAssignments", func() { cfg.GRPCClientTLS = &shared.GRPCClientTLS{} cfg.Application.ID = "some-application-ID" - svc, _ = service.NewService( + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), service.WithRoleService(roleService), ) + Expect(err).ToNot(HaveOccurred()) }) Describe("ListAppRoleAssignments", func() { diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go index 88280ef9dd..c692fd8855 100644 --- a/services/graph/pkg/service/v0/driveitems_test.go +++ b/services/graph/pkg/service/v0/driveitems_test.go @@ -85,12 +85,18 @@ var _ = Describe("Driveitems", func() { cfg.Commons = &shared.Commons{} cfg.GRPCClientTLS = &shared.GRPCClientTLS{} - svc, _ = service.NewService( + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), ) + Expect(err).ToNot(HaveOccurred()) }) Describe("GetRootDriveChildren", func() { diff --git a/services/graph/pkg/service/v0/educationclasses_test.go b/services/graph/pkg/service/v0/educationclasses_test.go index ecb5e7f02b..9ce013e2a3 100644 --- a/services/graph/pkg/service/v0/educationclasses_test.go +++ b/services/graph/pkg/service/v0/educationclasses_test.go @@ -14,6 +14,13 @@ import ( "github.com/go-chi/chi/v5" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + libregraph "github.com/opencloud-eu/libre-graph-api-go" + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" + "github.com/stretchr/testify/mock" + "google.golang.org/grpc" + "github.com/opencloud-eu/opencloud/pkg/shared" "github.com/opencloud-eu/opencloud/services/graph/mocks" "github.com/opencloud-eu/opencloud/services/graph/pkg/config" @@ -21,12 +28,6 @@ import ( "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks" service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" - revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" - libregraph "github.com/opencloud-eu/libre-graph-api-go" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc" ) var _ = Describe("EducationClass", func() { @@ -78,13 +79,19 @@ var _ = Describe("EducationClass", func() { cfg.Commons = &shared.Commons{} cfg.GRPCClientTLS = &shared.GRPCClientTLS{} - svc, _ = service.NewService( + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), service.WithIdentityEducationBackend(identityEducationBackend), ) + Expect(err).ToNot(HaveOccurred()) }) Describe("GetEducationClasses", func() { @@ -326,13 +333,19 @@ var _ = Describe("EducationClass", func() { Expect(err).ToNot(HaveOccurred()) cfg.API.GroupMembersPatchLimit = 21 - svc, _ = service.NewService( + + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), service.WithIdentityEducationBackend(identityEducationBackend), ) + Expect(err).ToNot(HaveOccurred()) r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/classes", bytes.NewBuffer(updatedClassJson)) rctx := chi.NewRouteContext() diff --git a/services/graph/pkg/service/v0/educationschools_test.go b/services/graph/pkg/service/v0/educationschools_test.go index 61cf06d6dd..b00573f0e8 100644 --- a/services/graph/pkg/service/v0/educationschools_test.go +++ b/services/graph/pkg/service/v0/educationschools_test.go @@ -21,13 +21,15 @@ import ( "github.com/stretchr/testify/mock" "google.golang.org/grpc" + libregraph "github.com/opencloud-eu/libre-graph-api-go" + "github.com/opencloud-eu/opencloud/pkg/shared" + "github.com/opencloud-eu/opencloud/services/graph/mocks" "github.com/opencloud-eu/opencloud/services/graph/pkg/config" "github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults" "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks" service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" - libregraph "github.com/opencloud-eu/libre-graph-api-go" ) type schoolList struct { @@ -78,11 +80,17 @@ var _ = Describe("Schools", func() { cfg.Commons = &shared.Commons{} cfg.GRPCClientTLS = &shared.GRPCClientTLS{} - svc, _ = service.NewService( + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.WithIdentityEducationBackend(identityEducationBackend), ) + Expect(err).ToNot(HaveOccurred()) }) Describe("GetEducationSchools", func() { diff --git a/services/graph/pkg/service/v0/educationuser_test.go b/services/graph/pkg/service/v0/educationuser_test.go index 1c8ab547c9..a1d5404dee 100644 --- a/services/graph/pkg/service/v0/educationuser_test.go +++ b/services/graph/pkg/service/v0/educationuser_test.go @@ -15,6 +15,14 @@ import ( "github.com/go-chi/chi/v5" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + libregraph "github.com/opencloud-eu/libre-graph-api-go" + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" + "github.com/stretchr/testify/mock" + "google.golang.org/grpc" + "github.com/opencloud-eu/opencloud/pkg/shared" settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0" "github.com/opencloud-eu/opencloud/services/graph/mocks" @@ -22,13 +30,6 @@ import ( "github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults" identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks" service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" - revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" - libregraph "github.com/opencloud-eu/libre-graph-api-go" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc" ) type educationUserList struct { @@ -80,13 +81,19 @@ var _ = Describe("EducationUsers", func() { cfg.Commons = &shared.Commons{} cfg.GRPCClientTLS = &shared.GRPCClientTLS{} - svc, _ = service.NewService( + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityEducationBackend(identityEducationBackend), service.WithRoleService(roleService), ) + Expect(err).ToNot(HaveOccurred()) }) Describe("GetEducationUsers", func() { diff --git a/services/graph/pkg/service/v0/graph_test.go b/services/graph/pkg/service/v0/graph_test.go index cb423db2b0..2d7e002e63 100644 --- a/services/graph/pkg/service/v0/graph_test.go +++ b/services/graph/pkg/service/v0/graph_test.go @@ -17,13 +17,13 @@ import ( "github.com/go-chi/chi/v5" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + libregraph "github.com/opencloud-eu/libre-graph-api-go" "github.com/opencloud-eu/reva/v2/pkg/conversions" revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "github.com/opencloud-eu/reva/v2/pkg/utils" cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" - libregraph "github.com/opencloud-eu/libre-graph-api-go" "github.com/pkg/errors" "github.com/stretchr/testify/mock" "github.com/tidwall/gjson" @@ -80,12 +80,19 @@ var _ = Describe("Graph", func() { eventsPublisher = mocks.Publisher{} permissionService = mocks.Permissions{} - svc, _ = service.NewService( + + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.PermissionService(&permissionService), ) + Expect(err).ToNot(HaveOccurred()) }) Describe("NewService", func() { diff --git a/services/graph/pkg/service/v0/groups_test.go b/services/graph/pkg/service/v0/groups_test.go index 94dc9ebb80..4c241a8f76 100644 --- a/services/graph/pkg/service/v0/groups_test.go +++ b/services/graph/pkg/service/v0/groups_test.go @@ -14,6 +14,13 @@ import ( "github.com/go-chi/chi/v5" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + libregraph "github.com/opencloud-eu/libre-graph-api-go" + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" + "github.com/stretchr/testify/mock" + "google.golang.org/grpc" + "github.com/opencloud-eu/opencloud/pkg/shared" settingsmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/settings/v0" settings "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0" @@ -23,12 +30,6 @@ import ( "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks" service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" - revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" - libregraph "github.com/opencloud-eu/libre-graph-api-go" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc" ) type groupList struct { @@ -84,13 +85,19 @@ var _ = Describe("Groups", func() { cfg.Commons = &shared.Commons{} cfg.GRPCClientTLS = &shared.GRPCClientTLS{} - svc, _ = service.NewService( + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), service.PermissionService(permissionService), ) + Expect(err).ToNot(HaveOccurred()) }) Describe("GetGroups", func() { @@ -410,13 +417,18 @@ var _ = Describe("Groups", func() { updatedGroupJson, err := json.Marshal(updatedGroup) Expect(err).ToNot(HaveOccurred()) + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + cfg.API.GroupMembersPatchLimit = 21 - svc, _ = service.NewService( + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), ) + Expect(err).ToNot(HaveOccurred()) r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/groups", bytes.NewBuffer(updatedGroupJson)) rctx := chi.NewRouteContext() diff --git a/services/graph/pkg/service/v0/option.go b/services/graph/pkg/service/v0/option.go index 6ed1f13740..ee85e32d9d 100644 --- a/services/graph/pkg/service/v0/option.go +++ b/services/graph/pkg/service/v0/option.go @@ -5,6 +5,11 @@ import ( "net/http" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + "github.com/opencloud-eu/reva/v2/pkg/events" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" + "go.opentelemetry.io/otel/trace" + "github.com/opencloud-eu/opencloud/pkg/keycloak" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/roles" @@ -13,9 +18,6 @@ import ( settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0" "github.com/opencloud-eu/opencloud/services/graph/pkg/config" "github.com/opencloud-eu/opencloud/services/graph/pkg/identity" - "github.com/opencloud-eu/reva/v2/pkg/events" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - "go.opentelemetry.io/otel/trace" ) // Option defines a single option function. @@ -41,6 +43,7 @@ type Options struct { KeycloakClient keycloak.Client EventHistoryClient ehsvc.EventHistoryService TraceProvider trace.TracerProvider + Storage metadata.Storage } // newOptions initializes the available default options. @@ -179,3 +182,10 @@ func TraceProvider(val trace.TracerProvider) Option { o.TraceProvider = val } } + +// MetadataStorage provides a function to set the MetadataStorage option. +func MetadataStorage(ms metadata.Storage) Option { + return func(o *Options) { + o.Storage = ms + } +} diff --git a/services/graph/pkg/service/v0/password_test.go b/services/graph/pkg/service/v0/password_test.go index 47d287614f..0ca80c4906 100644 --- a/services/graph/pkg/service/v0/password_test.go +++ b/services/graph/pkg/service/v0/password_test.go @@ -13,6 +13,14 @@ import ( "github.com/go-ldap/ldap/v3" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + libregraph "github.com/opencloud-eu/libre-graph-api-go" + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" + "github.com/stretchr/testify/mock" + "google.golang.org/grpc" + "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/shared" "github.com/opencloud-eu/opencloud/services/graph/mocks" @@ -21,13 +29,6 @@ import ( "github.com/opencloud-eu/opencloud/services/graph/pkg/identity" identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks" service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" - revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" - libregraph "github.com/opencloud-eu/libre-graph-api-go" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc" ) var _ = Describe("Users changing their own password", func() { @@ -74,17 +75,25 @@ var _ = Describe("Users changing their own password", func() { GroupIDAttribute: "openCloudUUID", GroupSearchScope: "sub", } - loggger := log.NewLogger() - identityBackend, err = identity.NewLDAPBackend(ldapClient, ldapConfig, &loggger) + logger := log.NewLogger() + identityBackend, err = identity.NewLDAPBackend(ldapClient, ldapConfig, &logger) Expect(err).To(BeNil()) eventsPublisher = mocks.Publisher{} - svc, _ = service.NewService( + + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.WithIdentityBackend(identityBackend), service.EventsPublisher(&eventsPublisher), ) + Expect(err).ToNot(HaveOccurred()) + user = &userv1beta1.User{ Id: &userv1beta1.UserId{ OpaqueId: "user", diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 7236c98b9d..4840f0a4c0 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -19,7 +19,6 @@ import ( "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" "github.com/opencloud-eu/reva/v2/pkg/store" "github.com/opencloud-eu/reva/v2/pkg/utils" @@ -144,17 +143,6 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx identity.IdentityCacheWithGroupsTTL(time.Duration(options.Config.Spaces.GroupsCacheTTL)), ) - storage, err := metadata.NewCS3Storage( - options.Config.Metadata.GatewayAddress, - options.Config.Metadata.StorageAddress, - options.Config.Metadata.SystemUserID, - options.Config.Metadata.SystemUserIDP, - options.Config.Metadata.SystemUserAPIKey, - ) - if err != nil { - return Graph{}, err - } - baseGraphService := BaseGraphService{ logger: &options.Logger, identityCache: identityCache, @@ -183,7 +171,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx return Graph{}, err } - usersUserProfilePhotoService, err := NewUsersUserProfilePhotoService(storage) + usersUserProfilePhotoService, err := NewUsersUserProfilePhotoService(options.Storage) if err != nil { return Graph{}, err } diff --git a/services/graph/pkg/service/v0/sharedbyme_test.go b/services/graph/pkg/service/v0/sharedbyme_test.go index c980e12e09..2c58105a53 100644 --- a/services/graph/pkg/service/v0/sharedbyme_test.go +++ b/services/graph/pkg/service/v0/sharedbyme_test.go @@ -17,6 +17,16 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + libregraph "github.com/opencloud-eu/libre-graph-api-go" + "github.com/opencloud-eu/reva/v2/pkg/conversions" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + "github.com/opencloud-eu/reva/v2/pkg/storagespace" + "github.com/opencloud-eu/reva/v2/pkg/utils" + cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" + "github.com/stretchr/testify/mock" + "google.golang.org/grpc" + "github.com/opencloud-eu/opencloud/pkg/shared" "github.com/opencloud-eu/opencloud/services/graph/mocks" "github.com/opencloud-eu/opencloud/services/graph/pkg/config" @@ -25,15 +35,6 @@ import ( "github.com/opencloud-eu/opencloud/services/graph/pkg/linktype" service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" "github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole" - "github.com/opencloud-eu/reva/v2/pkg/conversions" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - "github.com/opencloud-eu/reva/v2/pkg/storagespace" - "github.com/opencloud-eu/reva/v2/pkg/utils" - cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" - libregraph "github.com/opencloud-eu/libre-graph-api-go" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc" ) var _ = Describe("sharedbyme", func() { @@ -245,12 +246,17 @@ var _ = Describe("sharedbyme", func() { cfg.Commons = &shared.Commons{} cfg.GRPCClientTLS = &shared.GRPCClientTLS{} - svc, _ = service.NewService( + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), ) + Expect(err).ToNot(HaveOccurred()) }) emptyListPublicSharesMock := func() { diff --git a/services/graph/pkg/service/v0/sharedwithme_test.go b/services/graph/pkg/service/v0/sharedwithme_test.go index 4a3a8c5bf4..08dc95952f 100644 --- a/services/graph/pkg/service/v0/sharedwithme_test.go +++ b/services/graph/pkg/service/v0/sharedwithme_test.go @@ -26,13 +26,15 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/proto" + libregraph "github.com/opencloud-eu/libre-graph-api-go" + "github.com/opencloud-eu/opencloud/pkg/shared" + "github.com/opencloud-eu/opencloud/services/graph/mocks" "github.com/opencloud-eu/opencloud/services/graph/pkg/config" "github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults" "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks" service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" - libregraph "github.com/opencloud-eu/libre-graph-api-go" // "github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole" ) @@ -69,11 +71,17 @@ var _ = Describe("SharedWithMe", func() { cfg.Commons = &shared.Commons{} cfg.GRPCClientTLS = &shared.GRPCClientTLS{} - svc, _ = service.NewService( + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.WithIdentityBackend(identityBackend), ) + Expect(err).ToNot(HaveOccurred()) }) Describe("ListSharedWithMe", func() { diff --git a/services/graph/pkg/service/v0/users_test.go b/services/graph/pkg/service/v0/users_test.go index 7fd8a34ce1..46a0a68e17 100644 --- a/services/graph/pkg/service/v0/users_test.go +++ b/services/graph/pkg/service/v0/users_test.go @@ -17,11 +17,11 @@ import ( "github.com/go-chi/chi/v5" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + libregraph "github.com/opencloud-eu/libre-graph-api-go" revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" - libregraph "github.com/opencloud-eu/libre-graph-api-go" "github.com/stretchr/testify/mock" "go-micro.dev/v4/client" "google.golang.org/grpc" @@ -95,8 +95,13 @@ var _ = Describe("Users", func() { When("OCM is disabled", func() { BeforeEach(func() { - svc, _ = service.NewService( + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), @@ -104,6 +109,7 @@ var _ = Describe("Users", func() { service.WithValueService(valueService), service.PermissionService(permissionService), ) + Expect(err).ToNot(HaveOccurred()) }) Describe("GetMe", func() { @@ -901,13 +907,18 @@ var _ = Describe("Users", func() { localCfg.API.UsernameMatch = usernameMatch - localSvc, _ := service.NewService( + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + localSvc, err := service.NewService( service.Config(localCfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), service.WithRoleService(roleService), ) + Expect(err).ToNot(HaveOccurred()) return localSvc } @@ -1121,8 +1132,14 @@ var _ = Describe("Users", func() { When("OCM is enabled", func() { BeforeEach(func() { cfg.IncludeOCMSharees = true - svc, _ = service.NewService( + + mds := mocks.NewStorage(GinkgoT()) + mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil) + + var err error + svc, err = service.NewService( service.Config(cfg), + service.MetadataStorage(mds), service.WithGatewaySelector(gatewaySelector), service.EventsPublisher(&eventsPublisher), service.WithIdentityBackend(identityBackend), @@ -1130,6 +1147,7 @@ var _ = Describe("Users", func() { service.WithValueService(valueService), service.PermissionService(permissionService), ) + Expect(err).ToNot(HaveOccurred()) }) Describe("GetUsers", func() { It("does not list the federated users without a filter", func() { From 93471b56dddd2582bad7824067ecc789ddaaffb8 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Thu, 22 May 2025 13:41:48 +0200 Subject: [PATCH 11/11] enhancement(graph): check the received bytes content type for profile photos --- .../v0/api_users_user_profile_photo.go | 24 ++++++--- .../v0/api_users_user_profile_photo_test.go | 50 ++++++++++++++----- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/services/graph/pkg/service/v0/api_users_user_profile_photo.go b/services/graph/pkg/service/v0/api_users_user_profile_photo.go index 3cd43c1e1f..c8d75f7c71 100644 --- a/services/graph/pkg/service/v0/api_users_user_profile_photo.go +++ b/services/graph/pkg/service/v0/api_users_user_profile_photo.go @@ -3,8 +3,10 @@ package svc import ( "context" "errors" + "fmt" "io" "net/http" + "strings" "github.com/go-chi/render" "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" @@ -33,6 +35,12 @@ var ( // ErrNoBytes is returned when no bytes are found ErrNoBytes = errors.New("no bytes") + + // ErrInvalidContentType is returned when the content type is invalid + ErrInvalidContentType = errors.New("invalid content type") + + // ErrMissingArgument is returned when a required argument is missing + ErrMissingArgument = errors.New("required argument is missing") ) // UsersUserProfilePhotoService is the implementation of the UsersUserProfilePhotoProvider interface @@ -53,12 +61,7 @@ func NewUsersUserProfilePhotoService(storage metadata.Storage) (UsersUserProfile // GetPhoto retrieves the requested photo func (s UsersUserProfilePhotoService) GetPhoto(ctx context.Context, id string) ([]byte, error) { - photo, err := s.storage.SimpleDownload(ctx, id) - if err != nil { - return nil, err - } - - return photo, nil + return s.storage.SimpleDownload(ctx, id) } // DeletePhoto deletes the requested photo @@ -68,6 +71,10 @@ func (s UsersUserProfilePhotoService) DeletePhoto(ctx context.Context, id string // UpdatePhoto updates the requested photo func (s UsersUserProfilePhotoService) UpdatePhoto(ctx context.Context, id string, r io.Reader) error { + if id == "" { + return fmt.Errorf("%w: %s", ErrMissingArgument, "id") + } + photo, err := io.ReadAll(r) if err != nil { return err @@ -77,6 +84,11 @@ func (s UsersUserProfilePhotoService) UpdatePhoto(ctx context.Context, id string return ErrNoBytes } + contentType := http.DetectContentType(photo) + if !strings.HasPrefix(contentType, "image/") { + return fmt.Errorf("%w: %s", ErrInvalidContentType, contentType) + } + return s.storage.SimpleUpload(ctx, id, photo) } diff --git a/services/graph/pkg/service/v0/api_users_user_profile_photo_test.go b/services/graph/pkg/service/v0/api_users_user_profile_photo_test.go index cf38bcddfa..500148809c 100644 --- a/services/graph/pkg/service/v0/api_users_user_profile_photo_test.go +++ b/services/graph/pkg/service/v0/api_users_user_profile_photo_test.go @@ -1,6 +1,7 @@ package svc_test import ( + "bytes" "context" "errors" "io" @@ -17,25 +18,50 @@ import ( svc "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" ) +func TestNewUsersUserProfilePhotoService(t *testing.T) { + storage := mocks.NewStorage(t) + storage.EXPECT().Init(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, id string) error { return nil }) + + service, err := svc.NewUsersUserProfilePhotoService(storage) + assert.NoError(t, err) + + t.Run("UpdatePhoto", func(t *testing.T) { + t.Run("reports an error if id is empty", func(t *testing.T) { + err := service.UpdatePhoto(context.Background(), "", bytes.NewReader([]byte{})) + assert.ErrorIs(t, err, svc.ErrMissingArgument) + }) + + t.Run("reports an error if the reader does not contain any bytes", func(t *testing.T) { + err := service.UpdatePhoto(context.Background(), "123", bytes.NewReader([]byte{})) + assert.ErrorIs(t, err, svc.ErrNoBytes) + }) + + t.Run("reports an error if data is not an image", func(t *testing.T) { + err := service.UpdatePhoto(context.Background(), "234", bytes.NewReader([]byte("not an image"))) + assert.ErrorIs(t, err, svc.ErrInvalidContentType) + }) + }) +} + func TestUsersUserProfilePhotoApi(t *testing.T) { var ( - usersUserProfilePhotoProvider = mocks.NewUsersUserProfilePhotoProvider(t) - dummyDataProvider = func(w http.ResponseWriter, r *http.Request) (string, bool) { + serviceProvider = mocks.NewUsersUserProfilePhotoProvider(t) + dataProvider = func(w http.ResponseWriter, r *http.Request) (string, bool) { return "123", true } ) - api, err := svc.NewUsersUserProfilePhotoApi(usersUserProfilePhotoProvider, log.NopLogger()) + api, err := svc.NewUsersUserProfilePhotoApi(serviceProvider, log.NopLogger()) assert.NoError(t, err) t.Run("GetProfilePhoto", func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/", nil) - ep := api.GetProfilePhoto(dummyDataProvider) + ep := api.GetProfilePhoto(dataProvider) t.Run("fails if photo provider errors", func(t *testing.T) { w := httptest.NewRecorder() - usersUserProfilePhotoProvider.EXPECT().GetPhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) ([]byte, error) { + serviceProvider.EXPECT().GetPhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) ([]byte, error) { return nil, errors.New("any") }).Once() @@ -47,7 +73,7 @@ func TestUsersUserProfilePhotoApi(t *testing.T) { t.Run("successfully returns the requested photo", func(t *testing.T) { w := httptest.NewRecorder() - usersUserProfilePhotoProvider.EXPECT().GetPhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) ([]byte, error) { + serviceProvider.EXPECT().GetPhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) ([]byte, error) { return []byte("photo"), nil }).Once() @@ -60,12 +86,12 @@ func TestUsersUserProfilePhotoApi(t *testing.T) { t.Run("DeleteProfilePhoto", func(t *testing.T) { r := httptest.NewRequest(http.MethodDelete, "/", nil) - ep := api.DeleteProfilePhoto(dummyDataProvider) + ep := api.DeleteProfilePhoto(dataProvider) t.Run("fails if photo provider errors", func(t *testing.T) { w := httptest.NewRecorder() - usersUserProfilePhotoProvider.EXPECT().DeletePhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) error { + serviceProvider.EXPECT().DeletePhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) error { return errors.New("any") }).Once() @@ -77,7 +103,7 @@ func TestUsersUserProfilePhotoApi(t *testing.T) { t.Run("successfully deletes the requested photo", func(t *testing.T) { w := httptest.NewRecorder() - usersUserProfilePhotoProvider.EXPECT().DeletePhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) error { + serviceProvider.EXPECT().DeletePhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) error { return nil }).Once() @@ -89,12 +115,12 @@ func TestUsersUserProfilePhotoApi(t *testing.T) { t.Run("UpsertProfilePhoto", func(t *testing.T) { r := httptest.NewRequest(http.MethodPut, "/", strings.NewReader("body")) - ep := api.UpsertProfilePhoto(dummyDataProvider) + ep := api.UpsertProfilePhoto(dataProvider) t.Run("fails if photo provider errors", func(t *testing.T) { w := httptest.NewRecorder() - usersUserProfilePhotoProvider.EXPECT().UpdatePhoto(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string, r io.Reader) error { + serviceProvider.EXPECT().UpdatePhoto(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string, r io.Reader) error { return errors.New("any") }).Once() @@ -106,7 +132,7 @@ func TestUsersUserProfilePhotoApi(t *testing.T) { t.Run("successfully upserts the photo", func(t *testing.T) { w := httptest.NewRecorder() - usersUserProfilePhotoProvider.EXPECT().UpdatePhoto(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string, r io.Reader) error { + serviceProvider.EXPECT().UpdatePhoto(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string, r io.Reader) error { return nil }).Once()