From 71d321b0c79f470c166cc4164bbd2c587468961a Mon Sep 17 00:00:00 2001 From: Roman Perekhod <2403905@gmail.com> Date: Mon, 9 Mar 2026 12:27:25 +0100 Subject: [PATCH 01/12] feat: Implemet the vault-storage feat: Separate the storage-users and graph to handle feat: move the space create cache configurate the vault srorage Postprocessing hide the assignment of the MountID to VaultStorageProviderID behind a flag to avoid the ID mismatch configurate the proxy to create the vault home space update the filter Make the graph able to handle vault prefix. no extra service needed apply the vault filter to sharedbyme and sharedwithme added vault-storage docker use squashed reva fix the finishUpload event restict webdav copy/move from the vault --- .gitignore | 2 + .make/go.mk | 2 - deployments/examples/ocis_full/.env | 7 ++- .../examples/ocis_full/vault-storage.yml | 37 ++++++++++++++++ go.mod | 2 +- go.sum | 4 +- services/gateway/pkg/revaconfig/config.go | 16 +++++++ services/graph/pkg/config/config.go | 2 + services/graph/pkg/config/service.go | 2 +- services/graph/pkg/middleware/mfa.go | 23 ++++++++++ services/graph/pkg/middleware/vault.go | 27 ++++++++++++ services/graph/pkg/service/v0/driveitems.go | 6 +++ services/graph/pkg/service/v0/drives.go | 27 ++++++------ services/graph/pkg/service/v0/graph_test.go | 6 +-- services/graph/pkg/service/v0/service.go | 22 +++++++--- services/graph/pkg/service/v0/sharedbyme.go | 12 +++++- services/graph/pkg/service/v0/sharedwithme.go | 19 ++++++++ .../graph/pkg/service/v0/spacetemplates.go | 7 ++- .../policies/pkg/service/event/service.go | 1 + .../pkg/postprocessing/postprocessing.go | 1 + services/proxy/pkg/command/server.go | 1 + services/proxy/pkg/config/config.go | 1 + .../pkg/config/defaults/defaultconfig.go | 4 ++ services/proxy/pkg/middleware/create_home.go | 43 ++++++++++++++++++- services/proxy/pkg/middleware/options.go | 9 ++++ services/storage-users/pkg/config/config.go | 3 ++ .../pkg/config/defaults/defaultconfig.go | 6 +++ .../storage-users/pkg/revaconfig/drivers.go | 7 ++- .../grpc/services/gateway/storageprovider.go | 17 ++++++++ .../services/gateway/storageprovidercache.go | 15 +++++-- .../storageprovider/storageprovider.go | 3 +- .../http/services/owncloud/ocdav/copy.go | 7 +++ .../http/services/owncloud/ocdav/move.go | 7 +++ .../http/services/owncloud/ocdav/ocdav.go | 12 ++++++ .../handlers/apps/sharing/shares/spaces.go | 16 +------ .../reva/v2/pkg/events/postprocessing.go | 2 + .../v2/pkg/storage/registry/spaces/spaces.go | 27 ++++++++++-- .../utils/decomposedfs/decomposedfs.go | 14 +++++- .../utils/decomposedfs/options/options.go | 9 +++- .../utils/decomposedfs/upload/upload.go | 2 +- .../owncloud/reva/v2/pkg/utils/utils.go | 3 ++ vendor/modules.txt | 2 +- 42 files changed, 372 insertions(+), 63 deletions(-) create mode 100644 deployments/examples/ocis_full/vault-storage.yml create mode 100644 services/graph/pkg/middleware/mfa.go create mode 100644 services/graph/pkg/middleware/vault.go diff --git a/.gitignore b/.gitignore index 33e55761570..1a50be5c2be 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ protogen/buf.sha1.lock /third-party-licenses # misc +.agents/ /tmp go.work go.work.sum @@ -64,4 +65,5 @@ go.work.sum .envrc CLAUDE.md .claude/ +GEMINI.md .agents/ \ No newline at end of file diff --git a/.make/go.mk b/.make/go.mk index 7000c56e383..a16d58c4f21 100644 --- a/.make/go.mk +++ b/.make/go.mk @@ -118,7 +118,6 @@ debug-linux-docker-amd64: release-dirs -gcflags="all=-N -l" \ -tags 'netgo $(TAGS)' \ -buildmode=exe \ - -trimpath \ -ldflags '-extldflags "-static" $(DEBUG_LDFLAGS) $(DOCKER_LDFLAGS)' \ -o '$(DIST)/binaries/$(EXECUTABLE)-linux-amd64' \ ./cmd/$(NAME) @@ -130,7 +129,6 @@ debug-linux-docker-arm64: release-dirs -gcflags="all=-N -l" \ -tags 'netgo $(TAGS)' \ -buildmode=exe \ - -trimpath \ -ldflags '-extldflags "-static" $(DEBUG_LDFLAGS) $(DOCKER_LDFLAGS)' \ -o '$(DIST)/binaries/$(EXECUTABLE)-linux-arm64' \ ./cmd/$(NAME) diff --git a/deployments/examples/ocis_full/.env b/deployments/examples/ocis_full/.env index cd8bccf424f..b87b403717b 100644 --- a/deployments/examples/ocis_full/.env +++ b/deployments/examples/ocis_full/.env @@ -186,6 +186,11 @@ KEYCLOAK_TRACING= # Note: the leading colon is required to enable the service. #KEYCLOAK=:keycloak.yml +### oCIS Vault Storage Settings ### +# Enable the oCIS vault storage +# Note: the leading colon is required to enable the service. +#VAULT_STORAGE=:vault-storage.yml + ## Default Enabled Services ## @@ -297,4 +302,4 @@ MAIL_SERVER_DOCKER_TAG=v1.29.3 # This MUST be the last line as it assembles the supplemental compose files to be used. # ALL supplemental configs must be added here, whether commented or not. # Each var must either be empty or contain :path/file.yml -COMPOSE_FILE=docker-compose.yml${OCIS:-}${TIKA:-}${S3NG:-}${S3NG_MINIO:-}${COLLABORA:-}${IMPORTER:-}${CLAMAV:-}${ONLYOFFICE:-}${EXTENSIONS:-}${UNZIP:-}${DRAWIO:-}${JSONVIEWER:-}${PROGRESSBARS:-}${EXTERNALSITES:-}${PHOTOADDON:-}${ADVANCEDSEARCH:-}${MAIL_SERVER:-}${MONITORING:-}${KEYCLOAK:-} +COMPOSE_FILE=docker-compose.yml${OCIS:-}${TIKA:-}${S3NG:-}${S3NG_MINIO:-}${COLLABORA:-}${IMPORTER:-}${CLAMAV:-}${ONLYOFFICE:-}${EXTENSIONS:-}${UNZIP:-}${DRAWIO:-}${JSONVIEWER:-}${PROGRESSBARS:-}${EXTERNALSITES:-}${PHOTOADDON:-}${ADVANCEDSEARCH:-}${MAIL_SERVER:-}${MONITORING:-}${KEYCLOAK:-}${VAULT_STORAGE:-} diff --git a/deployments/examples/ocis_full/vault-storage.yml b/deployments/examples/ocis_full/vault-storage.yml new file mode 100644 index 00000000000..d5ad6d3c093 --- /dev/null +++ b/deployments/examples/ocis_full/vault-storage.yml @@ -0,0 +1,37 @@ +services: + ocis: + environment: + OCIS_MFA_ENABLED: true + NATS_NATS_HOST: 0.0.0.0 + SETTINGS_GRPC_ADDR: ocis:9191 + PROXY_CREATE_VAULT_HOME: true + GRAPH_ENABLE_VAULT_MODE: true + + storage-users-vault: + image: ${OCIS_DOCKER_IMAGE:-owncloud/ocis}:${OCIS_DOCKER_TAG:-latest} + networks: + ocis-net: + depends_on: + ocis: + condition: service_started + command: ["storage-users", "server"] + environment: + OCIS_LOG_LEVEL: debug + OCIS_GATEWAY_GRPC_ADDR: ocis:9142 + STORAGE_USERS_ENABLE_VAULT_MODE: true + STORAGE_USERS_SERVICE_NAME: storage-users-vault + STORAGE_USERS_GRPC_ADDR: storage-users-vault:9170 + STORAGE_USERS_HTTP_ADDR: storage-users-vault:9168 + STORAGE_USERS_DATA_SERVER_URL: http://storage-users-vault:9168/data + STORAGE_USERS_DEBUG_ADDR: storage-users-vault:9169 + STORAGE_USERS_OCIS_ROOT: /var/lib/ocis/storage/users-vault + STORAGE_USERS_EVENTS_CONSUMER_GROUP: vault-dcfs + MICRO_REGISTRY_ADDRESS: ocis:9233 + OCIS_EVENTS_ENDPOINT: ocis:9233 + OCIS_CACHE_STORE_NODES: ocis:9233 + volumes: + - ocis-data:/var/lib/ocis + - ocis-config:/etc/ocis + logging: + driver: ${LOG_DRIVER:-local} + restart: always diff --git a/go.mod b/go.mod index 669816983e9..ba108aff485 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/open-policy-agent/opa v1.12.3 github.com/orcaman/concurrent-map v1.0.0 github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 - github.com/owncloud/reva/v2 v2.0.0-20260324082555-823c2f1c2593 + github.com/owncloud/reva/v2 v2.0.0-20260408163105-01eba1425b63 github.com/pkg/errors v0.9.1 github.com/pkg/xattr v0.4.12 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index b7c43bda1ae..8f4fd7b37e2 100644 --- a/go.sum +++ b/go.sum @@ -742,8 +742,8 @@ github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HD github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 h1:JRidLTAKhnvyLMRtVtSF4lhBa0NSAOs6fof+d6JnKII= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245/go.mod h1:z61VMGAJRtR1nbgXWiNoCkxUXP1B3Je9rMuJbnGd+Og= -github.com/owncloud/reva/v2 v2.0.0-20260324082555-823c2f1c2593 h1:RNHAod2gNBEac0KQJfJ6+PCX1t7g9hFmONTGrXFvFII= -github.com/owncloud/reva/v2 v2.0.0-20260324082555-823c2f1c2593/go.mod h1:+rCy6oGYb2/qs5gmQa8y/pHARw634vB73MZGDY2SBIQ= +github.com/owncloud/reva/v2 v2.0.0-20260408163105-01eba1425b63 h1:t02rNoa8vXX8Yu461uX8uR/ATOxx+KqUB4LebgqAKic= +github.com/owncloud/reva/v2 v2.0.0-20260408163105-01eba1425b63/go.mod h1:+rCy6oGYb2/qs5gmQa8y/pHARw634vB73MZGDY2SBIQ= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pablodz/inotifywaitgo v0.0.9 h1:njquRbBU7fuwIe5rEvtaniVBjwWzcpdUVptSgzFqZsw= diff --git a/services/gateway/pkg/revaconfig/config.go b/services/gateway/pkg/revaconfig/config.go index 44dcda07348..a24a9a0e371 100644 --- a/services/gateway/pkg/revaconfig/config.go +++ b/services/gateway/pkg/revaconfig/config.go @@ -152,6 +152,22 @@ func spacesProviders(cfg *config.Config, logger log.Logger) map[string]map[strin }, }, }, + "com.owncloud.api.storage-users-vault": { + // Use the dedicated storage provider for vault + "providerid": utils.VaultStorageProviderID, + "spaces": map[string]interface{}{ + "personal": map[string]interface{}{ + // The mount point must have the "vault/" prefix to be picked up by the vault storage provider + "mount_point": "/vault/users", + "path_template": "/vault/users/{{.Space.Owner.Id.OpaqueId}}", + }, + "project": map[string]interface{}{ + // The mount point must have the "vault/" prefix to be picked up by the vault storage provider + "mount_point": "/vault/projects", + "path_template": "/vault/projects/{{.Space.Name}}", + }, + }, + }, cfg.StorageSharesEndpoint: { "providerid": utils.ShareStorageProviderID, "spaces": map[string]interface{}{ diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index 517a9d978e9..2eaad66190d 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -39,6 +39,8 @@ type Config struct { Validation Validation `yaml:"validation"` + EnableVaultMode bool `yaml:"enable_vault_mode" env:"GRAPH_ENABLE_VAULT_MODE" desc:"Enable vault mode for the graph service runned in addition to the regular graph service. Required the running the storage-users-vault additional service." introductionVersion:"daledda"` + Context context.Context `yaml:"-"` } diff --git a/services/graph/pkg/config/service.go b/services/graph/pkg/config/service.go index d1eac383f0b..f7edce2b7dd 100644 --- a/services/graph/pkg/config/service.go +++ b/services/graph/pkg/config/service.go @@ -2,5 +2,5 @@ package config // Service defines the available service configuration. type Service struct { - Name string `yaml:"-"` + Name string `yaml:"name" env:"GRAPH_SERVICE_NAME" desc:"The name of the service." introductionVersion:"daledda"` } diff --git a/services/graph/pkg/middleware/mfa.go b/services/graph/pkg/middleware/mfa.go new file mode 100644 index 00000000000..33c1206e09b --- /dev/null +++ b/services/graph/pkg/middleware/mfa.go @@ -0,0 +1,23 @@ +package middleware + +import ( + "net/http" + + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/ocis-pkg/mfa" +) + +// RequireMFA middleware is used to require the user in context to have MFA satisfied +func RequireMFA(logger log.Logger) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !mfa.Has(r.Context()) { + l := logger.SubloggerWithRequestID(r.Context()) + l.Error().Str("path", r.URL.Path).Msg("MFA required but not satisfied") + mfa.SetRequiredStatus(w) + return + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/services/graph/pkg/middleware/vault.go b/services/graph/pkg/middleware/vault.go new file mode 100644 index 00000000000..5dfd3450f8f --- /dev/null +++ b/services/graph/pkg/middleware/vault.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "context" + "net/http" +) + +type key int + +const vaultModeKey key = iota + +func SetVaultMode(ctx context.Context, enabled bool) context.Context { + return context.WithValue(ctx, vaultModeKey, enabled) +} + +func IsVaultMode(ctx context.Context) bool { + val, ok := ctx.Value(vaultModeKey).(bool) + return val && ok +} + +func VaultModeMiddleware() func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r.WithContext(SetVaultMode(r.Context(), true))) + }) + } +} diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index 2a134500cab..0edcbf50ea7 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -30,6 +30,7 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" + "github.com/owncloud/ocis/v2/services/graph/pkg/middleware" ) // CreateUploadSession create an upload session to allow your app to upload files up to the maximum file size. @@ -158,6 +159,11 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { filters = append(filters, listStorageSpacesUserFilter(currentUser.GetId().GetOpaqueId())) filters = append(filters, listStorageSpacesTypeFilter("personal")) + // force vault storage space if vault mode is enabled + if middleware.IsVaultMode(ctx) { + filters = append(filters, listStorageSpacesIDFilter(storagespace.FormatStorageID(utils.VaultStorageProviderID, currentUser.GetId().GetOpaqueId()))) + } + res, err := gatewayClient.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{ Filters: filters, }) diff --git a/services/graph/pkg/service/v0/drives.go b/services/graph/pkg/service/v0/drives.go index c4657b32054..bdb0e943414 100644 --- a/services/graph/pkg/service/v0/drives.go +++ b/services/graph/pkg/service/v0/drives.go @@ -29,10 +29,10 @@ import ( "google.golang.org/protobuf/proto" "github.com/owncloud/ocis/v2/ocis-pkg/l10n" - "github.com/owncloud/ocis/v2/ocis-pkg/mfa" v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" + "github.com/owncloud/ocis/v2/services/graph/pkg/middleware" settingsServiceExt "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults" ) @@ -133,13 +133,6 @@ func (g Graph) GetAllDrives(version APIVersion) http.HandlerFunc { // GetAllDrivesV1 attempts to retrieve the current users drives; // it includes another user's drives, if the current user has the permission. func (g Graph) GetAllDrivesV1(w http.ResponseWriter, r *http.Request) { - if !mfa.Has(r.Context()) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - logger.Error().Str("path", r.URL.Path).Msg("MFA required but not satisfied") - mfa.SetRequiredStatus(w) - return - } - spaces, errCode := g.getDrives(r, true, APIVersion_1) if errCode != nil { errorcode.RenderError(w, r, errCode) @@ -160,13 +153,6 @@ func (g Graph) GetAllDrivesV1(w http.ResponseWriter, r *http.Request) { // it includes the grantedtoV2 property // it uses unified roles instead of the cs3 representations func (g Graph) GetAllDrivesV1Beta1(w http.ResponseWriter, r *http.Request) { - if !mfa.Has(r.Context()) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - logger.Error().Str("path", r.URL.Path).Msg("MFA required but not satisfied") - mfa.SetRequiredStatus(w) - return - } - drives, errCode := g.getDrives(r, true, APIVersion_1_Beta_1) if errCode != nil { errorcode.RenderError(w, r, errCode) @@ -437,6 +423,11 @@ func (g Graph) createDrive(w http.ResponseWriter, r *http.Request, apiVersion AP csr.Owner = us } + // force vault storage space if vault mode is enabled + if middleware.IsVaultMode(ctx) { + csr.Opaque = utils.AppendPlainToOpaque(csr.Opaque, "storage_id", utils.VaultStorageProviderID) + } + resp, err := gatewayClient.CreateStorageSpace(ctx, &csr) if err != nil { logger.Error().Err(err).Msg("could not create drive: transport error") @@ -762,6 +753,7 @@ func (g Graph) ListStorageSpacesWithFilters(ctx context.Context, filters []*stor if err != nil { return nil, err } + lReq := &storageprovider.ListStorageSpacesRequest{ Opaque: &types.Opaque{Map: map[string]*types.OpaqueEntry{ "permissions": { @@ -776,6 +768,11 @@ func (g Graph) ListStorageSpacesWithFilters(ctx context.Context, filters []*stor Filters: filters, } + // force vault storage space if vault mode is enabled + if middleware.IsVaultMode(ctx) { + utils.AppendPlainToOpaque(lReq.Opaque, "storage_id", utils.VaultStorageProviderID) + } + gatewayClient, err := g.gatewaySelector.Next() if err != nil { return nil, err diff --git a/services/graph/pkg/service/v0/graph_test.go b/services/graph/pkg/service/v0/graph_test.go index 1112dacb2bd..f8c58e4b6cc 100644 --- a/services/graph/pkg/service/v0/graph_test.go +++ b/services/graph/pkg/service/v0/graph_test.go @@ -113,7 +113,7 @@ var _ = Describe("Graph", func() { r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives", nil) r = r.WithContext(ctx) rr := httptest.NewRecorder() - svc.GetDrivesV1(rr, r) + svc.ServeHTTP(rr, r) Expect(rr.Code).To(Equal(http.StatusOK)) }) @@ -126,7 +126,7 @@ var _ = Describe("Graph", func() { r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives", nil) r = r.WithContext(mfa.Set(ctx, true)) rr := httptest.NewRecorder() - svc.GetAllDrivesV1(rr, r) + svc.ServeHTTP(rr, r) Expect(rr.Code).To(Equal(http.StatusOK)) }) @@ -138,7 +138,7 @@ var _ = Describe("Graph", func() { r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives", nil) rr := httptest.NewRecorder() - svc.GetAllDrivesV1(rr, r) + svc.ServeHTTP(rr, r) Expect(rr.Code).To(Equal(http.StatusForbidden)) Expect(rr.Header().Get("X-Ocis-Mfa-Required")).To(Equal("true")) }) diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index a24029616f2..8969c960eaf 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -203,6 +203,8 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx requireAdmin = options.RequireAdminMiddleware } + requireMFA := graphm.RequireMFA(options.Logger) + drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector) if err != nil { return svc, err @@ -223,9 +225,8 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx return svc, err } - m.Route(options.Config.HTTP.Root, func(r chi.Router) { + graphRoutes := func(r chi.Router) { r.Use(middleware.StripSlashes) - r.Route("/v1beta1", func(r chi.Router) { r.Route("/me", func(r chi.Router) { r.Get("/drives", svc.GetDrives(APIVersion_1_Beta_1)) @@ -235,7 +236,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx }) }) r.Route("/drives", func(r chi.Router) { - r.Get("/", svc.GetAllDrives(APIVersion_1_Beta_1)) + r.With(requireMFA).Get("/", svc.GetAllDrives(APIVersion_1_Beta_1)) r.Post("/", svc.CreateDriveV1Beta1) r.Route("/{driveID}", func(r chi.Router) { r.Get("/", svc.GetSingleDriveV1Beta1) @@ -331,7 +332,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx }) }) r.Route("/drives", func(r chi.Router) { - r.Get("/", svc.GetAllDrives(APIVersion_1)) + r.With(requireMFA).Get("/", svc.GetAllDrives(APIVersion_1)) r.Post("/", svc.CreateDrive) r.Route("/{driveID}", func(r chi.Router) { r.Patch("/", svc.UpdateDrive) @@ -394,7 +395,18 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx }) }) }) - }) + } + + m.Route(options.Config.HTTP.Root, graphRoutes) + + // Ini the Vault routes + if options.Config.EnableVaultMode { + m.Route("/vault/graph", func(r chi.Router) { + r.Use(requireMFA) + r.Use(graphm.VaultModeMiddleware()) + graphRoutes(r) + }) + } _ = chi.Walk(m, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint") diff --git a/services/graph/pkg/service/v0/sharedbyme.go b/services/graph/pkg/service/v0/sharedbyme.go index 0daf1fa2e6d..3666696ca8e 100644 --- a/services/graph/pkg/service/v0/sharedbyme.go +++ b/services/graph/pkg/service/v0/sharedbyme.go @@ -5,8 +5,11 @@ import ( "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/reva/v2/pkg/storagespace" + "github.com/owncloud/reva/v2/pkg/utils" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" + "github.com/owncloud/ocis/v2/services/graph/pkg/middleware" ) type driveItemsByResourceID map[string]libregraph.DriveItem @@ -39,8 +42,15 @@ func (g Graph) GetSharedByMe(w http.ResponseWriter, r *http.Request) { } res := make([]libregraph.DriveItem, 0, len(driveItems)) + isVault := middleware.IsVaultMode(ctx) for _, v := range driveItems { - res = append(res, v) + storageID, _ := storagespace.SplitStorageID(v.GetId()) + // filters out shares that are not relevant to the current mode (vault or regular). + if isVault && storageID == utils.VaultStorageProviderID { + res = append(res, v) + } else if !isVault && storageID != utils.VaultStorageProviderID { + res = append(res, v) + } } render.Status(r, http.StatusOK) diff --git a/services/graph/pkg/service/v0/sharedwithme.go b/services/graph/pkg/service/v0/sharedwithme.go index 0a51d8b0880..a25ffadd702 100644 --- a/services/graph/pkg/service/v0/sharedwithme.go +++ b/services/graph/pkg/service/v0/sharedwithme.go @@ -8,8 +8,10 @@ import ( ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/reva/v2/pkg/utils" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" + "github.com/owncloud/ocis/v2/services/graph/pkg/middleware" "github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole" ) @@ -40,6 +42,9 @@ func (g Graph) listSharedWithMe(ctx context.Context) ([]libregraph.DriveItem, er g.logger.Error().Err(err).Msg("listing shares failed") return nil, err } + + listReceivedSharesResponse.Shares = filterVaultShares(ctx, listReceivedSharesResponse.GetShares()) + availableRoles := unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(g.config.UnifiedRoles.AvailableRoles...)) driveItems, err := cs3ReceivedSharesToDriveItems(ctx, g.logger, gatewayClient, g.identityCache, listReceivedSharesResponse.GetShares(), availableRoles) if err != nil { @@ -63,3 +68,17 @@ func (g Graph) listSharedWithMe(ctx context.Context) ([]libregraph.DriveItem, er return driveItems, err } + +// filterVaultShares filters out shares that are not relevant to the current mode (vault or regular). +func filterVaultShares(ctx context.Context, shares []*collaboration.ReceivedShare) []*collaboration.ReceivedShare { + result := make([]*collaboration.ReceivedShare, 0, len(shares)) + isVault := middleware.IsVaultMode(ctx) + for _, share := range shares { + if isVault && share.GetShare().GetResourceId().StorageId == utils.VaultStorageProviderID { + result = append(result, share) + } else if !isVault && share.GetShare().GetResourceId().StorageId != utils.VaultStorageProviderID { + result = append(result, share) + } + } + return result +} diff --git a/services/graph/pkg/service/v0/spacetemplates.go b/services/graph/pkg/service/v0/spacetemplates.go index 05b7ad02461..0f5e9798fba 100644 --- a/services/graph/pkg/service/v0/spacetemplates.go +++ b/services/graph/pkg/service/v0/spacetemplates.go @@ -13,6 +13,7 @@ import ( v1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/owncloud/ocis/v2/ocis-pkg/l10n" l10n_pkg "github.com/owncloud/ocis/v2/services/graph/pkg/l10n" + "github.com/owncloud/ocis/v2/services/graph/pkg/middleware" "github.com/owncloud/reva/v2/pkg/storage/utils/metadata" "github.com/owncloud/reva/v2/pkg/storagespace" "github.com/owncloud/reva/v2/pkg/utils" @@ -53,7 +54,11 @@ func (g Graph) applySpaceTemplate(ctx context.Context, gwc gateway.GatewayAPICli } func (g Graph) applyDefaultTemplate(ctx context.Context, gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, locale string) error { - mdc := metadata.NewCS3(g.config.Reva.Address, g.config.Spaces.StorageUsersAddress) + storageUsersAddress := g.config.Spaces.StorageUsersAddress + if middleware.IsVaultMode(ctx) { + storageUsersAddress = storageUsersAddress + "-vault" + } + mdc := metadata.NewCS3(g.config.Reva.Address, storageUsersAddress) mdc.SpaceRoot = root var opaque *v1beta1.Opaque diff --git a/services/policies/pkg/service/event/service.go b/services/policies/pkg/service/event/service.go index 69f035eebd2..defbd60fa14 100644 --- a/services/policies/pkg/service/event/service.go +++ b/services/policies/pkg/service/event/service.go @@ -125,6 +125,7 @@ func (s Service) processEvent(e events.Event) error { if err := events.Publish(ctx, s.stream, events.PostprocessingStepFinished{ Outcome: outcome, UploadID: ev.UploadID, + ResourceID: ev.ResourceID, ExecutingUser: ev.ExecutingUser, Filename: ev.Filename, FinishedStep: ev.StepToStart, diff --git a/services/postprocessing/pkg/postprocessing/postprocessing.go b/services/postprocessing/pkg/postprocessing/postprocessing.go index aca4ea3e86d..d067dcbe34b 100644 --- a/services/postprocessing/pkg/postprocessing/postprocessing.go +++ b/services/postprocessing/pkg/postprocessing/postprocessing.go @@ -119,6 +119,7 @@ func (pp *Postprocessing) finished(outcome events.PostprocessingOutcome) events. UploadID: pp.ID, ExecutingUser: pp.User, Filename: pp.Filename, + ResourceID: pp.ResourceID, Outcome: outcome, ImpersonatingUser: pp.ImpersonatingUser, } diff --git a/services/proxy/pkg/command/server.go b/services/proxy/pkg/command/server.go index c0a08116358..348f10a87f8 100644 --- a/services/proxy/pkg/command/server.go +++ b/services/proxy/pkg/command/server.go @@ -373,6 +373,7 @@ func loadMiddlewares(logger log.Logger, cfg *config.Config, middleware.Logger(logger), middleware.WithRevaGatewaySelector(gatewaySelector), middleware.RoleQuotas(cfg.RoleQuotas), + middleware.CreateVaultHome(cfg.CreateVaultHome), ), // trigger space assignment when a user logs in middleware.SpaceManager( diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go index 9ce6faf3f1d..7a2684c9136 100644 --- a/services/proxy/pkg/config/config.go +++ b/services/proxy/pkg/config/config.go @@ -48,6 +48,7 @@ type Config struct { ClaimSpaceManagement ClaimSpaceManagement `yaml:"claim_space_management"` MultiFactorAuthentication MFAConfig `yaml:"mfa"` MultiInstance MultiInstanceConfig `yaml:"multi_instance"` + CreateVaultHome bool `yaml:"create_vault_home" env:"PROXY_CREATE_VAULT_HOME" desc:"Set this to true to automatically create a new vault home for the user if it does not exist." introductionVersion:"daledda"` Context context.Context `json:"-" yaml:"-"` } diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index 820590f6f29..a1a97b454f1 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -273,6 +273,10 @@ func DefaultPolicies() []config.Policy { Endpoint: "/graph/", Service: "com.owncloud.web.graph", }, + { + Endpoint: "/vault/graph/", + Service: "com.owncloud.web.graph", + }, { Endpoint: "/api/v0/settings", Service: "com.owncloud.web.settings", diff --git a/services/proxy/pkg/middleware/create_home.go b/services/proxy/pkg/middleware/create_home.go index 4caf453ef31..670cf6877ce 100644 --- a/services/proxy/pkg/middleware/create_home.go +++ b/services/proxy/pkg/middleware/create_home.go @@ -13,6 +13,7 @@ import ( "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" revactx "github.com/owncloud/reva/v2/pkg/ctx" "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" + "github.com/owncloud/reva/v2/pkg/storagespace" "github.com/owncloud/reva/v2/pkg/utils" "google.golang.org/grpc/metadata" ) @@ -50,11 +51,23 @@ func (m *createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { token := req.Header.Get("x-access-token") // we need to pass the token to authenticate the CreateHome request. - //ctx := tokenpkg.ContextSetToken(r.Context(), token) ctx := metadata.AppendToOutgoingContext(req.Context(), revactx.TokenHeader, token) createHomeReq := &provider.CreateHomeRequest{} u, ok := revactx.ContextGetUser(ctx) + if !ok || u == nil { + m.logger.Error().Msg("no user in context") + m.next.ServeHTTP(w, req) + return + } + roleIDs, err := m.getUserRoles(u) + if err != nil { + m.logger.Error().Err(err).Str("userid", u.Id.OpaqueId).Msg("failed to get roles for user") + errorcode.GeneralException.Render(w, req, http.StatusInternalServerError, "Unauthorized") + return + } + if limit, hasLimit := m.checkRoleQuotaLimit(roleIDs); hasLimit { + createHomeReq.Opaque = utils.AppendPlainToOpaque(nil, "quota", strconv.FormatUint(limit, 10)) if ok { roleIDs, err := m.getUserRoles(u) if err != nil { @@ -85,6 +98,34 @@ func (m *createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { m.cache.Store(key, struct{}{}) default: m.logger.Error().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", createHomeRes.GetStatus()).Msg("personal space creation failed") + m.cache.Set(key, struct{}{}, 0) + case createHomeRes.GetStatus().GetCode() == rpc.Code_CODE_ALREADY_EXISTS: + m.logger.Info().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", createHomeRes.GetStatus()).Msg("personal space already exists") + m.cache.Set(key, struct{}{}, 0) + default: + m.logger.Error().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", createHomeRes.GetStatus()).Msg("personal space creation failed") + } + } + + if m.createVaultHome { + vaultKey := storagespace.FormatStorageID(utils.VaultStorageProviderID, u.GetId().GetOpaqueId()) + if !m.cache.Has(vaultKey) { + // Create vault personal space + // Inject storage_id into opaque for vault personal space + createHomeReq.Opaque = utils.AppendPlainToOpaque(createHomeReq.Opaque, "storage_id", utils.VaultStorageProviderID) + cpsRes, err := client.CreateHome(ctx, createHomeReq) + switch { + case err != nil: + m.logger.Err(err).Msg("error calling CreateHome for vault personal") + case cpsRes.GetStatus().GetCode() == rpc.Code_CODE_OK: + m.logger.Debug().Interface("userID", u.GetId().GetOpaqueId()).Msg("vault personal space created") + m.cache.Set(vaultKey, struct{}{}, 0) + case cpsRes.GetStatus().GetCode() == rpc.Code_CODE_ALREADY_EXISTS: + m.logger.Info().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", cpsRes.GetStatus()).Msg("vault personal space already exists") + m.cache.Set(vaultKey, struct{}{}, 0) + default: + m.logger.Error().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", cpsRes.GetStatus()).Msg("vault personal space creation failed") + } } } } diff --git a/services/proxy/pkg/middleware/options.go b/services/proxy/pkg/middleware/options.go index 503273a564e..243d69114c7 100644 --- a/services/proxy/pkg/middleware/options.go +++ b/services/proxy/pkg/middleware/options.go @@ -69,6 +69,8 @@ type Options struct { // RoleQuotas hold userid:quota mappings. These will be used when provisioning new users. // The users will get as much quota as is set for their role. RoleQuotas map[string]uint64 + // CreateVaultHome creates a new vault home for the user if it does not exist. + CreateVaultHome bool // TraceProvider sets the tracing provider. TraceProvider trace.TracerProvider // SkipUserInfo prevents the oidc middleware from querying the userinfo endpoint and read any claims directly from the access token instead @@ -243,6 +245,13 @@ func RoleQuotas(roleQuotas map[string]uint64) Option { } } +// CreateVaultHome sets the create vault home flag +func CreateVaultHome(createVaultHome bool) Option { + return func(o *Options) { + o.CreateVaultHome = createVaultHome + } +} + // TraceProvider sets the tracing provider. func TraceProvider(tp trace.TracerProvider) Option { return func(o *Options) { diff --git a/services/storage-users/pkg/config/config.go b/services/storage-users/pkg/config/config.go index c3bfa18c90e..09526b90b4b 100644 --- a/services/storage-users/pkg/config/config.go +++ b/services/storage-users/pkg/config/config.go @@ -45,6 +45,8 @@ type Config struct { MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;STORAGE_USERS_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services." introductionVersion:"5.0"` CliMaxAttemptsRenameFile int `yaml:"max_attempts_rename_file" env:"STORAGE_USERS_CLI_MAX_ATTEMPTS_RENAME_FILE" desc:"The maximum number of attempts to rename a file when a user restores a file to an existing destination with the same name. The minimum value is 100." introductionVersion:"5.0"` + EnableVaultMode bool `yaml:"enable_vault_mode" env:"STORAGE_USERS_ENABLE_VAULT_MODE" desc:"Enable vault mode for the storage-users service runned in addition to the regular storage-users service by owerrwiting the MountID to VaultStorageProviderID. Required the running the storage-users-vault additional service." introductionVersion:"daledda"` + Context context.Context `yaml:"-"` } @@ -215,6 +217,7 @@ type Events struct { TLSRootCaCertPath string `yaml:"tls_root_ca_cert_path" env:"OCIS_EVENTS_TLS_ROOT_CA_CERTIFICATE;STORAGE_USERS_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided STORAGE_USERS_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"pre5.0"` EnableTLS bool `yaml:"enable_tls" env:"OCIS_EVENTS_ENABLE_TLS;STORAGE_USERS_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"pre5.0"` NumConsumers int `yaml:"num_consumers" env:"STORAGE_USERS_EVENTS_NUM_CONSUMERS" desc:"The amount of concurrent event consumers to start. Event consumers are used for post-processing files. Multiple consumers increase parallelisation, but will also increase CPU and memory demands. The setting has no effect when the OCIS_ASYNC_UPLOADS is set to false. The default and minimum value is 1." introductionVersion:"pre5.0"` + ConsumerGroup string `yaml:"consumer_group" env:"STORAGE_USERS_EVENTS_CONSUMER_GROUP" desc:"The consumer group name to use for the event consumers. The consumer group name is used to identify the consumers." introductionVersion:"daledda"` AuthUsername string `yaml:"username" env:"OCIS_EVENTS_AUTH_USERNAME;STORAGE_USERS_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"5.0"` AuthPassword string `yaml:"password" env:"OCIS_EVENTS_AUTH_PASSWORD;STORAGE_USERS_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"5.0"` } diff --git a/services/storage-users/pkg/config/defaults/defaultconfig.go b/services/storage-users/pkg/config/defaults/defaultconfig.go index d6de665deea..f77ef6372f5 100644 --- a/services/storage-users/pkg/config/defaults/defaultconfig.go +++ b/services/storage-users/pkg/config/defaults/defaultconfig.go @@ -8,6 +8,7 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/shared" "github.com/owncloud/ocis/v2/ocis-pkg/structs" "github.com/owncloud/ocis/v2/services/storage-users/pkg/config" + "github.com/owncloud/reva/v2/pkg/utils" ) // FullDefaultConfig returns a fully initialized default configuration @@ -226,6 +227,11 @@ func EnsureDefaults(cfg *config.Config) { cfg.HTTP.CORS.AllowedOrigins[0] == "https://localhost:9200") { cfg.HTTP.CORS.AllowedOrigins = []string{cfg.Commons.OcisURL} } + + // set mount id to vault storage provider id + if cfg.EnableVaultMode { + cfg.MountID = utils.VaultStorageProviderID + } } // Sanitize sanitized the configuration diff --git a/services/storage-users/pkg/revaconfig/drivers.go b/services/storage-users/pkg/revaconfig/drivers.go index 311e40c7591..66c932139be 100644 --- a/services/storage-users/pkg/revaconfig/drivers.go +++ b/services/storage-users/pkg/revaconfig/drivers.go @@ -157,6 +157,7 @@ func OwnCloudSQL(cfg *config.Config) map[string]interface{} { // Ocis is the config mapping for the Ocis storage driver func Ocis(cfg *config.Config) map[string]interface{} { return map[string]interface{}{ + "mount_id": cfg.MountID, "metadata_backend": "messagepack", "propagator": cfg.Drivers.OCIS.Propagator, "async_propagator_options": map[string]interface{}{ @@ -198,7 +199,8 @@ func Ocis(cfg *config.Config) map[string]interface{} { "cache_auth_password": cfg.IDCache.AuthPassword, }, "events": map[string]interface{}{ - "numconsumers": cfg.Events.NumConsumers, + "numconsumers": cfg.Events.NumConsumers, + "consumer_group": cfg.Events.ConsumerGroup, }, "tokens": map[string]interface{}{ "transfer_shared_secret": cfg.Commons.TransferSecret, @@ -321,7 +323,8 @@ func S3NG(cfg *config.Config) map[string]interface{} { "cache_auth_password": cfg.IDCache.AuthPassword, }, "events": map[string]interface{}{ - "numconsumers": cfg.Events.NumConsumers, + "numconsumers": cfg.Events.NumConsumers, + "consumer_group": cfg.Events.ConsumerGroup, }, "tokens": map[string]interface{}{ "transfer_shared_secret": cfg.Commons.TransferSecret, diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/services/gateway/storageprovider.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/services/gateway/storageprovider.go index 4a8d15df4f4..c45f26e3061 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/services/gateway/storageprovider.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/services/gateway/storageprovider.go @@ -143,6 +143,12 @@ func (s *svc) CreateHome(ctx context.Context, req *provider.CreateHomeRequest) ( }, } } + + // pass storage_id to the storage provider to handle vault storage id + if storageId := utils.ReadPlainFromOpaque(req.GetOpaque(), "storage_id"); storageId != "" { + createReq.Opaque = utils.AppendPlainToOpaque(createReq.Opaque, "storage_id", storageId) + } + res, err := s.CreateStorageSpace(ctx, createReq) if err != nil { return &provider.CreateHomeResponse{ @@ -170,6 +176,11 @@ func (s *svc) CreateStorageSpace(ctx context.Context, req *provider.CreateStorag } } + if storageId := utils.ReadPlainFromOpaque(req.GetOpaque(), "storage_id"); storageId != "" { + space.Root = &provider.ResourceId{StorageId: storageId} + req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "storage_id", storageId) + } + srClient, err := s.getStorageRegistryClient(ctx, s.c.StorageRegistryEndpoint) if err != nil { return &provider.CreateStorageSpaceResponse{ @@ -247,6 +258,7 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp filters["path"] = path } + hasFileIdFilter := false for _, f := range req.Filters { switch f.Type { case provider.ListStorageSpacesRequest_Filter_TYPE_ID: @@ -255,6 +267,7 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp continue } filters["storage_id"], filters["space_id"], filters["opaque_id"] = sid, spid, oid + hasFileIdFilter = true case provider.ListStorageSpacesRequest_Filter_TYPE_OWNER: filters["owner_idp"] = f.GetOwner().GetIdp() filters["owner_id"] = f.GetOwner().GetOpaqueId() @@ -270,6 +283,10 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp } } + if !hasFileIdFilter && utils.ReadPlainFromOpaque(req.Opaque, "storage_id") != "" { + filters["storage_id"] = utils.ReadPlainFromOpaque(req.Opaque, "storage_id") + } + c, err := s.getStorageRegistryClient(ctx, s.c.StorageRegistryEndpoint) if err != nil { return &provider.ListStorageSpacesResponse{ diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/services/gateway/storageprovidercache.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/services/gateway/storageprovidercache.go index ba76690e235..855a7832cfc 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/services/gateway/storageprovidercache.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/services/gateway/storageprovidercache.go @@ -24,8 +24,8 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" - sdk "github.com/owncloud/reva/v2/pkg/sdk/common" "github.com/owncloud/reva/v2/pkg/storage/cache" + "github.com/owncloud/reva/v2/pkg/storagespace" "github.com/owncloud/reva/v2/pkg/utils" "github.com/pkg/errors" "google.golang.org/grpc" @@ -41,15 +41,22 @@ type cachedRegistryClient struct { } func (c *cachedRegistryClient) ListStorageProviders(ctx context.Context, in *registry.ListStorageProvidersRequest, opts ...grpc.CallOption) (*registry.ListStorageProvidersResponse, error) { - - spaceID := sdk.DecodeOpaqueMap(in.Opaque)["space_id"] + spaceID := utils.ReadPlainFromOpaque(in.GetOpaque(), "space_id") + resourceID := spaceID + if storageID := utils.ReadPlainFromOpaque(in.GetOpaque(), "storage_id"); storageID != "" { + if spaceID != "" { + resourceID = storagespace.FormatStorageID(storageID, spaceID) + } else { + resourceID = storageID + } + } u, ok := ctxpkg.ContextGetUser(ctx) if !ok { return nil, errors.New("user not found in context") } - key := c.cache.GetKey(u.GetId(), spaceID) + key := c.cache.GetKey(u.GetId(), resourceID) if key != "" { s := ®istry.ListStorageProvidersResponse{} if err := c.cache.PullFromCache(key, s); err == nil { diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/services/storageprovider/storageprovider.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/services/storageprovider/storageprovider.go index d790bf2c1d5..2bf7025e9b8 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/services/storageprovider/storageprovider.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/services/storageprovider/storageprovider.go @@ -33,6 +33,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/mitchellh/mapstructure" "github.com/owncloud/reva/v2/pkg/appctx" "github.com/owncloud/reva/v2/pkg/conversions" ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" @@ -47,7 +48,6 @@ import ( "github.com/owncloud/reva/v2/pkg/storage/fs/registry" "github.com/owncloud/reva/v2/pkg/storagespace" "github.com/owncloud/reva/v2/pkg/utils" - "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/rs/zerolog" "go.opentelemetry.io/otel/attribute" @@ -787,6 +787,7 @@ func (s *Service) Stat(ctx context.Context, req *provider.StatRequest) (*provide s.addMissingStorageProviderID(md.GetId(), nil) s.addMissingStorageProviderID(md.GetParentId(), nil) s.addMissingStorageProviderID(md.GetSpace().GetRoot(), nil) + s.addMissingStorageProviderID(md.GetSpace().GetRootInfo().GetId(), nil) return &provider.StatResponse{ Status: status.NewOK(ctx), diff --git a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/copy.go b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/copy.go index 307bed917f2..737e010e2cb 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/copy.go +++ b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/copy.go @@ -553,6 +553,13 @@ func (s *svc) executeSpacesCopy(ctx context.Context, w http.ResponseWriter, sele } func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, srcRef, dstRef *provider.Reference, log *zerolog.Logger, destInShareJail bool) *copy { + // restict copy from the vault + if destinationIsNotAllowed(srcRef, dstRef) { + w.WriteHeader(http.StatusConflict) + b, err := errors.Marshal(http.StatusBadRequest, "destination is not allowed", "", "") + errors.HandleWebdavError(log, w, b, err) + return nil + } isChild, err := s.referenceIsChildOf(ctx, s.gatewaySelector, dstRef, srcRef) if err != nil { switch err.(type) { diff --git a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/move.go b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/move.go index 65192cfa3a6..f9a0e30999b 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/move.go +++ b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/move.go @@ -141,6 +141,13 @@ func (s *svc) handleSpacesMove(w http.ResponseWriter, r *http.Request, srcSpaceI } func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Request, src, dst *provider.Reference, log zerolog.Logger) { + // restrict move from the vault + if destinationIsNotAllowed(src, dst) { + w.WriteHeader(http.StatusConflict) + b, err := errors.Marshal(http.StatusBadRequest, "destination is not allowed", "", "") + errors.HandleWebdavError(&log, w, b, err) + return + } isChild, err := s.referenceIsChildOf(ctx, s.gatewaySelector, dst, src) if err != nil { switch err.(type) { diff --git a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/ocdav.go b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/ocdav.go index fee2dd86b01..850cccbf75c 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/ocdav.go +++ b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/ocdav.go @@ -359,6 +359,10 @@ func (s *svc) sspReferenceIsChildOf(ctx context.Context, selector pool.Selectabl } func (s *svc) referenceIsChildOf(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], child, parent *provider.Reference) (bool, error) { + if child.ResourceId.StorageId != parent.ResourceId.StorageId { + return false, nil // Not on the same storage -> not a child + } + if child.ResourceId.SpaceId != parent.ResourceId.SpaceId { return false, nil // Not on the same storage -> not a child } @@ -414,3 +418,11 @@ func isBodyEmpty(r *http.Request) bool { } return true } + +func destinationIsNotAllowed(srcRef, dstRef *provider.Reference) bool { + if srcRef.GetResourceId().GetStorageId() == utils.VaultStorageProviderID && + dstRef.GetResourceId().GetStorageId() != utils.VaultStorageProviderID { + return true + } + return false +} diff --git a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/spaces.go b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/spaces.go index 41c92b936c6..12093c61230 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/spaces.go +++ b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/spaces.go @@ -137,8 +137,7 @@ func (h *Handler) addSpaceMember(w http.ResponseWriter, r *http.Request, info *p response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider", err) return } - - providerClient, err := h.getStorageProviderClient(p) + providerClient, err := pool.GetStorageProviderServiceClient(p.Address) if err != nil { response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider client", err) return @@ -244,8 +243,7 @@ func (h *Handler) removeSpaceMember(w http.ResponseWriter, r *http.Request, spac if ref.ResourceId.OpaqueId == "" { ref.ResourceId.OpaqueId = ref.ResourceId.SpaceId } - - providerClient, err := h.getStorageProviderClient(prov) + providerClient, err := pool.GetStorageProviderServiceClient(prov.Address) if err != nil { response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider client", err) return @@ -290,16 +288,6 @@ func (h *Handler) removeSpaceMember(w http.ResponseWriter, r *http.Request, spac response.WriteOCSSuccess(w, r, nil) } -func (h *Handler) getStorageProviderClient(p *registry.ProviderInfo) (provider.ProviderAPIClient, error) { - c, err := pool.GetStorageProviderServiceClient(p.Address) - if err != nil { - err = errors.Wrap(err, "shares spaces: error getting a storage provider client") - return nil, err - } - - return c, nil -} - func (h *Handler) findProvider(ctx context.Context, ref *provider.Reference) (*registry.ProviderInfo, error) { c, err := pool.GetStorageRegistryClient(h.storageRegistryAddr) if err != nil { diff --git a/vendor/github.com/owncloud/reva/v2/pkg/events/postprocessing.go b/vendor/github.com/owncloud/reva/v2/pkg/events/postprocessing.go index f4268920a3d..64318cb9487 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/events/postprocessing.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/events/postprocessing.go @@ -103,6 +103,7 @@ type PostprocessingStepFinished struct { UploadID string ExecutingUser *user.User Filename string + ResourceID *provider.ResourceId FinishedStep Postprocessingstep // name of the step Result interface{} // result information see VirusscanResult for example @@ -145,6 +146,7 @@ type VirusscanResult struct { type PostprocessingFinished struct { UploadID string Filename string + ResourceID *provider.ResourceId SpaceOwner *user.UserId ExecutingUser *user.User Result map[Postprocessingstep]interface{} // it is a map[step]Event diff --git a/vendor/github.com/owncloud/reva/v2/pkg/storage/registry/spaces/spaces.go b/vendor/github.com/owncloud/reva/v2/pkg/storage/registry/spaces/spaces.go index ac586e96d39..d2b0cbc7fb7 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/storage/registry/spaces/spaces.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/storage/registry/spaces/spaces.go @@ -34,6 +34,7 @@ import ( providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" registrypb "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/mitchellh/mapstructure" "github.com/owncloud/reva/v2/pkg/appctx" ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" "github.com/owncloud/reva/v2/pkg/errtypes" @@ -44,7 +45,6 @@ import ( pkgregistry "github.com/owncloud/reva/v2/pkg/storage/registry/registry" "github.com/owncloud/reva/v2/pkg/storagespace" "github.com/owncloud/reva/v2/pkg/utils" - "github.com/mitchellh/mapstructure" "google.golang.org/grpc" ) @@ -195,6 +195,18 @@ func (r *registry) GetProvider(ctx context.Context, space *providerpb.StorageSpa if space.SpaceType != "" && spaceType != space.SpaceType { continue } + + // Filter out vault spaces if no storageId is provided + if space.GetRoot().GetStorageId() != "" { + if space.GetRoot().GetStorageId() != provider.ProviderID { + continue + } + } else { + if strings.HasPrefix(sc.MountPoint, "/vault/") { + continue + } + } + if space.Owner != nil { user := ctxpkg.ContextMustGetUser(ctx) spacePath, err = sc.SpacePath(user, space) @@ -289,7 +301,7 @@ func (r *registry) ListProviders(ctx context.Context, filters map[string]string) // return all providers return r.findAllProviders(ctx, mask), nil default: - return r.findProvidersForFilter(ctx, r.buildFilters(filters), unrestricted, mask), nil + return r.findProvidersForFilter(ctx, r.buildFilters(filters), filters["storage_id"], unrestricted, mask), nil } } @@ -340,7 +352,7 @@ func (r *registry) buildFilters(filterMap map[string]string) []*providerpb.ListS return filters } -func (r *registry) findProvidersForFilter(ctx context.Context, filters []*providerpb.ListStorageSpacesRequest_Filter, unrestricted bool, _ string) []*registrypb.ProviderInfo { +func (r *registry) findProvidersForFilter(ctx context.Context, filters []*providerpb.ListStorageSpacesRequest_Filter, storageId string, unrestricted bool, _ string) []*registrypb.ProviderInfo { var requestedSpaceType string for _, f := range filters { @@ -352,7 +364,10 @@ func (r *registry) findProvidersForFilter(ctx context.Context, filters []*provid currentUser := ctxpkg.ContextMustGetUser(ctx) providerInfos := []*registrypb.ProviderInfo{} for address, provider := range r.c.Providers { - + // skip mismatching storageproviders + if storageId != "" && storageId != provider.ProviderID { + continue + } // when a specific space type is requested we may skip this provider altogether if it is not configured for that type // we have to ignore a space type filter with +grant or +mountpoint type because they can live on any provider if requestedSpaceType != "" && !strings.HasPrefix(requestedSpaceType, "+") { @@ -385,6 +400,10 @@ func (r *registry) findProvidersForFilter(ctx context.Context, filters []*provid if sc, ok = provider.Spaces[space.SpaceType]; !ok { continue } + // Filter out vault spaces if no storageId is provided + if storageId == "" && strings.HasPrefix(sc.MountPoint, "/vault/") { + continue + } spacePath, err = sc.SpacePath(currentUser, space) if err != nil { appctx.GetLogger(ctx).Error().Err(err).Interface("provider", provider).Interface("space", space).Msg("failed to execute template, continuing") diff --git a/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go b/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go index 79dcc454a76..c4c4fd1e08f 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -258,7 +258,7 @@ func New(o *options.Options, aspects aspects.Aspects, log *zerolog.Logger) (stor return nil, errors.New("need nats for async file processing") } - ch, err := events.Consume(fs.stream, "dcfs", _registeredEvents...) + ch, err := events.Consume(fs.stream, o.Events.ConsumerGroup, _registeredEvents...) if err != nil { return nil, err } @@ -285,6 +285,10 @@ func (fs *Decomposedfs) Postprocessing(ch <-chan events.Event) { switch ev := event.Event.(type) { case events.PostprocessingFinished: sublog := log.With().Str("event", "PostprocessingFinished").Str("uploadid", ev.UploadID).Logger() + if ev.ResourceID != nil && ev.ResourceID.GetStorageId() != "" && ev.ResourceID.GetStorageId() != fs.o.MountID { + sublog.Debug().Msg("ignoring event for different storage") + continue + } session, err := fs.sessionStore.Get(ctx, ev.UploadID) if err != nil { sublog.Error().Err(err).Msg("Failed to get upload") @@ -450,6 +454,10 @@ func (fs *Decomposedfs) Postprocessing(ch <-chan events.Event) { session.Cleanup(true, !ev.KeepUpload, !ev.KeepUpload, true) case events.RevertRevision: sublog := log.With().Str("event", "RevertRevision").Interface("nodeid", ev.ResourceID).Logger() + if ev.ResourceID != nil && ev.ResourceID.GetStorageId() != "" && ev.ResourceID.GetStorageId() != fs.o.MountID { + sublog.Debug().Msg("ignoring event for different storage") + continue + } n, err := fs.lu.NodeFromID(ctx, ev.ResourceID) if err != nil { sublog.Error().Err(err).Msg("Failed to get node") @@ -462,6 +470,10 @@ func (fs *Decomposedfs) Postprocessing(ch <-chan events.Event) { } case events.PostprocessingStepFinished: sublog := log.With().Str("event", "PostprocessingStepFinished").Str("uploadid", ev.UploadID).Logger() + if ev.ResourceID != nil && ev.ResourceID.GetStorageId() != "" && ev.ResourceID.GetStorageId() != fs.o.MountID { + sublog.Debug().Msg("ignoring event for different storage") + continue + } if ev.FinishedStep != events.PPStepAntivirus { // atm we are only interested in antivirus results continue diff --git a/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/options/options.go b/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/options/options.go index 5c76a383eac..210f2068130 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/options/options.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/options/options.go @@ -23,10 +23,10 @@ import ( "strings" "time" + "github.com/mitchellh/mapstructure" "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" "github.com/owncloud/reva/v2/pkg/sharedconf" "github.com/owncloud/reva/v2/pkg/storage/cache" - "github.com/mitchellh/mapstructure" "github.com/pkg/errors" ) @@ -103,7 +103,8 @@ type AsyncPropagatorOptions struct { // EventOptions are the configurable options for events type EventOptions struct { - NumConsumers int `mapstructure:"numconsumers"` + NumConsumers int `mapstructure:"numconsumers"` + ConsumerGroup string `mapstructure:"consumer_group"` } // TokenOptions are the configurable option for tokens @@ -172,5 +173,9 @@ func New(m map[string]interface{}) (*Options, error) { o.UploadDirectory = filepath.Join(o.Root, "uploads") } + if o.Events.ConsumerGroup == "" { + o.Events.ConsumerGroup = "dcfs" + } + return o, nil } diff --git a/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go b/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go index 97b506d855c..6e91c8a409f 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go @@ -224,7 +224,7 @@ func (session *OcisSession) FinishUploadDecomposed(ctx context.Context) error { URL: s, SpaceOwner: n.SpaceOwnerOrManager(session.Context(ctx)), ExecutingUser: u, - ResourceID: &provider.ResourceId{SpaceId: n.SpaceID, OpaqueId: n.ID}, + ResourceID: &provider.ResourceId{StorageId: session.ProviderID(), SpaceId: n.SpaceID, OpaqueId: n.ID}, Filename: session.Filename(), Filesize: uint64(session.Size()), ImpersonatingUser: iu, diff --git a/vendor/github.com/owncloud/reva/v2/pkg/utils/utils.go b/vendor/github.com/owncloud/reva/v2/pkg/utils/utils.go index c1031368743..c562636e8b0 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/utils/utils.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/utils/utils.go @@ -64,6 +64,9 @@ var ( // OCMStorageSpaceID is the space id used by the ocmreceived storageprovider OCMStorageSpaceID = "89f37a33-858b-45fa-8890-a1f2b27d90e1" + // VaultStorageProviderID is the storage id used by the vault storageprovider + VaultStorageProviderID = "1a01c2c4-4309-4483-a845-842fd56d8622" + // SpaceGrant is used to signal the storageprovider that the grant is on a space SpaceGrant struct{} ) diff --git a/vendor/modules.txt b/vendor/modules.txt index c103ca6b257..6714113ca59 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1316,7 +1316,7 @@ github.com/orcaman/concurrent-map # github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 ## explicit; go 1.18 github.com/owncloud/libre-graph-api-go -# github.com/owncloud/reva/v2 v2.0.0-20260324082555-823c2f1c2593 +# github.com/owncloud/reva/v2 v2.0.0-20260408163105-01eba1425b63 ## explicit; go 1.24.0 github.com/owncloud/reva/v2/cmd/revad/internal/grace github.com/owncloud/reva/v2/cmd/revad/runtime From 14cdcf32559072379c7f3954202b2872e492d11b Mon Sep 17 00:00:00 2001 From: Roman Perekhod <2403905@gmail.com> Date: Tue, 14 Apr 2026 18:34:35 +0200 Subject: [PATCH 02/12] feat: provide the mfa to webdav and grpc --- go.mod | 2 +- go.sum | 4 +- services/graph/pkg/middleware/auth.go | 11 ++ services/proxy/pkg/command/server.go | 2 +- services/proxy/pkg/middleware/create_home.go | 19 +++- services/proxy/pkg/middleware/mfa.go | 69 +++++++++++- services/proxy/pkg/middleware/options.go | 11 ++ .../storage-users/pkg/revaconfig/config.go | 10 +- .../thumbnails/pkg/service/grpc/v0/service.go | 27 ++++- .../thumbnails/pkg/thumbnail/imgsource/cs3.go | 16 +++ services/webdav/pkg/service/v0/service.go | 23 +++- .../internal/grpc/interceptors/auth/auth.go | 22 +++- .../grpc/interceptors/loader/loader.go | 1 + .../v2/internal/grpc/interceptors/mfa/mfa.go | 102 ++++++++++++++++++ .../internal/grpc/interceptors/token/token.go | 14 +++ .../internal/http/interceptors/auth/auth.go | 9 ++ .../http/services/archiver/handler.go | 10 +- .../reva/v2/pkg/auth/manager/oidc/oidc.go | 14 ++- .../owncloud/reva/v2/pkg/ctx/mfactx.go | 21 ++++ .../owncloud/reva/v2/pkg/ctx/userctx.go | 1 + vendor/modules.txt | 3 +- 21 files changed, 370 insertions(+), 21 deletions(-) create mode 100644 vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa/mfa.go create mode 100644 vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go diff --git a/go.mod b/go.mod index ba108aff485..e4e3da3e215 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/open-policy-agent/opa v1.12.3 github.com/orcaman/concurrent-map v1.0.0 github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 - github.com/owncloud/reva/v2 v2.0.0-20260408163105-01eba1425b63 + github.com/owncloud/reva/v2 v2.0.0-20260414163338-be87e5bbeba9 github.com/pkg/errors v0.9.1 github.com/pkg/xattr v0.4.12 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index 8f4fd7b37e2..16c877de43c 100644 --- a/go.sum +++ b/go.sum @@ -742,8 +742,8 @@ github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HD github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 h1:JRidLTAKhnvyLMRtVtSF4lhBa0NSAOs6fof+d6JnKII= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245/go.mod h1:z61VMGAJRtR1nbgXWiNoCkxUXP1B3Je9rMuJbnGd+Og= -github.com/owncloud/reva/v2 v2.0.0-20260408163105-01eba1425b63 h1:t02rNoa8vXX8Yu461uX8uR/ATOxx+KqUB4LebgqAKic= -github.com/owncloud/reva/v2 v2.0.0-20260408163105-01eba1425b63/go.mod h1:+rCy6oGYb2/qs5gmQa8y/pHARw634vB73MZGDY2SBIQ= +github.com/owncloud/reva/v2 v2.0.0-20260414163338-be87e5bbeba9 h1:lvQllNwB5pEUybvXGD1ehQWndIl6M4Wd3c7oCIOVXLc= +github.com/owncloud/reva/v2 v2.0.0-20260414163338-be87e5bbeba9/go.mod h1:+rCy6oGYb2/qs5gmQa8y/pHARw634vB73MZGDY2SBIQ= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pablodz/inotifywaitgo v0.0.9 h1:njquRbBU7fuwIe5rEvtaniVBjwWzcpdUVptSgzFqZsw= diff --git a/services/graph/pkg/middleware/auth.go b/services/graph/pkg/middleware/auth.go index 3e382ec8d17..2b3fb87d2ab 100644 --- a/services/graph/pkg/middleware/auth.go +++ b/services/graph/pkg/middleware/auth.go @@ -93,6 +93,17 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler { ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorID) } + // Propagate MFA status to outgoing gRPC metadata so that services + // protected by the mfa interceptor (e.g. storage-users-vault) + // can enforce MFA at the gRPC layer. + hasMFA := mfa.Has(ctx) + ctx = ctxpkg.ContextSetMFA(ctx, hasMFA) + mfaVal := "false" + if hasMFA { + mfaVal = "true" + } + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAHeader, mfaVal) + next.ServeHTTP(w, r.WithContext(ctx)) }) } diff --git a/services/proxy/pkg/command/server.go b/services/proxy/pkg/command/server.go index 348f10a87f8..f43a6c38fee 100644 --- a/services/proxy/pkg/command/server.go +++ b/services/proxy/pkg/command/server.go @@ -357,7 +357,7 @@ func loadMiddlewares(logger log.Logger, cfg *config.Config, middleware.EventsPublisher(publisher), middleware.MultiInstance(cfg.MultiInstance.Enabled, cfg.MultiInstance.InstanceID, cfg.MultiInstance.MemberClaim, cfg.MultiInstance.GuestClaim, cfg.MultiInstance.GuestRole), ), - middleware.MultiFactor(cfg.MultiFactorAuthentication, middleware.Logger(logger)), + middleware.MultiFactor(cfg.MultiFactorAuthentication, middleware.Logger(logger), middleware.MFAStore(signingKeyStore)), middleware.SelectorCookie( middleware.Logger(logger), middleware.PolicySelectorConfig(*cfg.PolicySelector), diff --git a/services/proxy/pkg/middleware/create_home.go b/services/proxy/pkg/middleware/create_home.go index 670cf6877ce..704d12e5dba 100644 --- a/services/proxy/pkg/middleware/create_home.go +++ b/services/proxy/pkg/middleware/create_home.go @@ -11,10 +11,12 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" + ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" revactx "github.com/owncloud/reva/v2/pkg/ctx" "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" "github.com/owncloud/reva/v2/pkg/storagespace" "github.com/owncloud/reva/v2/pkg/utils" + gmmetadata "go-micro.dev/v4/metadata" "google.golang.org/grpc/metadata" ) @@ -108,12 +110,27 @@ func (m *createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { } if m.createVaultHome { + // TODO there is no MFA context + if md, ok := gmmetadata.FromContext(ctx); ok { + if v, ok := md.Get(revactx.MFAHeader); ok && v != "" { + ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAHeader, v) + } + } else if md, ok := metadata.FromIncomingContext(ctx); ok { + if vals := md.Get(revactx.MFAHeader); len(vals) > 0 && vals[0] != "" { + ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAHeader, vals[0]) + } + } + // TODO Can we avoid to force MFA and get it from the context? + vctx := ctxpkg.ContextSetMFA(ctx, true) + vctx = metadata.AppendToOutgoingContext(vctx, ctxpkg.MFAHeader, "true") + vaultKey := storagespace.FormatStorageID(utils.VaultStorageProviderID, u.GetId().GetOpaqueId()) if !m.cache.Has(vaultKey) { // Create vault personal space // Inject storage_id into opaque for vault personal space createHomeReq.Opaque = utils.AppendPlainToOpaque(createHomeReq.Opaque, "storage_id", utils.VaultStorageProviderID) - cpsRes, err := client.CreateHome(ctx, createHomeReq) + + cpsRes, err := client.CreateHome(vctx, createHomeReq) switch { case err != nil: m.logger.Err(err).Msg("error calling CreateHome for vault personal") diff --git a/services/proxy/pkg/middleware/mfa.go b/services/proxy/pkg/middleware/mfa.go index f41c0b97019..15e77d5f974 100644 --- a/services/proxy/pkg/middleware/mfa.go +++ b/services/proxy/pkg/middleware/mfa.go @@ -2,6 +2,10 @@ package middleware import ( "net/http" + "time" + + revactx "github.com/owncloud/reva/v2/pkg/ctx" + microstore "go-micro.dev/v4/store" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/ocis-pkg/mfa" @@ -9,6 +13,11 @@ import ( "github.com/owncloud/ocis/v2/services/proxy/pkg/config" ) +// mfaStoreTTL is how long a verified MFA status is remembered for non-OIDC +// requests (e.g. signed-URL archiver downloads). It should be at least as +// long as the signed-URL expiry (OC-Expires). Default: 1 hour. +const mfaStoreTTL = time.Hour + // MultiFactor returns a middleware that checks requests for mfa func MultiFactor(cfg config.MFAConfig, opts ...Option) func(next http.Handler) http.Handler { options := newOptions(opts...) @@ -20,6 +29,7 @@ func MultiFactor(cfg config.MFAConfig, opts ...Option) func(next http.Handler) h logger: logger, enabled: cfg.Enabled, authLevelNames: cfg.AuthLevelNames, + store: options.MFAStore, } } } @@ -30,6 +40,10 @@ type MultiFactorAuthentication struct { logger log.Logger enabled bool authLevelNames []string + // store persists verified MFA status so that non-OIDC requests (e.g. + // signed-URL archiver downloads) can inherit it from the user's most + // recent OIDC session. Nil when no store is configured. + store microstore.Store } // ServeHTTP adds the mfa header if the request contains a valid mfa token @@ -48,11 +62,36 @@ func (m MultiFactorAuthentication) ServeHTTP(w http.ResponseWriter, req *http.Re mfa.SetHeader(req, false) claims := oidc.FromContext(req.Context()) + // TODO: + // Should we set the "acr" to the claims "X-Access-Token" for the "aud":["reva"]? + // Should we get the claims from the "X-Access-Token"? + + if claims == nil { + // No OIDC claims — request was authenticated via a non-OIDC method + // (e.g. signed URL, basic auth, app token). MFA cannot be determined + // from claims directly. + // + // Fall back to the persisted MFA status from the user's most recent + // OIDC-authenticated session. This allows, for example, a signed-URL + // archiver download to succeed when the user has recently proven MFA + // in their browser session. + if m.store != nil { + if u, ok := revactx.ContextGetUser(req.Context()); ok && u.GetId().GetOpaqueId() != "" { + if m.readMFAFromStore(u.GetId().GetOpaqueId()) { + mfa.SetHeader(req, true) + m.logger.Debug().Str("path", req.URL.Path).Msg("MFA status restored from store for non-OIDC request") + return + } + } + } + m.logger.Debug().Str("path", req.URL.Path).Msg("no OIDC claims in context, skipping MFA check") + return + } // acr is a standard OIDC claim. value, err := oidc.ReadStringClaim("acr", claims) if err != nil { - m.logger.Error().Str("path", req.URL.Path).Interface("required", m.authLevelNames).Err(err).Interface("claims", claims).Msg("no acr claim found in access token") + m.logger.Debug().Str("path", req.URL.Path).Interface("required", m.authLevelNames).Err(err).Msg("acr claim not set in access token") return } @@ -63,6 +102,34 @@ func (m MultiFactorAuthentication) ServeHTTP(w http.ResponseWriter, req *http.Re mfa.SetHeader(req, true) m.logger.Debug().Str("acr", value).Str("url", req.URL.Path).Msg("mfa authenticated") + + // Persist the verified MFA status so that subsequent non-OIDC requests + // (e.g. signed-URL archiver downloads) can inherit it. The entry is + // refreshed on every successful OIDC MFA verification and expires after + // mfaStoreTTL if no further OIDC requests are made. + if m.store != nil { + if u, ok := revactx.ContextGetUser(req.Context()); ok && u.GetId().GetOpaqueId() != "" { + m.writeMFAToStore(u.GetId().GetOpaqueId()) + } + } +} + +func (m MultiFactorAuthentication) readMFAFromStore(userID string) bool { + records, err := m.store.Read("mfa:" + userID) + if err != nil || len(records) == 0 { + return false + } + return string(records[0].Value) == "true" +} + +func (m MultiFactorAuthentication) writeMFAToStore(userID string) { + if err := m.store.Write(µstore.Record{ + Key: "mfa:" + userID, + Value: []byte("true"), + Expiry: mfaStoreTTL, + }); err != nil { + m.logger.Error().Err(err).Str("userID", userID).Msg("failed to write MFA status to store") + } } // containsMFA checks if the given value is in the list of authentication level names diff --git a/services/proxy/pkg/middleware/options.go b/services/proxy/pkg/middleware/options.go index 243d69114c7..b3067060fed 100644 --- a/services/proxy/pkg/middleware/options.go +++ b/services/proxy/pkg/middleware/options.go @@ -59,6 +59,10 @@ type Options struct { DefaultAccessTokenTTL time.Duration // UserInfoCache sets the access token cache store UserInfoCache store.Store + // MFAStore is used to persist verified MFA status so that non-OIDC + // requests (e.g. signed-URL archiver downloads) can inherit the status + // from the user's most recent OIDC-authenticated session. + MFAStore store.Store // CredentialsByUserAgent sets the auth challenges on a per user-agent basis CredentialsByUserAgent map[string]string // AccessTokenVerifyMethod configures how access_tokens should be verified but the oidc_auth middleware. @@ -217,6 +221,13 @@ func UserInfoCache(val store.Store) Option { } } +// MFAStore provides a function to set the MFA session store. +func MFAStore(val store.Store) Option { + return func(o *Options) { + o.MFAStore = val + } +} + // UserProvider sets the accounts user provider func UserProvider(up backend.UserBackend) Option { return func(o *Options) { diff --git a/services/storage-users/pkg/revaconfig/config.go b/services/storage-users/pkg/revaconfig/config.go index 2f47e4c5fda..86551afa167 100644 --- a/services/storage-users/pkg/revaconfig/config.go +++ b/services/storage-users/pkg/revaconfig/config.go @@ -116,11 +116,19 @@ func StorageUsersConfigFromStruct(cfg *config.Config) map[string]interface{} { }, }, } + gcfg := rcfg["grpc"].(map[string]interface{}) if cfg.ReadOnly { - gcfg := rcfg["grpc"].(map[string]interface{}) + // Replace all interceptors with readonly when the storage is read-only. + // eventsmiddleware and prometheus are intentionally dropped in this mode. gcfg["interceptors"] = map[string]interface{}{ "readonly": map[string]interface{}{}, } } + if cfg.EnableVaultMode { + // Add the mfa interceptor so that all gRPC calls to this vault + // storage-users instance require MFA authentication. + interceptors := gcfg["interceptors"].(map[string]interface{}) + interceptors["mfa"] = map[string]interface{}{} + } return rcfg } diff --git a/services/thumbnails/pkg/service/grpc/v0/service.go b/services/thumbnails/pkg/service/grpc/v0/service.go index 44e16a9a935..8bd243c7191 100644 --- a/services/thumbnails/pkg/service/grpc/v0/service.go +++ b/services/thumbnails/pkg/service/grpc/v0/service.go @@ -18,6 +18,7 @@ import ( "github.com/owncloud/reva/v2/pkg/utils" "github.com/pkg/errors" merrors "go-micro.dev/v4/errors" + gmmetadata "go-micro.dev/v4/metadata" "google.golang.org/grpc/metadata" "github.com/owncloud/ocis/v2/ocis-pkg/log" @@ -140,7 +141,7 @@ func (g Thumbnail) checkThumbnail(req *thumbnailssvc.GetThumbnailRequest, sRes * func (g Thumbnail) handleCS3Source(ctx context.Context, req *thumbnailssvc.GetThumbnailRequest) (string, error) { src := req.GetCs3Source() - sRes, err := g.stat(src.GetPath(), src.GetAuthorization()) + sRes, err := g.stat(ctx, src.GetPath(), src.GetAuthorization()) if err != nil { return "", err } @@ -223,7 +224,7 @@ func (g Thumbnail) handleWebdavSource(ctx context.Context, req *thumbnailssvc.Ge auth = src.GetRevaAuthorization() statPath = req.GetFilepath() } - sRes, err := g.stat(statPath, auth) + sRes, err := g.stat(ctx, statPath, auth) if err != nil { return "", err } @@ -272,8 +273,24 @@ func (g Thumbnail) handleWebdavSource(ctx context.Context, req *thumbnailssvc.Ge return key, err } -func (g Thumbnail) stat(path, auth string) (*provider.StatResponse, error) { - ctx := metadata.AppendToOutgoingContext(context.Background(), revactx.TokenHeader, auth) +func (g Thumbnail) stat(ctx context.Context, path, auth string) (*provider.StatResponse, error) { + outCtx := metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, auth) + + // Propagate MFA status to the outgoing gRPC call so that vault storage + // (guarded by the mfa interceptor) grants access. + // go-micro callers (e.g. webdav service) send metadata via go-micro's own + // mechanism (Grpc-Metadata- headers), which is read back via + // gmmetadata.FromContext. Standard gRPC incoming metadata is checked as + // fallback for non-go-micro callers. + if md, ok := gmmetadata.FromContext(ctx); ok { + if v, ok := md.Get(revactx.MFAHeader); ok && v != "" { + outCtx = metadata.AppendToOutgoingContext(outCtx, revactx.MFAHeader, v) + } + } else if md, ok := metadata.FromIncomingContext(ctx); ok { + if vals := md.Get(revactx.MFAHeader); len(vals) > 0 && vals[0] != "" { + outCtx = metadata.AppendToOutgoingContext(outCtx, revactx.MFAHeader, vals[0]) + } + } ref, err := storagespace.ParseReference(path) if err != nil { @@ -289,7 +306,7 @@ func (g Thumbnail) stat(path, auth string) (*provider.StatResponse, error) { return nil, merrors.InternalServerError(g.serviceID, "could not select next gateway client: %s", err.Error()) } req := &provider.StatRequest{Ref: &ref} - rsp, err := client.Stat(ctx, req) + rsp, err := client.Stat(outCtx, req) if err != nil { g.logger.Error().Err(err).Str("path", path).Msg("could not stat file") return nil, merrors.InternalServerError(g.serviceID, "could not stat file: %s", err.Error()) diff --git a/services/thumbnails/pkg/thumbnail/imgsource/cs3.go b/services/thumbnails/pkg/thumbnail/imgsource/cs3.go index e399592e4f8..d50c66ea615 100644 --- a/services/thumbnails/pkg/thumbnail/imgsource/cs3.go +++ b/services/thumbnails/pkg/thumbnail/imgsource/cs3.go @@ -18,6 +18,7 @@ import ( "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" "github.com/owncloud/reva/v2/pkg/rhttp" "github.com/owncloud/reva/v2/pkg/storagespace" + gmmetadata "go-micro.dev/v4/metadata" "google.golang.org/grpc/metadata" ) @@ -60,6 +61,21 @@ func (s CS3) Get(ctx context.Context, path string) (io.ReadCloser, error) { } ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, auth) + + // Propagate MFA status to the outgoing gRPC call so that vault storage + // (guarded by the mfa interceptor) grants access. + // go-micro callers send MFA via go-micro metadata; non-go-micro callers + // via standard gRPC incoming metadata. + if md, ok := gmmetadata.FromContext(ctx); ok { + if v, ok := md.Get(revactx.MFAHeader); ok && v != "" { + ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAHeader, v) + } + } else if md, ok := metadata.FromIncomingContext(ctx); ok { + if vals := md.Get(revactx.MFAHeader); len(vals) > 0 && vals[0] != "" { + ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAHeader, vals[0]) + } + } + err = s.checkImageFileSize(ctx, ref) if err != nil { return nil, err diff --git a/services/webdav/pkg/service/v0/service.go b/services/webdav/pkg/service/v0/service.go index 27949babaff..6b6c8215bf3 100644 --- a/services/webdav/pkg/service/v0/service.go +++ b/services/webdav/pkg/service/v0/service.go @@ -21,6 +21,7 @@ import ( "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" "github.com/owncloud/reva/v2/pkg/storage/utils/templates" merrors "go-micro.dev/v4/errors" + gmmetadata "go-micro.dev/v4/metadata" grpcmetadata "google.golang.org/grpc/metadata" "github.com/owncloud/ocis/v2/ocis-pkg/log" @@ -228,7 +229,7 @@ func (g Webdav) SpacesThumbnail(w http.ResponseWriter, r *http.Request) { t := r.Header.Get(revactx.TokenHeader) fullPath := filepath.Join(tr.Identifier, tr.Filepath) - rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{ + rsp, err := g.thumbnailsClient.GetThumbnail(mfaOutgoingCtx(r), &thumbnailssvc.GetThumbnailRequest{ Filepath: strings.TrimLeft(tr.Filepath, "/"), ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")), Width: tr.Width, @@ -326,7 +327,7 @@ func (g Webdav) Thumbnail(w http.ResponseWriter, r *http.Request) { } fullPath := filepath.Join(templates.WithUser(user, g.config.WebdavNamespace), tr.Filepath) - rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{ + rsp, err := g.thumbnailsClient.GetThumbnail(mfaOutgoingCtx(r), &thumbnailssvc.GetThumbnailRequest{ Filepath: strings.TrimLeft(tr.Filepath, "/"), ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")), Width: tr.Width, @@ -376,7 +377,7 @@ func (g Webdav) PublicThumbnail(w http.ResponseWriter, r *http.Request) { return } - rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{ + rsp, err := g.thumbnailsClient.GetThumbnail(mfaOutgoingCtx(r), &thumbnailssvc.GetThumbnailRequest{ Filepath: strings.TrimLeft(tr.Filepath, "/"), ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")), Width: tr.Width, @@ -421,7 +422,7 @@ func (g Webdav) PublicThumbnailHead(w http.ResponseWriter, r *http.Request) { return } - _, err = g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{ + _, err = g.thumbnailsClient.GetThumbnail(mfaOutgoingCtx(r), &thumbnailssvc.GetThumbnailRequest{ Filepath: strings.TrimLeft(tr.Filepath, "/"), ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")), Width: tr.Width, @@ -497,6 +498,20 @@ func (g Webdav) sendThumbnailResponse(rsp *thumbnailssvc.GetThumbnailResponse, w } } +// mfaOutgoingCtx returns a context derived from the HTTP request with the +// MFA status forwarded as go-micro metadata. The thumbnail service is a +// go-micro service: go-micro propagates metadata via its own mechanism +// (sent as Grpc-Metadata- headers), not standard gRPC outgoing metadata. +// The thumbnail service then converts this to standard gRPC outgoing metadata +// when calling the gateway / vault storage. +func mfaOutgoingCtx(r *http.Request) context.Context { + mfaVal := "false" + if r.Header.Get("X-Multi-Factor-Authentication") == "true" { + mfaVal = "true" + } + return gmmetadata.Set(r.Context(), revactx.MFAHeader, mfaVal) +} + func extensionToThumbnailType(ext string) thumbnailsmsg.ThumbnailType { switch strings.ToUpper(ext) { case "GIF": diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go index b3ff6171481..4a2a63d1c0d 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go @@ -27,6 +27,7 @@ import ( authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/mitchellh/mapstructure" "github.com/owncloud/reva/v2/pkg/appctx" "github.com/owncloud/reva/v2/pkg/auth/scope" ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" @@ -36,12 +37,12 @@ import ( "github.com/owncloud/reva/v2/pkg/token" tokenmgr "github.com/owncloud/reva/v2/pkg/token/manager/registry" "github.com/owncloud/reva/v2/pkg/utils" - "github.com/mitchellh/mapstructure" "github.com/pkg/errors" semconv "go.opentelemetry.io/otel/semconv/v1.20.0" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) @@ -154,6 +155,7 @@ func NewUnary(m map[string]interface{}, unprotected []string, tp trace.TracerPro // store user and scopes in context ctx = ctxpkg.ContextSetUser(ctx, u) ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) + ctx = grantMFAForServiceAccount(ctx, u) span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId)) @@ -243,6 +245,7 @@ func NewStream(m map[string]interface{}, unprotected []string, tp trace.TracerPr // store user and scopes in context ctx = ctxpkg.ContextSetUser(ctx, u) ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) + ctx = grantMFAForServiceAccount(ctx, u) wrapped := newWrappedServerStream(ctx, ss) span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId)) @@ -300,6 +303,23 @@ func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token. return u, tokenScope, nil } +// grantMFAForServiceAccount automatically sets MFA=true for service accounts. +// Service accounts are trusted internal processes that never authenticate via OIDC and +// therefore never carry an acr/MFA claim. Granting them implicit MFA allows +// them to access MFA-gated resources such as vault storage without +// compromising the MFA requirement for regular users. +func grantMFAForServiceAccount(ctx context.Context, u *userpb.User) context.Context { + if u.GetId().GetType() != userpb.UserType_USER_TYPE_SERVICE { + return ctx + } + if _, alreadySet := ctxpkg.ContextGetMFA(ctx); alreadySet { + return ctx + } + ctx = ctxpkg.ContextSetMFA(ctx, true) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAHeader, "true") + return ctx +} + func getUserGroups(ctx context.Context, u *userpb.User, client gatewayv1beta1.GatewayAPIClient) ([]string, error) { if groupsIf, err := userGroupsCache.Get(u.Id.OpaqueId); err == nil { log := appctx.GetLogger(ctx) diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/loader/loader.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/loader/loader.go index c5707555d2b..98b94fd5c81 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/loader/loader.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/loader/loader.go @@ -21,6 +21,7 @@ package loader import ( // Load core GRPC services _ "github.com/owncloud/reva/v2/internal/grpc/interceptors/eventsmiddleware" + _ "github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa" _ "github.com/owncloud/reva/v2/internal/grpc/interceptors/prometheus" _ "github.com/owncloud/reva/v2/internal/grpc/interceptors/readonly" // Add your own service here diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa/mfa.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa/mfa.go new file mode 100644 index 00000000000..e43161de060 --- /dev/null +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa/mfa.go @@ -0,0 +1,102 @@ +package mfa + +import ( + "context" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/owncloud/reva/v2/pkg/appctx" + ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" + "github.com/owncloud/reva/v2/pkg/rgrpc" + rstatus "github.com/owncloud/reva/v2/pkg/rgrpc/status" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + grpcstatus "google.golang.org/grpc/status" +) + +const ( + // defaultPriority places mfa before readonly (200) so the MFA check + // runs first and returns a clear PermissionDenied rather than a readonly error. + defaultPriority = 150 +) + +func init() { + rgrpc.RegisterUnaryInterceptor("mfa", NewUnary) +} + +// NewUnary returns a new unary interceptor that requires MFA to be satisfied +// for every gRPC call on the vault storage provider. +// Service accounts (UserType_USER_TYPE_SERVICE) are exempt because they are +// used for internal operations (postprocessing, event handling, etc.) that +// never carry an MFA claim. +func NewUnary(map[string]interface{}) (grpc.UnaryServerInterceptor, int, error) { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + log := appctx.GetLogger(ctx) + + // Bypass for service accounts — they perform internal operations and + // never carry an MFA claim. + if u, ok := ctxpkg.ContextGetUser(ctx); ok { + if u.GetId().GetType() == userpb.UserType_USER_TYPE_SERVICE { + return handler(ctx, req) + } + } + + hasMFA, _ := ctxpkg.ContextGetMFA(ctx) + if hasMFA { + return handler(ctx, req) + } + + log.Warn().Str("method", info.FullMethod).Msg("mfa: access denied, MFA required") + + const msg = "MFA required to access vault storage" + switch req.(type) { + case *provider.StatRequest: + return &provider.StatResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.ListContainerRequest: + return &provider.ListContainerResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.GetPathRequest: + return &provider.GetPathResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.GetQuotaRequest: + return &provider.GetQuotaResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.InitiateFileDownloadRequest: + return &provider.InitiateFileDownloadResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.InitiateFileUploadRequest: + return &provider.InitiateFileUploadResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.CreateContainerRequest: + return &provider.CreateContainerResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.TouchFileRequest: + return &provider.TouchFileResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.DeleteRequest: + return &provider.DeleteResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.MoveRequest: + return &provider.MoveResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.CreateHomeRequest: + return &provider.CreateHomeResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.AddGrantRequest: + return &provider.AddGrantResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.RemoveGrantRequest: + return &provider.RemoveGrantResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.UpdateGrantRequest: + return &provider.UpdateGrantResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.ListGrantsRequest: + return &provider.ListGrantsResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.ListFileVersionsRequest: + return &provider.ListFileVersionsResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.RestoreFileVersionRequest: + return &provider.RestoreFileVersionResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.ListRecycleRequest: + return &provider.ListRecycleResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.RestoreRecycleItemRequest: + return &provider.RestoreRecycleItemResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.PurgeRecycleRequest: + return &provider.PurgeRecycleResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.SetArbitraryMetadataRequest: + return &provider.SetArbitraryMetadataResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.UnsetArbitraryMetadataRequest: + return &provider.UnsetArbitraryMetadataResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + default: + log.Debug().Str("method", info.FullMethod).Msg("mfa: blocking unknown request type") + return nil, grpcstatus.Errorf(codes.PermissionDenied, "mfa: %s: %T", msg, req) + } + }, defaultPriority, nil +} diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/token/token.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/token/token.go index 2dcd4b66639..a00cc87db12 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/token/token.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/token/token.go @@ -47,6 +47,13 @@ func NewUnary() grpc.UnaryServerInterceptor { ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorID) } } + + if val, ok := md[ctxpkg.MFAHeader]; ok { + if len(val) > 0 && val[0] != "" { + ctx = ctxpkg.ContextSetMFA(ctx, val[0] == "true") + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAHeader, val[0]) + } + } } return handler(ctx, req) @@ -77,6 +84,13 @@ func NewStream() grpc.StreamServerInterceptor { ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorID) } } + + if val, ok := md[ctxpkg.MFAHeader]; ok { + if len(val) > 0 && val[0] != "" { + ctx = ctxpkg.ContextSetMFA(ctx, val[0] == "true") + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAHeader, val[0]) + } + } } wrapped := newWrappedServerStream(ctx, ss) diff --git a/vendor/github.com/owncloud/reva/v2/internal/http/interceptors/auth/auth.go b/vendor/github.com/owncloud/reva/v2/internal/http/interceptors/auth/auth.go index f7b494c684d..f7cce842835 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/http/interceptors/auth/auth.go +++ b/vendor/github.com/owncloud/reva/v2/internal/http/interceptors/auth/auth.go @@ -354,6 +354,15 @@ func ctxWithUserInfo(ctx context.Context, r *http.Request, user *userpb.User, to ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, r.UserAgent()) ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorid) ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) + + // Forward MFA status from the proxy's HTTP header to outgoing gRPC metadata. + // The proxy MultiFactor middleware always sets X-Multi-Factor-Authentication: + // "true" when MFA is disabled globally, or the real outcome when enabled. + if mfaVal := r.Header.Get("X-Multi-Factor-Authentication"); mfaVal != "" { + ctx = ctxpkg.ContextSetMFA(ctx, mfaVal == "true") + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAHeader, mfaVal) + } + return ctx } diff --git a/vendor/github.com/owncloud/reva/v2/internal/http/services/archiver/handler.go b/vendor/github.com/owncloud/reva/v2/internal/http/services/archiver/handler.go index c2e2dfdc73a..7cd3d2b31ff 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/http/services/archiver/handler.go +++ b/vendor/github.com/owncloud/reva/v2/internal/http/services/archiver/handler.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "regexp" @@ -204,8 +205,15 @@ func (s *svc) writeHTTPError(rw http.ResponseWriter, err error) { s.log.Error().Msg(err.Error()) switch err.(type) { - case errtypes.NotFound, errtypes.PermissionDenied: + case errtypes.NotFound: rw.WriteHeader(http.StatusNotFound) + case errtypes.PermissionDenied: + if strings.Contains(err.Error(), "MFA required") { + rw.Header().Set("X-Ocis-Mfa-Required", "true") + rw.WriteHeader(http.StatusForbidden) + } else { + rw.WriteHeader(http.StatusNotFound) + } case manager.ErrMaxSize, manager.ErrMaxFileCount: rw.WriteHeader(http.StatusRequestEntityTooLarge) case errtypes.BadRequest: diff --git a/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/oidc/oidc.go b/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/oidc/oidc.go index bbe950dc191..f716e1c86e7 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/oidc/oidc.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/oidc/oidc.go @@ -32,6 +32,8 @@ import ( authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/juliangruber/go-intersect" + "github.com/mitchellh/mapstructure" "github.com/owncloud/reva/v2/pkg/appctx" "github.com/owncloud/reva/v2/pkg/auth" "github.com/owncloud/reva/v2/pkg/auth/manager/registry" @@ -41,8 +43,7 @@ import ( "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" "github.com/owncloud/reva/v2/pkg/rhttp" "github.com/owncloud/reva/v2/pkg/sharedconf" - "github.com/juliangruber/go-intersect" - "github.com/mitchellh/mapstructure" + "github.com/owncloud/reva/v2/pkg/utils" "github.com/pkg/errors" "golang.org/x/oauth2" ) @@ -241,6 +242,15 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) GidNumber: claims[am.c.GIDClaim].(int64), } + // Embed the OIDC acr (Authentication Context Class Reference) claim into + // User.Opaque so it is cryptographically bound to the minted reva JWT. + // Downstream services (e.g. the mfa gRPC interceptor) can use this + // as a secondary MFA proof independent of the X-Multi-Factor-Authentication + // HTTP header propagation path. + if acr, ok := claims["acr"].(string); ok && acr != "" { + u.Opaque = utils.AppendPlainToOpaque(u.Opaque, "acr", acr) + } + var scopes map[string]*authpb.Scope if userID != nil && (userID.Type == user.UserType_USER_TYPE_LIGHTWEIGHT || userID.Type == user.UserType_USER_TYPE_FEDERATED) { scopes, err = scope.AddLightweightAccountScope(authpb.Role_ROLE_OWNER, nil) diff --git a/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go b/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go new file mode 100644 index 00000000000..cb2d4024e68 --- /dev/null +++ b/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go @@ -0,0 +1,21 @@ +package ctx + +import "context" + +// MFAHeader is the gRPC metadata key used to propagate MFA status across +// service boundaries. Lowercased to satisfy gRPC metadata requirements. +// The corresponding HTTP header set by the proxy is "X-Multi-Factor-Authentication". +const MFAHeader = "x-mfa-authenticated" + +// ContextGetMFA returns the MFA status stored in the context, and whether it +// was set at all. A missing value (second return = false) should be treated as +// MFA not satisfied. +func ContextGetMFA(ctx context.Context) (bool, bool) { + v, ok := ctx.Value(mfaKey).(bool) + return v, ok +} + +// ContextSetMFA stores the MFA status in the context. +func ContextSetMFA(ctx context.Context, mfa bool) context.Context { + return context.WithValue(ctx, mfaKey, mfa) +} diff --git a/vendor/github.com/owncloud/reva/v2/pkg/ctx/userctx.go b/vendor/github.com/owncloud/reva/v2/pkg/ctx/userctx.go index bb0c8076757..c32e96910a7 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/ctx/userctx.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/ctx/userctx.go @@ -33,6 +33,7 @@ const ( lockIDKey scopeKey initiatorKey + mfaKey ) // ContextGetUser returns the user if set in the given context. diff --git a/vendor/modules.txt b/vendor/modules.txt index 6714113ca59..4e7d44456dd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1316,7 +1316,7 @@ github.com/orcaman/concurrent-map # github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 ## explicit; go 1.18 github.com/owncloud/libre-graph-api-go -# github.com/owncloud/reva/v2 v2.0.0-20260408163105-01eba1425b63 +# github.com/owncloud/reva/v2 v2.0.0-20260414163338-be87e5bbeba9 ## explicit; go 1.24.0 github.com/owncloud/reva/v2/cmd/revad/internal/grace github.com/owncloud/reva/v2/cmd/revad/runtime @@ -1326,6 +1326,7 @@ github.com/owncloud/reva/v2/internal/grpc/interceptors/eventsmiddleware github.com/owncloud/reva/v2/internal/grpc/interceptors/loader github.com/owncloud/reva/v2/internal/grpc/interceptors/log github.com/owncloud/reva/v2/internal/grpc/interceptors/metadata +github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa github.com/owncloud/reva/v2/internal/grpc/interceptors/prometheus github.com/owncloud/reva/v2/internal/grpc/interceptors/readonly github.com/owncloud/reva/v2/internal/grpc/interceptors/recovery From c06eb078ea4da52bcb21c2c44cd7cf26721dff20 Mon Sep 17 00:00:00 2001 From: Roman Perekhod <2403905@gmail.com> Date: Thu, 16 Apr 2026 11:14:30 +0200 Subject: [PATCH 03/12] feat: refactoring: provide the mfa to webdav and grpc --- go.mod | 2 +- go.sum | 4 +- services/graph/pkg/middleware/auth.go | 6 +- services/proxy/pkg/middleware/create_home.go | 17 +-- .../storage-users/pkg/revaconfig/config.go | 12 ++- .../thumbnails/pkg/service/grpc/v0/service.go | 18 ++-- .../thumbnails/pkg/thumbnail/imgsource/cs3.go | 15 +-- services/webdav/pkg/service/v0/service.go | 5 +- .../internal/grpc/interceptors/auth/auth.go | 24 +++-- .../v2/internal/grpc/interceptors/auth/mfa.go | 65 +++++++++++ .../grpc/interceptors/loader/loader.go | 1 - .../v2/internal/grpc/interceptors/mfa/mfa.go | 102 ------------------ .../internal/grpc/interceptors/token/token.go | 14 --- .../internal/http/interceptors/auth/auth.go | 11 +- .../owncloud/reva/v2/pkg/ctx/mfactx.go | 24 ++--- .../owncloud/reva/v2/pkg/ctx/userctx.go | 1 - vendor/modules.txt | 3 +- 17 files changed, 126 insertions(+), 198 deletions(-) create mode 100644 vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/mfa.go delete mode 100644 vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa/mfa.go diff --git a/go.mod b/go.mod index e4e3da3e215..d373e2978f8 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/open-policy-agent/opa v1.12.3 github.com/orcaman/concurrent-map v1.0.0 github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 - github.com/owncloud/reva/v2 v2.0.0-20260414163338-be87e5bbeba9 + github.com/owncloud/reva/v2 v2.0.0-20260416090636-3f827ba4fe4e github.com/pkg/errors v0.9.1 github.com/pkg/xattr v0.4.12 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index 16c877de43c..e0d0869b847 100644 --- a/go.sum +++ b/go.sum @@ -742,8 +742,8 @@ github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HD github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 h1:JRidLTAKhnvyLMRtVtSF4lhBa0NSAOs6fof+d6JnKII= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245/go.mod h1:z61VMGAJRtR1nbgXWiNoCkxUXP1B3Je9rMuJbnGd+Og= -github.com/owncloud/reva/v2 v2.0.0-20260414163338-be87e5bbeba9 h1:lvQllNwB5pEUybvXGD1ehQWndIl6M4Wd3c7oCIOVXLc= -github.com/owncloud/reva/v2 v2.0.0-20260414163338-be87e5bbeba9/go.mod h1:+rCy6oGYb2/qs5gmQa8y/pHARw634vB73MZGDY2SBIQ= +github.com/owncloud/reva/v2 v2.0.0-20260416090636-3f827ba4fe4e h1:4rPIjsPciB5RLOCGJFjvGuTCNU5nnXkpJpWElUttbXY= +github.com/owncloud/reva/v2 v2.0.0-20260416090636-3f827ba4fe4e/go.mod h1:+rCy6oGYb2/qs5gmQa8y/pHARw634vB73MZGDY2SBIQ= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pablodz/inotifywaitgo v0.0.9 h1:njquRbBU7fuwIe5rEvtaniVBjwWzcpdUVptSgzFqZsw= diff --git a/services/graph/pkg/middleware/auth.go b/services/graph/pkg/middleware/auth.go index 2b3fb87d2ab..e2847a06ec0 100644 --- a/services/graph/pkg/middleware/auth.go +++ b/services/graph/pkg/middleware/auth.go @@ -96,13 +96,11 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler { // Propagate MFA status to outgoing gRPC metadata so that services // protected by the mfa interceptor (e.g. storage-users-vault) // can enforce MFA at the gRPC layer. - hasMFA := mfa.Has(ctx) - ctx = ctxpkg.ContextSetMFA(ctx, hasMFA) mfaVal := "false" - if hasMFA { + if mfa.Has(ctx) { mfaVal = "true" } - ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAHeader, mfaVal) + ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAOutgoingHeader, mfaVal) next.ServeHTTP(w, r.WithContext(ctx)) }) diff --git a/services/proxy/pkg/middleware/create_home.go b/services/proxy/pkg/middleware/create_home.go index 704d12e5dba..621f211d663 100644 --- a/services/proxy/pkg/middleware/create_home.go +++ b/services/proxy/pkg/middleware/create_home.go @@ -16,7 +16,6 @@ import ( "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" "github.com/owncloud/reva/v2/pkg/storagespace" "github.com/owncloud/reva/v2/pkg/utils" - gmmetadata "go-micro.dev/v4/metadata" "google.golang.org/grpc/metadata" ) @@ -110,19 +109,9 @@ func (m *createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { } if m.createVaultHome { - // TODO there is no MFA context - if md, ok := gmmetadata.FromContext(ctx); ok { - if v, ok := md.Get(revactx.MFAHeader); ok && v != "" { - ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAHeader, v) - } - } else if md, ok := metadata.FromIncomingContext(ctx); ok { - if vals := md.Get(revactx.MFAHeader); len(vals) > 0 && vals[0] != "" { - ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAHeader, vals[0]) - } - } - // TODO Can we avoid to force MFA and get it from the context? - vctx := ctxpkg.ContextSetMFA(ctx, true) - vctx = metadata.AppendToOutgoingContext(vctx, ctxpkg.MFAHeader, "true") + // Force MFA=true for vault home creation so provisioning succeeds even + // before the user has made a regular MFA-verified request. + vctx := metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAOutgoingHeader, "true") vaultKey := storagespace.FormatStorageID(utils.VaultStorageProviderID, u.GetId().GetOpaqueId()) if !m.cache.Has(vaultKey) { diff --git a/services/storage-users/pkg/revaconfig/config.go b/services/storage-users/pkg/revaconfig/config.go index 86551afa167..f5f08fff599 100644 --- a/services/storage-users/pkg/revaconfig/config.go +++ b/services/storage-users/pkg/revaconfig/config.go @@ -125,10 +125,16 @@ func StorageUsersConfigFromStruct(cfg *config.Config) map[string]interface{} { } } if cfg.EnableVaultMode { - // Add the mfa interceptor so that all gRPC calls to this vault - // storage-users instance require MFA authentication. + // Set mfa_enabled inside the auth interceptor config so that all gRPC + // calls to this vault storage-users instance require MFA authentication. interceptors := gcfg["interceptors"].(map[string]interface{}) - interceptors["mfa"] = map[string]interface{}{} + if authCfg, ok := interceptors["auth"].(map[string]interface{}); ok { + authCfg["mfa_enabled"] = true + } else { + interceptors["auth"] = map[string]interface{}{ + "mfa_enabled": true, + } + } } return rcfg } diff --git a/services/thumbnails/pkg/service/grpc/v0/service.go b/services/thumbnails/pkg/service/grpc/v0/service.go index 8bd243c7191..888178b334b 100644 --- a/services/thumbnails/pkg/service/grpc/v0/service.go +++ b/services/thumbnails/pkg/service/grpc/v0/service.go @@ -29,6 +29,7 @@ import ( tjwt "github.com/owncloud/ocis/v2/services/thumbnails/pkg/service/jwt" "github.com/owncloud/ocis/v2/services/thumbnails/pkg/thumbnail" "github.com/owncloud/ocis/v2/services/thumbnails/pkg/thumbnail/imgsource" + ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" ) // NewService returns a service implementation for Service. @@ -276,19 +277,12 @@ func (g Thumbnail) handleWebdavSource(ctx context.Context, req *thumbnailssvc.Ge func (g Thumbnail) stat(ctx context.Context, path, auth string) (*provider.StatResponse, error) { outCtx := metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, auth) - // Propagate MFA status to the outgoing gRPC call so that vault storage - // (guarded by the mfa interceptor) grants access. - // go-micro callers (e.g. webdav service) send metadata via go-micro's own - // mechanism (Grpc-Metadata- headers), which is read back via - // gmmetadata.FromContext. Standard gRPC incoming metadata is checked as - // fallback for non-go-micro callers. + // Bridge MFA status from go-micro metadata (set by the webdav service) into + // outgoing gRPC metadata. The autoprop-prefixed key is then forwarded + // automatically at every subsequent gRPC hop by the metadata interceptor. if md, ok := gmmetadata.FromContext(ctx); ok { - if v, ok := md.Get(revactx.MFAHeader); ok && v != "" { - outCtx = metadata.AppendToOutgoingContext(outCtx, revactx.MFAHeader, v) - } - } else if md, ok := metadata.FromIncomingContext(ctx); ok { - if vals := md.Get(revactx.MFAHeader); len(vals) > 0 && vals[0] != "" { - outCtx = metadata.AppendToOutgoingContext(outCtx, revactx.MFAHeader, vals[0]) + if v, ok := md.Get(ctxpkg.MFAOutgoingHeader); ok && v != "" { + outCtx = metadata.AppendToOutgoingContext(outCtx, ctxpkg.MFAOutgoingHeader, v) } } diff --git a/services/thumbnails/pkg/thumbnail/imgsource/cs3.go b/services/thumbnails/pkg/thumbnail/imgsource/cs3.go index d50c66ea615..d770494f05f 100644 --- a/services/thumbnails/pkg/thumbnail/imgsource/cs3.go +++ b/services/thumbnails/pkg/thumbnail/imgsource/cs3.go @@ -62,17 +62,12 @@ func (s CS3) Get(ctx context.Context, path string) (io.ReadCloser, error) { ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, auth) - // Propagate MFA status to the outgoing gRPC call so that vault storage - // (guarded by the mfa interceptor) grants access. - // go-micro callers send MFA via go-micro metadata; non-go-micro callers - // via standard gRPC incoming metadata. + // Bridge MFA status from go-micro metadata into outgoing gRPC metadata. + // The autoprop-prefixed key is then forwarded automatically at every + // subsequent gRPC hop by the metadata interceptor. if md, ok := gmmetadata.FromContext(ctx); ok { - if v, ok := md.Get(revactx.MFAHeader); ok && v != "" { - ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAHeader, v) - } - } else if md, ok := metadata.FromIncomingContext(ctx); ok { - if vals := md.Get(revactx.MFAHeader); len(vals) > 0 && vals[0] != "" { - ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAHeader, vals[0]) + if v, ok := md.Get(revactx.MFAOutgoingHeader); ok && v != "" { + ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAOutgoingHeader, v) } } diff --git a/services/webdav/pkg/service/v0/service.go b/services/webdav/pkg/service/v0/service.go index 6b6c8215bf3..ed116336cd0 100644 --- a/services/webdav/pkg/service/v0/service.go +++ b/services/webdav/pkg/service/v0/service.go @@ -25,6 +25,7 @@ import ( grpcmetadata "google.golang.org/grpc/metadata" "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/ocis-pkg/mfa" "github.com/owncloud/ocis/v2/ocis-pkg/registry" "github.com/owncloud/ocis/v2/ocis-pkg/tracing" thumbnailsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/thumbnails/v0" @@ -506,10 +507,10 @@ func (g Webdav) sendThumbnailResponse(rsp *thumbnailssvc.GetThumbnailResponse, w // when calling the gateway / vault storage. func mfaOutgoingCtx(r *http.Request) context.Context { mfaVal := "false" - if r.Header.Get("X-Multi-Factor-Authentication") == "true" { + if r.Header.Get(mfa.MFAHeader) == "true" { mfaVal = "true" } - return gmmetadata.Set(r.Context(), revactx.MFAHeader, mfaVal) + return gmmetadata.Set(r.Context(), revactx.MFAOutgoingHeader, mfaVal) } func extensionToThumbnailType(ext string) thumbnailsmsg.ThumbnailType { diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go index 4a2a63d1c0d..db5ea3c5b1e 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go @@ -20,6 +20,7 @@ package auth import ( "context" + "slices" "sync" "time" @@ -63,6 +64,7 @@ type config struct { GatewayAddr string `mapstructure:"gateway_addr"` UserGroupsCacheSize int `mapstructure:"usergroups_cache_size"` ScopeExpansionCacheSize int `mapstructure:"scope_expansion_cache_size"` + MFAEnabled bool `mapstructure:"mfa_enabled"` } func parseConfig(m map[string]interface{}) (*config, error) { @@ -156,6 +158,12 @@ func NewUnary(m map[string]interface{}, unprotected []string, tp trace.TracerPro ctx = ctxpkg.ContextSetUser(ctx, u) ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) ctx = grantMFAForServiceAccount(ctx, u) + if conf.MFAEnabled { + if mfav := metadata.ValueFromIncomingContext(ctx, ctxpkg.MFAOutgoingHeader); !slices.Contains(mfav, "true") { + log.Warn().Err(err).Msg("access token is invalid: MFA is required") + return mfaResponse(ctx, req, info) + } + } span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId)) @@ -246,6 +254,12 @@ func NewStream(m map[string]interface{}, unprotected []string, tp trace.TracerPr ctx = ctxpkg.ContextSetUser(ctx, u) ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) ctx = grantMFAForServiceAccount(ctx, u) + if conf.MFAEnabled { + if mfav := metadata.ValueFromIncomingContext(ctx, ctxpkg.MFAOutgoingHeader); !slices.Contains(mfav, "true") { + log.Warn().Err(err).Msg("access token is invalid: MFA is required") + return status.Errorf(codes.PermissionDenied, "MFA required to access vault storage") + } + } wrapped := newWrappedServerStream(ctx, ss) span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId)) @@ -303,7 +317,8 @@ func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token. return u, tokenScope, nil } -// grantMFAForServiceAccount automatically sets MFA=true for service accounts. +// grantMFAForServiceAccount automatically grants MFA for service accounts by +// injecting the autoprop-prefixed metadata key into outgoing context. // Service accounts are trusted internal processes that never authenticate via OIDC and // therefore never carry an acr/MFA claim. Granting them implicit MFA allows // them to access MFA-gated resources such as vault storage without @@ -312,12 +327,7 @@ func grantMFAForServiceAccount(ctx context.Context, u *userpb.User) context.Cont if u.GetId().GetType() != userpb.UserType_USER_TYPE_SERVICE { return ctx } - if _, alreadySet := ctxpkg.ContextGetMFA(ctx); alreadySet { - return ctx - } - ctx = ctxpkg.ContextSetMFA(ctx, true) - ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAHeader, "true") - return ctx + return metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAOutgoingHeader, "true") } func getUserGroups(ctx context.Context, u *userpb.User, client gatewayv1beta1.GatewayAPIClient) ([]string, error) { diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/mfa.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/mfa.go new file mode 100644 index 00000000000..513ed50d1bd --- /dev/null +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/mfa.go @@ -0,0 +1,65 @@ +package auth + +import ( + "context" + + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + rstatus "github.com/owncloud/reva/v2/pkg/rgrpc/status" + "github.com/rs/zerolog/log" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + grpcstatus "google.golang.org/grpc/status" +) + +func mfaResponse(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo) (interface{}, error) { + const msg = "MFA required to access vault storage" + switch req.(type) { + case *provider.StatRequest: + return &provider.StatResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.ListContainerRequest: + return &provider.ListContainerResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.GetPathRequest: + return &provider.GetPathResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.GetQuotaRequest: + return &provider.GetQuotaResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.InitiateFileDownloadRequest: + return &provider.InitiateFileDownloadResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.InitiateFileUploadRequest: + return &provider.InitiateFileUploadResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.CreateContainerRequest: + return &provider.CreateContainerResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.TouchFileRequest: + return &provider.TouchFileResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.DeleteRequest: + return &provider.DeleteResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.MoveRequest: + return &provider.MoveResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.CreateHomeRequest: + return &provider.CreateHomeResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.AddGrantRequest: + return &provider.AddGrantResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.RemoveGrantRequest: + return &provider.RemoveGrantResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.UpdateGrantRequest: + return &provider.UpdateGrantResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.ListGrantsRequest: + return &provider.ListGrantsResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.ListFileVersionsRequest: + return &provider.ListFileVersionsResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.RestoreFileVersionRequest: + return &provider.RestoreFileVersionResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.ListRecycleRequest: + return &provider.ListRecycleResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.RestoreRecycleItemRequest: + return &provider.RestoreRecycleItemResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.PurgeRecycleRequest: + return &provider.PurgeRecycleResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.SetArbitraryMetadataRequest: + return &provider.SetArbitraryMetadataResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.UnsetArbitraryMetadataRequest: + return &provider.UnsetArbitraryMetadataResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + default: + log.Debug().Str("method", info.FullMethod).Msg("mfa: blocking unknown request type") + return nil, grpcstatus.Errorf(codes.PermissionDenied, "mfa: %s: %T", msg, req) + } +} diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/loader/loader.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/loader/loader.go index 98b94fd5c81..c5707555d2b 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/loader/loader.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/loader/loader.go @@ -21,7 +21,6 @@ package loader import ( // Load core GRPC services _ "github.com/owncloud/reva/v2/internal/grpc/interceptors/eventsmiddleware" - _ "github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa" _ "github.com/owncloud/reva/v2/internal/grpc/interceptors/prometheus" _ "github.com/owncloud/reva/v2/internal/grpc/interceptors/readonly" // Add your own service here diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa/mfa.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa/mfa.go deleted file mode 100644 index e43161de060..00000000000 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa/mfa.go +++ /dev/null @@ -1,102 +0,0 @@ -package mfa - -import ( - "context" - - userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - "github.com/owncloud/reva/v2/pkg/appctx" - ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" - "github.com/owncloud/reva/v2/pkg/rgrpc" - rstatus "github.com/owncloud/reva/v2/pkg/rgrpc/status" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - grpcstatus "google.golang.org/grpc/status" -) - -const ( - // defaultPriority places mfa before readonly (200) so the MFA check - // runs first and returns a clear PermissionDenied rather than a readonly error. - defaultPriority = 150 -) - -func init() { - rgrpc.RegisterUnaryInterceptor("mfa", NewUnary) -} - -// NewUnary returns a new unary interceptor that requires MFA to be satisfied -// for every gRPC call on the vault storage provider. -// Service accounts (UserType_USER_TYPE_SERVICE) are exempt because they are -// used for internal operations (postprocessing, event handling, etc.) that -// never carry an MFA claim. -func NewUnary(map[string]interface{}) (grpc.UnaryServerInterceptor, int, error) { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - log := appctx.GetLogger(ctx) - - // Bypass for service accounts — they perform internal operations and - // never carry an MFA claim. - if u, ok := ctxpkg.ContextGetUser(ctx); ok { - if u.GetId().GetType() == userpb.UserType_USER_TYPE_SERVICE { - return handler(ctx, req) - } - } - - hasMFA, _ := ctxpkg.ContextGetMFA(ctx) - if hasMFA { - return handler(ctx, req) - } - - log.Warn().Str("method", info.FullMethod).Msg("mfa: access denied, MFA required") - - const msg = "MFA required to access vault storage" - switch req.(type) { - case *provider.StatRequest: - return &provider.StatResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.ListContainerRequest: - return &provider.ListContainerResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.GetPathRequest: - return &provider.GetPathResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.GetQuotaRequest: - return &provider.GetQuotaResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.InitiateFileDownloadRequest: - return &provider.InitiateFileDownloadResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.InitiateFileUploadRequest: - return &provider.InitiateFileUploadResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.CreateContainerRequest: - return &provider.CreateContainerResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.TouchFileRequest: - return &provider.TouchFileResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.DeleteRequest: - return &provider.DeleteResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.MoveRequest: - return &provider.MoveResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.CreateHomeRequest: - return &provider.CreateHomeResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.AddGrantRequest: - return &provider.AddGrantResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.RemoveGrantRequest: - return &provider.RemoveGrantResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.UpdateGrantRequest: - return &provider.UpdateGrantResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.ListGrantsRequest: - return &provider.ListGrantsResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.ListFileVersionsRequest: - return &provider.ListFileVersionsResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.RestoreFileVersionRequest: - return &provider.RestoreFileVersionResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.ListRecycleRequest: - return &provider.ListRecycleResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.RestoreRecycleItemRequest: - return &provider.RestoreRecycleItemResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.PurgeRecycleRequest: - return &provider.PurgeRecycleResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.SetArbitraryMetadataRequest: - return &provider.SetArbitraryMetadataResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - case *provider.UnsetArbitraryMetadataRequest: - return &provider.UnsetArbitraryMetadataResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil - default: - log.Debug().Str("method", info.FullMethod).Msg("mfa: blocking unknown request type") - return nil, grpcstatus.Errorf(codes.PermissionDenied, "mfa: %s: %T", msg, req) - } - }, defaultPriority, nil -} diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/token/token.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/token/token.go index a00cc87db12..2dcd4b66639 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/token/token.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/token/token.go @@ -47,13 +47,6 @@ func NewUnary() grpc.UnaryServerInterceptor { ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorID) } } - - if val, ok := md[ctxpkg.MFAHeader]; ok { - if len(val) > 0 && val[0] != "" { - ctx = ctxpkg.ContextSetMFA(ctx, val[0] == "true") - ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAHeader, val[0]) - } - } } return handler(ctx, req) @@ -84,13 +77,6 @@ func NewStream() grpc.StreamServerInterceptor { ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorID) } } - - if val, ok := md[ctxpkg.MFAHeader]; ok { - if len(val) > 0 && val[0] != "" { - ctx = ctxpkg.ContextSetMFA(ctx, val[0] == "true") - ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAHeader, val[0]) - } - } } wrapped := newWrappedServerStream(ctx, ss) diff --git a/vendor/github.com/owncloud/reva/v2/internal/http/interceptors/auth/auth.go b/vendor/github.com/owncloud/reva/v2/internal/http/interceptors/auth/auth.go index f7cce842835..ebfbdf02ccf 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/http/interceptors/auth/auth.go +++ b/vendor/github.com/owncloud/reva/v2/internal/http/interceptors/auth/auth.go @@ -355,12 +355,11 @@ func ctxWithUserInfo(ctx context.Context, r *http.Request, user *userpb.User, to ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorid) ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) - // Forward MFA status from the proxy's HTTP header to outgoing gRPC metadata. - // The proxy MultiFactor middleware always sets X-Multi-Factor-Authentication: - // "true" when MFA is disabled globally, or the real outcome when enabled. - if mfaVal := r.Header.Get("X-Multi-Factor-Authentication"); mfaVal != "" { - ctx = ctxpkg.ContextSetMFA(ctx, mfaVal == "true") - ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAHeader, mfaVal) + // Forward MFA status from the proxy's HTTP header into outgoing gRPC metadata. + // Using the autoprop-prefixed key causes the metadata interceptor to propagate + // it automatically at every subsequent gRPC hop. + if mfaVal := r.Header.Get(ctxpkg.MFAHeader); mfaVal != "" { + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAOutgoingHeader, mfaVal) } return ctx diff --git a/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go b/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go index cb2d4024e68..39b94b6f772 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go @@ -1,21 +1,11 @@ package ctx -import "context" +// MFAOutgoingHeader is the gRPC metadata key used to propagate MFA status across +// service boundaries. The "autoprop-" prefix causes the metadata interceptor +// (internal/grpc/interceptors/metadata) to forward it automatically at every +// gRPC hop, so no manual re-forwarding is required. +// The const rgrpc.AutoPropPrefix causes the cycle import +const MFAOutgoingHeader = "autoprop-mfa-authenticated" -// MFAHeader is the gRPC metadata key used to propagate MFA status across -// service boundaries. Lowercased to satisfy gRPC metadata requirements. // The corresponding HTTP header set by the proxy is "X-Multi-Factor-Authentication". -const MFAHeader = "x-mfa-authenticated" - -// ContextGetMFA returns the MFA status stored in the context, and whether it -// was set at all. A missing value (second return = false) should be treated as -// MFA not satisfied. -func ContextGetMFA(ctx context.Context) (bool, bool) { - v, ok := ctx.Value(mfaKey).(bool) - return v, ok -} - -// ContextSetMFA stores the MFA status in the context. -func ContextSetMFA(ctx context.Context, mfa bool) context.Context { - return context.WithValue(ctx, mfaKey, mfa) -} +const MFAHeader = "X-Multi-Factor-Authentication" diff --git a/vendor/github.com/owncloud/reva/v2/pkg/ctx/userctx.go b/vendor/github.com/owncloud/reva/v2/pkg/ctx/userctx.go index c32e96910a7..bb0c8076757 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/ctx/userctx.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/ctx/userctx.go @@ -33,7 +33,6 @@ const ( lockIDKey scopeKey initiatorKey - mfaKey ) // ContextGetUser returns the user if set in the given context. diff --git a/vendor/modules.txt b/vendor/modules.txt index 4e7d44456dd..39483859d49 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1316,7 +1316,7 @@ github.com/orcaman/concurrent-map # github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 ## explicit; go 1.18 github.com/owncloud/libre-graph-api-go -# github.com/owncloud/reva/v2 v2.0.0-20260414163338-be87e5bbeba9 +# github.com/owncloud/reva/v2 v2.0.0-20260416090636-3f827ba4fe4e ## explicit; go 1.24.0 github.com/owncloud/reva/v2/cmd/revad/internal/grace github.com/owncloud/reva/v2/cmd/revad/runtime @@ -1326,7 +1326,6 @@ github.com/owncloud/reva/v2/internal/grpc/interceptors/eventsmiddleware github.com/owncloud/reva/v2/internal/grpc/interceptors/loader github.com/owncloud/reva/v2/internal/grpc/interceptors/log github.com/owncloud/reva/v2/internal/grpc/interceptors/metadata -github.com/owncloud/reva/v2/internal/grpc/interceptors/mfa github.com/owncloud/reva/v2/internal/grpc/interceptors/prometheus github.com/owncloud/reva/v2/internal/grpc/interceptors/readonly github.com/owncloud/reva/v2/internal/grpc/interceptors/recovery From d68a36f6b6395a641156614f228f69e844e60c23 Mon Sep 17 00:00:00 2001 From: Roman Perekhod <2403905@gmail.com> Date: Sat, 18 Apr 2026 22:50:59 +0200 Subject: [PATCH 04/12] mfa collaboration --- go.mod | 2 +- go.sum | 4 +-- .../pkg/connector/httpadapter.go | 36 +++++++++++++------ .../pkg/middleware/wopicontext.go | 8 +++++ .../pkg/service/grpc/v0/service.go | 6 ++++ .../internal/grpc/interceptors/auth/auth.go | 19 ++-------- .../v2/internal/grpc/interceptors/auth/mfa.go | 8 +++++ .../reva/v2/pkg/auth/manager/oidc/oidc.go | 10 ------ .../serviceaccounts/serviceaccounts.go | 2 +- vendor/modules.txt | 2 +- 10 files changed, 54 insertions(+), 43 deletions(-) diff --git a/go.mod b/go.mod index d373e2978f8..bfb55cedca5 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/open-policy-agent/opa v1.12.3 github.com/orcaman/concurrent-map v1.0.0 github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 - github.com/owncloud/reva/v2 v2.0.0-20260416090636-3f827ba4fe4e + github.com/owncloud/reva/v2 v2.0.0-20260422211312-0300dc8978e0 github.com/pkg/errors v0.9.1 github.com/pkg/xattr v0.4.12 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index e0d0869b847..f385bc2b5bc 100644 --- a/go.sum +++ b/go.sum @@ -742,8 +742,8 @@ github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HD github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 h1:JRidLTAKhnvyLMRtVtSF4lhBa0NSAOs6fof+d6JnKII= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245/go.mod h1:z61VMGAJRtR1nbgXWiNoCkxUXP1B3Je9rMuJbnGd+Og= -github.com/owncloud/reva/v2 v2.0.0-20260416090636-3f827ba4fe4e h1:4rPIjsPciB5RLOCGJFjvGuTCNU5nnXkpJpWElUttbXY= -github.com/owncloud/reva/v2 v2.0.0-20260416090636-3f827ba4fe4e/go.mod h1:+rCy6oGYb2/qs5gmQa8y/pHARw634vB73MZGDY2SBIQ= +github.com/owncloud/reva/v2 v2.0.0-20260422211312-0300dc8978e0 h1:hhbhzWdBfMoXKLyFRkrdEggxGD3jarE4IAt/O/QRzrA= +github.com/owncloud/reva/v2 v2.0.0-20260422211312-0300dc8978e0/go.mod h1:+rCy6oGYb2/qs5gmQa8y/pHARw634vB73MZGDY2SBIQ= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pablodz/inotifywaitgo v0.0.9 h1:njquRbBU7fuwIe5rEvtaniVBjwWzcpdUVptSgzFqZsw= diff --git a/services/collaboration/pkg/connector/httpadapter.go b/services/collaboration/pkg/connector/httpadapter.go index 3f711878542..0b1407be6a9 100644 --- a/services/collaboration/pkg/connector/httpadapter.go +++ b/services/collaboration/pkg/connector/httpadapter.go @@ -1,18 +1,22 @@ package connector import ( + "context" "encoding/json" "errors" "net/http" "strconv" gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + "github.com/owncloud/ocis/v2/ocis-pkg/mfa" "github.com/owncloud/ocis/v2/services/collaboration/pkg/config" "github.com/owncloud/ocis/v2/services/collaboration/pkg/connector/utf7" "github.com/owncloud/ocis/v2/services/collaboration/pkg/locks" + ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" "github.com/rs/zerolog" microstore "go-micro.dev/v4/store" + "google.golang.org/grpc/metadata" ) const ( @@ -67,13 +71,23 @@ func NewHttpAdapterWithConnector(con ConnectorService, l locks.LockParser) *Http } } +// mfaOutgoingCtx reads the MFA status from the HTTP request header and injects +// it into the outgoing gRPC metadata so that vault storage calls carry the MFA claim. +func mfaOutgoingCtx(r *http.Request) context.Context { + mfaVal := "false" + if r.Header.Get(mfa.MFAHeader) == "true" { + mfaVal = "true" + } + return metadata.AppendToOutgoingContext(r.Context(), ctxpkg.MFAOutgoingHeader, mfaVal) +} + // GetLock adapts the "GetLock" operation for WOPI. // Only the request's context is needed in order to extract the WOPI context. // The operation's response will be sent through the response writer and // the headers according to the spec func (h *HttpAdapter) GetLock(w http.ResponseWriter, r *http.Request) { fileCon := h.con.GetFileConnector() - response, err := fileCon.GetLock(r.Context()) + response, err := fileCon.GetLock(mfaOutgoingCtx(r)) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -94,7 +108,7 @@ func (h *HttpAdapter) Lock(w http.ResponseWriter, r *http.Request) { lockID := h.locks.ParseLock(r.Header.Get(HeaderWopiLock)) fileCon := h.con.GetFileConnector() - response, err := fileCon.Lock(r.Context(), lockID, oldLockID) + response, err := fileCon.Lock(mfaOutgoingCtx(r), lockID, oldLockID) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -115,7 +129,7 @@ func (h *HttpAdapter) RefreshLock(w http.ResponseWriter, r *http.Request) { lockID := h.locks.ParseLock(r.Header.Get(HeaderWopiLock)) fileCon := h.con.GetFileConnector() - response, err := fileCon.RefreshLock(r.Context(), lockID) + response, err := fileCon.RefreshLock(mfaOutgoingCtx(r), lockID) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -134,7 +148,7 @@ func (h *HttpAdapter) UnLock(w http.ResponseWriter, r *http.Request) { lockID := h.locks.ParseLock(r.Header.Get(HeaderWopiLock)) fileCon := h.con.GetFileConnector() - response, err := fileCon.UnLock(r.Context(), lockID) + response, err := fileCon.UnLock(mfaOutgoingCtx(r), lockID) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -150,7 +164,7 @@ func (h *HttpAdapter) UnLock(w http.ResponseWriter, r *http.Request) { // the headers according to the spec func (h *HttpAdapter) CheckFileInfo(w http.ResponseWriter, r *http.Request) { fileCon := h.con.GetFileConnector() - response, err := fileCon.CheckFileInfo(r.Context()) + response, err := fileCon.CheckFileInfo(mfaOutgoingCtx(r)) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -165,7 +179,7 @@ func (h *HttpAdapter) CheckFileInfo(w http.ResponseWriter, r *http.Request) { // The file's content will be written in the response writer func (h *HttpAdapter) GetFile(w http.ResponseWriter, r *http.Request) { contentCon := h.con.GetContentConnector() - err := contentCon.GetFile(r.Context(), w) + err := contentCon.GetFile(mfaOutgoingCtx(r), w) if err != nil { var conError *ConnectorError if errors.As(err, &conError) { @@ -188,7 +202,7 @@ func (h *HttpAdapter) PutFile(w http.ResponseWriter, r *http.Request) { lockID := h.locks.ParseLock(r.Header.Get(HeaderWopiLock)) contentCon := h.con.GetContentConnector() - response, err := contentCon.PutFile(r.Context(), r.Body, r.ContentLength, lockID) + response, err := contentCon.PutFile(mfaOutgoingCtx(r), r.Body, r.ContentLength, lockID) if err != nil { var connErr *ConnectorError @@ -232,7 +246,7 @@ func (h *HttpAdapter) PutRelativeFile(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - response, putErr = fileCon.PutRelativeFileSuggested(r.Context(), h.con.GetContentConnector(), r.Body, r.ContentLength, utf8Target) + response, putErr = fileCon.PutRelativeFileSuggested(mfaOutgoingCtx(r), h.con.GetContentConnector(), r.Body, r.ContentLength, utf8Target) } if relativeTarget != "" { @@ -241,7 +255,7 @@ func (h *HttpAdapter) PutRelativeFile(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - response, putErr = fileCon.PutRelativeFileRelative(r.Context(), h.con.GetContentConnector(), r.Body, r.ContentLength, utf8Target) + response, putErr = fileCon.PutRelativeFileRelative(mfaOutgoingCtx(r), h.con.GetContentConnector(), r.Body, r.ContentLength, utf8Target) } if putErr != nil { @@ -263,7 +277,7 @@ func (h *HttpAdapter) DeleteFile(w http.ResponseWriter, r *http.Request) { lockID := r.Header.Get(HeaderWopiLock) fileCon := h.con.GetFileConnector() - response, err := fileCon.DeleteFile(r.Context(), lockID) + response, err := fileCon.DeleteFile(mfaOutgoingCtx(r), lockID) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -292,7 +306,7 @@ func (h *HttpAdapter) RenameFile(w http.ResponseWriter, r *http.Request) { } fileCon := h.con.GetFileConnector() - response, err := fileCon.RenameFile(r.Context(), lockID, utf8Target) + response, err := fileCon.RenameFile(mfaOutgoingCtx(r), lockID, utf8Target) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return diff --git a/services/collaboration/pkg/middleware/wopicontext.go b/services/collaboration/pkg/middleware/wopicontext.go index 0d137eb8d73..ab791495708 100644 --- a/services/collaboration/pkg/middleware/wopicontext.go +++ b/services/collaboration/pkg/middleware/wopicontext.go @@ -36,6 +36,7 @@ type WopiContext struct { FileReference *providerv1beta1.Reference TemplateReference *providerv1beta1.Reference ViewMode appproviderv1beta1.ViewMode + HasMFA bool } // WopiContextAuthMiddleware will prepare an HTTP handler to be used as @@ -133,6 +134,13 @@ func WopiContextAuthMiddleware(cfg *config.Config, st microstore.Store, next htt ctx = ctxpkg.ContextSetUser(ctx, user) ctx = ctxpkg.ContextSetScopes(ctx, scopes) + // Propagate MFA status embedded in the WOPI token to outgoing gRPC metadata. + mfaVal := "false" + if claims.WopiContext.HasMFA { + mfaVal = "true" + } + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAOutgoingHeader, mfaVal) + // include additional info in the context's logger wopiLogger = wopiLogger.With(). Str("FileReference", claims.WopiContext.FileReference.String()). diff --git a/services/collaboration/pkg/service/grpc/v0/service.go b/services/collaboration/pkg/service/grpc/v0/service.go index 0d0fd3172cb..9ec51677369 100644 --- a/services/collaboration/pkg/service/grpc/v0/service.go +++ b/services/collaboration/pkg/service/grpc/v0/service.go @@ -5,6 +5,7 @@ import ( "errors" "net/url" "path" + "slices" "strconv" "strings" @@ -13,10 +14,12 @@ import ( userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" "github.com/owncloud/reva/v2/pkg/storagespace" "github.com/owncloud/reva/v2/pkg/utils" microstore "go-micro.dev/v4/store" + "google.golang.org/grpc/metadata" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/collaboration/pkg/config" @@ -121,11 +124,14 @@ func (s *Service) OpenInApp( } // create the wopiContext and generate the token + mfav := metadata.ValueFromIncomingContext(ctx, ctxpkg.MFAOutgoingHeader) + hasMFA := slices.Contains(mfav, "true") wopiContext := middleware.WopiContext{ AccessToken: req.GetAccessToken(), // it will be encrypted ViewOnlyToken: utils.ReadPlainFromOpaque(req.GetOpaque(), "viewOnlyToken"), FileReference: &providerFileRef, ViewMode: req.GetViewMode(), + HasMFA: hasMFA, } if templateID := utils.ReadPlainFromOpaque(req.GetOpaque(), "template"); templateID != "" { diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go index db5ea3c5b1e..deb8fa5b79a 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go @@ -157,10 +157,9 @@ func NewUnary(m map[string]interface{}, unprotected []string, tp trace.TracerPro // store user and scopes in context ctx = ctxpkg.ContextSetUser(ctx, u) ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) - ctx = grantMFAForServiceAccount(ctx, u) if conf.MFAEnabled { if mfav := metadata.ValueFromIncomingContext(ctx, ctxpkg.MFAOutgoingHeader); !slices.Contains(mfav, "true") { - log.Warn().Err(err).Msg("access token is invalid: MFA is required") + log.Warn().Str("user_id", u.Id.OpaqueId).Strs("mfa_values", mfav).Msg("MFA is required") return mfaResponse(ctx, req, info) } } @@ -253,10 +252,9 @@ func NewStream(m map[string]interface{}, unprotected []string, tp trace.TracerPr // store user and scopes in context ctx = ctxpkg.ContextSetUser(ctx, u) ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) - ctx = grantMFAForServiceAccount(ctx, u) if conf.MFAEnabled { if mfav := metadata.ValueFromIncomingContext(ctx, ctxpkg.MFAOutgoingHeader); !slices.Contains(mfav, "true") { - log.Warn().Err(err).Msg("access token is invalid: MFA is required") + log.Warn().Str("user_id", u.Id.OpaqueId).Strs("mfa_values", mfav).Msg("MFA is required") return status.Errorf(codes.PermissionDenied, "MFA required to access vault storage") } } @@ -317,19 +315,6 @@ func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token. return u, tokenScope, nil } -// grantMFAForServiceAccount automatically grants MFA for service accounts by -// injecting the autoprop-prefixed metadata key into outgoing context. -// Service accounts are trusted internal processes that never authenticate via OIDC and -// therefore never carry an acr/MFA claim. Granting them implicit MFA allows -// them to access MFA-gated resources such as vault storage without -// compromising the MFA requirement for regular users. -func grantMFAForServiceAccount(ctx context.Context, u *userpb.User) context.Context { - if u.GetId().GetType() != userpb.UserType_USER_TYPE_SERVICE { - return ctx - } - return metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAOutgoingHeader, "true") -} - func getUserGroups(ctx context.Context, u *userpb.User, client gatewayv1beta1.GatewayAPIClient) ([]string, error) { if groupsIf, err := userGroupsCache.Get(u.Id.OpaqueId); err == nil { log := appctx.GetLogger(ctx) diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/mfa.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/mfa.go index 513ed50d1bd..98cd97e3a82 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/mfa.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/mfa.go @@ -58,6 +58,14 @@ func mfaResponse(ctx context.Context, req interface{}, info *grpc.UnaryServerInf return &provider.SetArbitraryMetadataResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil case *provider.UnsetArbitraryMetadataRequest: return &provider.UnsetArbitraryMetadataResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.ListStorageSpacesRequest: + return &provider.ListStorageSpacesResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.CreateStorageSpaceRequest: + return &provider.CreateStorageSpaceResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.UpdateStorageSpaceRequest: + return &provider.UpdateStorageSpaceResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil + case *provider.DeleteStorageSpaceRequest: + return &provider.DeleteStorageSpaceResponse{Status: rstatus.NewPermissionDenied(ctx, nil, msg)}, nil default: log.Debug().Str("method", info.FullMethod).Msg("mfa: blocking unknown request type") return nil, grpcstatus.Errorf(codes.PermissionDenied, "mfa: %s: %T", msg, req) diff --git a/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/oidc/oidc.go b/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/oidc/oidc.go index f716e1c86e7..757aca3b9cb 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/oidc/oidc.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/oidc/oidc.go @@ -43,7 +43,6 @@ import ( "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" "github.com/owncloud/reva/v2/pkg/rhttp" "github.com/owncloud/reva/v2/pkg/sharedconf" - "github.com/owncloud/reva/v2/pkg/utils" "github.com/pkg/errors" "golang.org/x/oauth2" ) @@ -242,15 +241,6 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) GidNumber: claims[am.c.GIDClaim].(int64), } - // Embed the OIDC acr (Authentication Context Class Reference) claim into - // User.Opaque so it is cryptographically bound to the minted reva JWT. - // Downstream services (e.g. the mfa gRPC interceptor) can use this - // as a secondary MFA proof independent of the X-Multi-Factor-Authentication - // HTTP header propagation path. - if acr, ok := claims["acr"].(string); ok && acr != "" { - u.Opaque = utils.AppendPlainToOpaque(u.Opaque, "acr", acr) - } - var scopes map[string]*authpb.Scope if userID != nil && (userID.Type == user.UserType_USER_TYPE_LIGHTWEIGHT || userID.Type == user.UserType_USER_TYPE_FEDERATED) { scopes, err = scope.AddLightweightAccountScope(authpb.Role_ROLE_OWNER, nil) diff --git a/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/serviceaccounts/serviceaccounts.go b/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/serviceaccounts/serviceaccounts.go index 2d3fabfa68d..a7238352739 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/serviceaccounts/serviceaccounts.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/serviceaccounts/serviceaccounts.go @@ -6,10 +6,10 @@ import ( authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/mitchellh/mapstructure" "github.com/owncloud/reva/v2/pkg/auth" "github.com/owncloud/reva/v2/pkg/auth/manager/registry" "github.com/owncloud/reva/v2/pkg/auth/scope" - "github.com/mitchellh/mapstructure" "github.com/pkg/errors" ) diff --git a/vendor/modules.txt b/vendor/modules.txt index 39483859d49..d0d267e7264 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1316,7 +1316,7 @@ github.com/orcaman/concurrent-map # github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 ## explicit; go 1.18 github.com/owncloud/libre-graph-api-go -# github.com/owncloud/reva/v2 v2.0.0-20260416090636-3f827ba4fe4e +# github.com/owncloud/reva/v2 v2.0.0-20260422211312-0300dc8978e0 ## explicit; go 1.24.0 github.com/owncloud/reva/v2/cmd/revad/internal/grace github.com/owncloud/reva/v2/cmd/revad/runtime From 81ecc72a376f8112a0bbd285510051ad28f9026d Mon Sep 17 00:00:00 2001 From: Roman Perekhod <2403905@gmail.com> Date: Fri, 24 Apr 2026 00:08:57 +0200 Subject: [PATCH 05/12] fix GetRootDriveChildren and more --- services/graph/pkg/config/config.go | 2 +- services/graph/pkg/config/service.go | 2 +- services/graph/pkg/service/v0/driveitems.go | 15 ++++---- .../graph/pkg/service/v0/driveitems_test.go | 38 +++++++++++++++++++ .../graph/pkg/service/v0/spacetemplates.go | 1 + services/proxy/pkg/config/config.go | 2 +- services/proxy/pkg/middleware/create_home.go | 25 +++--------- services/storage-users/pkg/config/config.go | 4 +- 8 files changed, 58 insertions(+), 31 deletions(-) diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index 2eaad66190d..d5c32903a3b 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -39,7 +39,7 @@ type Config struct { Validation Validation `yaml:"validation"` - EnableVaultMode bool `yaml:"enable_vault_mode" env:"GRAPH_ENABLE_VAULT_MODE" desc:"Enable vault mode for the graph service runned in addition to the regular graph service. Required the running the storage-users-vault additional service." introductionVersion:"daledda"` + EnableVaultMode bool `yaml:"enable_vault_mode" env:"GRAPH_ENABLE_VAULT_MODE" desc:"Enable vault mode for the graph service runned in addition to the regular graph service. Required the running the storage-users-vault additional service." introductionVersion:"Deledda"` Context context.Context `yaml:"-"` } diff --git a/services/graph/pkg/config/service.go b/services/graph/pkg/config/service.go index f7edce2b7dd..6e9ee82bb15 100644 --- a/services/graph/pkg/config/service.go +++ b/services/graph/pkg/config/service.go @@ -2,5 +2,5 @@ package config // Service defines the available service configuration. type Service struct { - Name string `yaml:"name" env:"GRAPH_SERVICE_NAME" desc:"The name of the service." introductionVersion:"daledda"` + Name string `yaml:"name" env:"GRAPH_SERVICE_NAME" desc:"The name of the service." introductionVersion:"Deledda"` } diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index 0edcbf50ea7..73b101b52a4 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -155,18 +155,19 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { currentUser := revactx.ContextMustGetUser(r.Context()) // do we need to list all or only the personal drive - filters := []*storageprovider.ListStorageSpacesRequest_Filter{} - filters = append(filters, listStorageSpacesUserFilter(currentUser.GetId().GetOpaqueId())) - filters = append(filters, listStorageSpacesTypeFilter("personal")) + listReq := &storageprovider.ListStorageSpacesRequest{ + Filters: []*storageprovider.ListStorageSpacesRequest_Filter{ + listStorageSpacesUserFilter(currentUser.GetId().GetOpaqueId()), + listStorageSpacesTypeFilter("personal"), + }, + } // force vault storage space if vault mode is enabled if middleware.IsVaultMode(ctx) { - filters = append(filters, listStorageSpacesIDFilter(storagespace.FormatStorageID(utils.VaultStorageProviderID, currentUser.GetId().GetOpaqueId()))) + listReq.Opaque = utils.AppendPlainToOpaque(listReq.Opaque, "storage_id", utils.VaultStorageProviderID) } - res, err := gatewayClient.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{ - Filters: filters, - }) + res, err := gatewayClient.ListStorageSpaces(ctx, listReq) switch { case err != nil: g.logger.Error().Err(err).Msg("error making ListStorageSpaces grpc call") diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go index 4cbe4769097..07168dee2a3 100644 --- a/services/graph/pkg/service/v0/driveitems_test.go +++ b/services/graph/pkg/service/v0/driveitems_test.go @@ -30,6 +30,7 @@ import ( "github.com/owncloud/ocis/v2/services/graph/pkg/config" "github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults" identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks" + graphmw "github.com/owncloud/ocis/v2/services/graph/pkg/middleware" service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0" ) @@ -197,6 +198,43 @@ var _ = Describe("Driveitems", func() { }) }) + Describe("GetRootDriveChildren vault mode filter", func() { + It("does not set storage_id opaque in normal mode", func() { + var captured *provider.ListStorageSpacesRequest + gatewayClient.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *provider.ListStorageSpacesRequest) bool { + captured = req + return true + })).Return(&provider.ListStorageSpacesResponse{ + Status: status.NewNotFound(ctx, "not found"), + }, nil).Once() + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children", nil) + r = r.WithContext(revactx.ContextSetUser(ctx, currentUser)) + svc.GetRootDriveChildren(rr, r) + + Expect(captured).ToNot(BeNil()) + Expect(captured.GetOpaque().GetMap()).ToNot(HaveKey("storage_id")) + }) + + It("sets storage_id opaque to VaultStorageProviderID in vault mode", func() { + var captured *provider.ListStorageSpacesRequest + gatewayClient.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *provider.ListStorageSpacesRequest) bool { + captured = req + return true + })).Return(&provider.ListStorageSpacesResponse{ + Status: status.NewNotFound(ctx, "not found"), + }, nil).Once() + + r := httptest.NewRequest(http.MethodGet, "/vault/graph/v1.0/me/drive/root/children", nil) + r = r.WithContext(graphmw.SetVaultMode(revactx.ContextSetUser(ctx, currentUser), true)) + svc.GetRootDriveChildren(rr, r) + + Expect(captured).ToNot(BeNil()) + Expect(captured.GetOpaque().GetMap()).To(HaveKey("storage_id")) + Expect(string(captured.GetOpaque().GetMap()["storage_id"].Value)).To(Equal(utils.VaultStorageProviderID)) + }) + }) + Describe("GetDriveItemChildren", func() { It("handles ListContainer not found", func() { gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{ diff --git a/services/graph/pkg/service/v0/spacetemplates.go b/services/graph/pkg/service/v0/spacetemplates.go index 0f5e9798fba..d38e7bab7ae 100644 --- a/services/graph/pkg/service/v0/spacetemplates.go +++ b/services/graph/pkg/service/v0/spacetemplates.go @@ -56,6 +56,7 @@ func (g Graph) applySpaceTemplate(ctx context.Context, gwc gateway.GatewayAPICli func (g Graph) applyDefaultTemplate(ctx context.Context, gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, locale string) error { storageUsersAddress := g.config.Spaces.StorageUsersAddress if middleware.IsVaultMode(ctx) { + // TODO add the environment variable storageUsersAddress = storageUsersAddress + "-vault" } mdc := metadata.NewCS3(g.config.Reva.Address, storageUsersAddress) diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go index 7a2684c9136..0d79203b907 100644 --- a/services/proxy/pkg/config/config.go +++ b/services/proxy/pkg/config/config.go @@ -48,7 +48,7 @@ type Config struct { ClaimSpaceManagement ClaimSpaceManagement `yaml:"claim_space_management"` MultiFactorAuthentication MFAConfig `yaml:"mfa"` MultiInstance MultiInstanceConfig `yaml:"multi_instance"` - CreateVaultHome bool `yaml:"create_vault_home" env:"PROXY_CREATE_VAULT_HOME" desc:"Set this to true to automatically create a new vault home for the user if it does not exist." introductionVersion:"daledda"` + CreateVaultHome bool `yaml:"create_vault_home" env:"PROXY_CREATE_VAULT_HOME" desc:"Set this to true to automatically create a new vault home for the user if it does not exist." introductionVersion:"Deledda"` Context context.Context `json:"-" yaml:"-"` } diff --git a/services/proxy/pkg/middleware/create_home.go b/services/proxy/pkg/middleware/create_home.go index 621f211d663..6f009e7f6c9 100644 --- a/services/proxy/pkg/middleware/create_home.go +++ b/services/proxy/pkg/middleware/create_home.go @@ -30,6 +30,7 @@ func CreateHome(optionSetters ...Option) func(next http.Handler) http.Handler { logger: logger, revaGatewaySelector: options.RevaGatewaySelector, roleQuotas: options.RoleQuotas, + createVaultHome: options.CreateVaultHome, cache: sync.Map{}, } } @@ -40,6 +41,7 @@ type createHome struct { logger log.Logger revaGatewaySelector pool.Selectable[gateway.GatewayAPIClient] roleQuotas map[string]uint64 + createVaultHome bool cache sync.Map // Store users for which personal space has been in memory indefinitely. Persistence isn't critical. } @@ -69,16 +71,6 @@ func (m *createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { } if limit, hasLimit := m.checkRoleQuotaLimit(roleIDs); hasLimit { createHomeReq.Opaque = utils.AppendPlainToOpaque(nil, "quota", strconv.FormatUint(limit, 10)) - if ok { - roleIDs, err := m.getUserRoles(u) - if err != nil { - m.logger.Error().Err(err).Str("userid", u.Id.OpaqueId).Msg("failed to get roles for user") - errorcode.GeneralException.Render(w, req, http.StatusInternalServerError, "Unauthorized") - return - } - if limit, hasLimit := m.checkRoleQuotaLimit(roleIDs); hasLimit { - createHomeReq.Opaque = utils.AppendPlainToOpaque(nil, "quota", strconv.FormatUint(limit, 10)) - } } client, err := m.revaGatewaySelector.Next() @@ -97,24 +89,19 @@ func (m *createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { case createHomeRes.GetStatus().GetCode() == rpc.Code_CODE_ALREADY_EXISTS: m.logger.Info().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", createHomeRes.GetStatus()).Msg("personal space already exists") m.cache.Store(key, struct{}{}) - default: - m.logger.Error().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", createHomeRes.GetStatus()).Msg("personal space creation failed") - m.cache.Set(key, struct{}{}, 0) - case createHomeRes.GetStatus().GetCode() == rpc.Code_CODE_ALREADY_EXISTS: - m.logger.Info().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", createHomeRes.GetStatus()).Msg("personal space already exists") - m.cache.Set(key, struct{}{}, 0) default: m.logger.Error().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", createHomeRes.GetStatus()).Msg("personal space creation failed") } } + // TODO Perekhod: Create the vault home based on User permission if m.createVaultHome { // Force MFA=true for vault home creation so provisioning succeeds even // before the user has made a regular MFA-verified request. vctx := metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAOutgoingHeader, "true") vaultKey := storagespace.FormatStorageID(utils.VaultStorageProviderID, u.GetId().GetOpaqueId()) - if !m.cache.Has(vaultKey) { + if _, exists := m.cache.Load(vaultKey); !exists { // Create vault personal space // Inject storage_id into opaque for vault personal space createHomeReq.Opaque = utils.AppendPlainToOpaque(createHomeReq.Opaque, "storage_id", utils.VaultStorageProviderID) @@ -125,10 +112,10 @@ func (m *createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { m.logger.Err(err).Msg("error calling CreateHome for vault personal") case cpsRes.GetStatus().GetCode() == rpc.Code_CODE_OK: m.logger.Debug().Interface("userID", u.GetId().GetOpaqueId()).Msg("vault personal space created") - m.cache.Set(vaultKey, struct{}{}, 0) + m.cache.Store(vaultKey, struct{}{}) case cpsRes.GetStatus().GetCode() == rpc.Code_CODE_ALREADY_EXISTS: m.logger.Info().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", cpsRes.GetStatus()).Msg("vault personal space already exists") - m.cache.Set(vaultKey, struct{}{}, 0) + m.cache.Store(vaultKey, struct{}{}) default: m.logger.Error().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", cpsRes.GetStatus()).Msg("vault personal space creation failed") } diff --git a/services/storage-users/pkg/config/config.go b/services/storage-users/pkg/config/config.go index 09526b90b4b..49cd8b42b44 100644 --- a/services/storage-users/pkg/config/config.go +++ b/services/storage-users/pkg/config/config.go @@ -45,7 +45,7 @@ type Config struct { MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;STORAGE_USERS_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services." introductionVersion:"5.0"` CliMaxAttemptsRenameFile int `yaml:"max_attempts_rename_file" env:"STORAGE_USERS_CLI_MAX_ATTEMPTS_RENAME_FILE" desc:"The maximum number of attempts to rename a file when a user restores a file to an existing destination with the same name. The minimum value is 100." introductionVersion:"5.0"` - EnableVaultMode bool `yaml:"enable_vault_mode" env:"STORAGE_USERS_ENABLE_VAULT_MODE" desc:"Enable vault mode for the storage-users service runned in addition to the regular storage-users service by owerrwiting the MountID to VaultStorageProviderID. Required the running the storage-users-vault additional service." introductionVersion:"daledda"` + EnableVaultMode bool `yaml:"enable_vault_mode" env:"STORAGE_USERS_ENABLE_VAULT_MODE" desc:"Enable vault mode for the storage-users service was run in addition to the regular storage-users service by owerrwiting the MountID to VaultStorageProviderID. Required the running the storage-users-vault additional service." introductionVersion:"Deledda"` Context context.Context `yaml:"-"` } @@ -217,7 +217,7 @@ type Events struct { TLSRootCaCertPath string `yaml:"tls_root_ca_cert_path" env:"OCIS_EVENTS_TLS_ROOT_CA_CERTIFICATE;STORAGE_USERS_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided STORAGE_USERS_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"pre5.0"` EnableTLS bool `yaml:"enable_tls" env:"OCIS_EVENTS_ENABLE_TLS;STORAGE_USERS_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"pre5.0"` NumConsumers int `yaml:"num_consumers" env:"STORAGE_USERS_EVENTS_NUM_CONSUMERS" desc:"The amount of concurrent event consumers to start. Event consumers are used for post-processing files. Multiple consumers increase parallelisation, but will also increase CPU and memory demands. The setting has no effect when the OCIS_ASYNC_UPLOADS is set to false. The default and minimum value is 1." introductionVersion:"pre5.0"` - ConsumerGroup string `yaml:"consumer_group" env:"STORAGE_USERS_EVENTS_CONSUMER_GROUP" desc:"The consumer group name to use for the event consumers. The consumer group name is used to identify the consumers." introductionVersion:"daledda"` + ConsumerGroup string `yaml:"consumer_group" env:"STORAGE_USERS_EVENTS_CONSUMER_GROUP" desc:"The consumer group name to use for the event consumers. The consumer group name is used to identify the consumers." introductionVersion:"Deledda"` AuthUsername string `yaml:"username" env:"OCIS_EVENTS_AUTH_USERNAME;STORAGE_USERS_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"5.0"` AuthPassword string `yaml:"password" env:"OCIS_EVENTS_AUTH_PASSWORD;STORAGE_USERS_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"5.0"` } From 21cd10262e864b2d149469c77d853c04eaddd538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Pablo=20Villaf=C3=A1=C3=B1ez?= Date: Tue, 28 Apr 2026 10:32:52 +0200 Subject: [PATCH 06/12] fix: send the MFA header in HTTP clients from the collaboration service --- .../pkg/connector/contentconnector.go | 4 +++ .../pkg/connector/httpadapter.go | 36 ++++++------------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/services/collaboration/pkg/connector/contentconnector.go b/services/collaboration/pkg/connector/contentconnector.go index 88751e4f314..4b31e30b6e6 100644 --- a/services/collaboration/pkg/connector/contentconnector.go +++ b/services/collaboration/pkg/connector/contentconnector.go @@ -15,6 +15,7 @@ import ( rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/owncloud/ocis/v2/ocis-pkg/mfa" "github.com/owncloud/ocis/v2/ocis-pkg/tracing" "github.com/owncloud/ocis/v2/services/collaboration/pkg/config" "github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware" @@ -71,6 +72,9 @@ func newHttpRequest(ctx context.Context, wopiContext middleware.WopiContext, met } else { httpReq.Header.Add("X-Access-Token", wopiContext.AccessToken) } + if wopiContext.HasMFA { + httpReq.Header.Add(mfa.MFAHeader, "true") + } return httpReq, nil } diff --git a/services/collaboration/pkg/connector/httpadapter.go b/services/collaboration/pkg/connector/httpadapter.go index 0b1407be6a9..3f711878542 100644 --- a/services/collaboration/pkg/connector/httpadapter.go +++ b/services/collaboration/pkg/connector/httpadapter.go @@ -1,22 +1,18 @@ package connector import ( - "context" "encoding/json" "errors" "net/http" "strconv" gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - "github.com/owncloud/ocis/v2/ocis-pkg/mfa" "github.com/owncloud/ocis/v2/services/collaboration/pkg/config" "github.com/owncloud/ocis/v2/services/collaboration/pkg/connector/utf7" "github.com/owncloud/ocis/v2/services/collaboration/pkg/locks" - ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" "github.com/rs/zerolog" microstore "go-micro.dev/v4/store" - "google.golang.org/grpc/metadata" ) const ( @@ -71,23 +67,13 @@ func NewHttpAdapterWithConnector(con ConnectorService, l locks.LockParser) *Http } } -// mfaOutgoingCtx reads the MFA status from the HTTP request header and injects -// it into the outgoing gRPC metadata so that vault storage calls carry the MFA claim. -func mfaOutgoingCtx(r *http.Request) context.Context { - mfaVal := "false" - if r.Header.Get(mfa.MFAHeader) == "true" { - mfaVal = "true" - } - return metadata.AppendToOutgoingContext(r.Context(), ctxpkg.MFAOutgoingHeader, mfaVal) -} - // GetLock adapts the "GetLock" operation for WOPI. // Only the request's context is needed in order to extract the WOPI context. // The operation's response will be sent through the response writer and // the headers according to the spec func (h *HttpAdapter) GetLock(w http.ResponseWriter, r *http.Request) { fileCon := h.con.GetFileConnector() - response, err := fileCon.GetLock(mfaOutgoingCtx(r)) + response, err := fileCon.GetLock(r.Context()) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -108,7 +94,7 @@ func (h *HttpAdapter) Lock(w http.ResponseWriter, r *http.Request) { lockID := h.locks.ParseLock(r.Header.Get(HeaderWopiLock)) fileCon := h.con.GetFileConnector() - response, err := fileCon.Lock(mfaOutgoingCtx(r), lockID, oldLockID) + response, err := fileCon.Lock(r.Context(), lockID, oldLockID) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -129,7 +115,7 @@ func (h *HttpAdapter) RefreshLock(w http.ResponseWriter, r *http.Request) { lockID := h.locks.ParseLock(r.Header.Get(HeaderWopiLock)) fileCon := h.con.GetFileConnector() - response, err := fileCon.RefreshLock(mfaOutgoingCtx(r), lockID) + response, err := fileCon.RefreshLock(r.Context(), lockID) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -148,7 +134,7 @@ func (h *HttpAdapter) UnLock(w http.ResponseWriter, r *http.Request) { lockID := h.locks.ParseLock(r.Header.Get(HeaderWopiLock)) fileCon := h.con.GetFileConnector() - response, err := fileCon.UnLock(mfaOutgoingCtx(r), lockID) + response, err := fileCon.UnLock(r.Context(), lockID) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -164,7 +150,7 @@ func (h *HttpAdapter) UnLock(w http.ResponseWriter, r *http.Request) { // the headers according to the spec func (h *HttpAdapter) CheckFileInfo(w http.ResponseWriter, r *http.Request) { fileCon := h.con.GetFileConnector() - response, err := fileCon.CheckFileInfo(mfaOutgoingCtx(r)) + response, err := fileCon.CheckFileInfo(r.Context()) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -179,7 +165,7 @@ func (h *HttpAdapter) CheckFileInfo(w http.ResponseWriter, r *http.Request) { // The file's content will be written in the response writer func (h *HttpAdapter) GetFile(w http.ResponseWriter, r *http.Request) { contentCon := h.con.GetContentConnector() - err := contentCon.GetFile(mfaOutgoingCtx(r), w) + err := contentCon.GetFile(r.Context(), w) if err != nil { var conError *ConnectorError if errors.As(err, &conError) { @@ -202,7 +188,7 @@ func (h *HttpAdapter) PutFile(w http.ResponseWriter, r *http.Request) { lockID := h.locks.ParseLock(r.Header.Get(HeaderWopiLock)) contentCon := h.con.GetContentConnector() - response, err := contentCon.PutFile(mfaOutgoingCtx(r), r.Body, r.ContentLength, lockID) + response, err := contentCon.PutFile(r.Context(), r.Body, r.ContentLength, lockID) if err != nil { var connErr *ConnectorError @@ -246,7 +232,7 @@ func (h *HttpAdapter) PutRelativeFile(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - response, putErr = fileCon.PutRelativeFileSuggested(mfaOutgoingCtx(r), h.con.GetContentConnector(), r.Body, r.ContentLength, utf8Target) + response, putErr = fileCon.PutRelativeFileSuggested(r.Context(), h.con.GetContentConnector(), r.Body, r.ContentLength, utf8Target) } if relativeTarget != "" { @@ -255,7 +241,7 @@ func (h *HttpAdapter) PutRelativeFile(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - response, putErr = fileCon.PutRelativeFileRelative(mfaOutgoingCtx(r), h.con.GetContentConnector(), r.Body, r.ContentLength, utf8Target) + response, putErr = fileCon.PutRelativeFileRelative(r.Context(), h.con.GetContentConnector(), r.Body, r.ContentLength, utf8Target) } if putErr != nil { @@ -277,7 +263,7 @@ func (h *HttpAdapter) DeleteFile(w http.ResponseWriter, r *http.Request) { lockID := r.Header.Get(HeaderWopiLock) fileCon := h.con.GetFileConnector() - response, err := fileCon.DeleteFile(mfaOutgoingCtx(r), lockID) + response, err := fileCon.DeleteFile(r.Context(), lockID) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -306,7 +292,7 @@ func (h *HttpAdapter) RenameFile(w http.ResponseWriter, r *http.Request) { } fileCon := h.con.GetFileConnector() - response, err := fileCon.RenameFile(mfaOutgoingCtx(r), lockID, utf8Target) + response, err := fileCon.RenameFile(r.Context(), lockID, utf8Target) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return From ced494f05f5eb1723877d6fe7a8c543b21b002ff Mon Sep 17 00:00:00 2001 From: Roman Perekhod <2403905@gmail.com> Date: Tue, 28 Apr 2026 21:39:40 +0200 Subject: [PATCH 07/12] provide the default storage user vault configuration --- services/gateway/pkg/config/config.go | 1 + services/gateway/pkg/config/defaults/defaultconfig.go | 1 + services/gateway/pkg/revaconfig/config.go | 2 +- services/graph/pkg/config/config.go | 3 ++- services/graph/pkg/config/defaults/defaultconfig.go | 9 +++++---- services/graph/pkg/service/v0/service.go | 2 +- services/graph/pkg/service/v0/spacetemplates.go | 5 ++--- services/storage-users/pkg/config/config.go | 2 +- 8 files changed, 14 insertions(+), 11 deletions(-) diff --git a/services/gateway/pkg/config/config.go b/services/gateway/pkg/config/config.go index 92f44c19f4e..e3af79687d8 100644 --- a/services/gateway/pkg/config/config.go +++ b/services/gateway/pkg/config/config.go @@ -42,6 +42,7 @@ type Config struct { AuthServiceEndpoint string `yaml:"auth_service_endpoint" env:"GATEWAY_AUTH_SERVICE_ENDPOINT" desc:"The endpoint of the auth-service service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` StoragePublicLinkEndpoint string `yaml:"storage_public_link_endpoint" env:"GATEWAY_STORAGE_PUBLIC_LINK_ENDPOINT" desc:"The endpoint of the storage-publiclink service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` StorageUsersEndpoint string `yaml:"storage_users_endpoint" env:"GATEWAY_STORAGE_USERS_ENDPOINT" desc:"The endpoint of the storage-users service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` + StorageUsersVaultEndpoint string `yaml:"storage_users_vault_endpoint" env:"GATEWAY_STORAGE_USERS_VAULT_ENDPOINT" desc:"The endpoint of the storage-users-vault service. The storage-users-vault is additional storage-users service ran in vault mode. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"Deledda"` StorageSharesEndpoint string `yaml:"storage_shares_endpoint" env:"GATEWAY_STORAGE_SHARES_ENDPOINT" desc:"The endpoint of the storage-shares service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` AppRegistryEndpoint string `yaml:"app_registry_endpoint" env:"GATEWAY_APP_REGISTRY_ENDPOINT" desc:"The endpoint of the app-registry service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` OCMEndpoint string `yaml:"ocm_endpoint" env:"GATEWAY_OCM_ENDPOINT" desc:"The endpoint of the ocm service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` diff --git a/services/gateway/pkg/config/defaults/defaultconfig.go b/services/gateway/pkg/config/defaults/defaultconfig.go index 4bab0339312..2a2911c5dce 100644 --- a/services/gateway/pkg/config/defaults/defaultconfig.go +++ b/services/gateway/pkg/config/defaults/defaultconfig.go @@ -58,6 +58,7 @@ func DefaultConfig() *config.Config { StoragePublicLinkEndpoint: "com.owncloud.api.storage-publiclink", StorageSharesEndpoint: "com.owncloud.api.storage-shares", StorageUsersEndpoint: "com.owncloud.api.storage-users", + StorageUsersVaultEndpoint: "com.owncloud.api.storage-users-vault", UsersEndpoint: "com.owncloud.api.users", OCMEndpoint: "com.owncloud.api.ocm", diff --git a/services/gateway/pkg/revaconfig/config.go b/services/gateway/pkg/revaconfig/config.go index a24a9a0e371..7ac686cb98d 100644 --- a/services/gateway/pkg/revaconfig/config.go +++ b/services/gateway/pkg/revaconfig/config.go @@ -152,7 +152,7 @@ func spacesProviders(cfg *config.Config, logger log.Logger) map[string]map[strin }, }, }, - "com.owncloud.api.storage-users-vault": { + cfg.StorageUsersVaultEndpoint: { // Use the dedicated storage provider for vault "providerid": utils.VaultStorageProviderID, "spaces": map[string]interface{}{ diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index d5c32903a3b..c4294a1b667 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -39,7 +39,7 @@ type Config struct { Validation Validation `yaml:"validation"` - EnableVaultMode bool `yaml:"enable_vault_mode" env:"GRAPH_ENABLE_VAULT_MODE" desc:"Enable vault mode for the graph service runned in addition to the regular graph service. Required the running the storage-users-vault additional service." introductionVersion:"Deledda"` + EnableVaultMode bool `yaml:"enable_vault_mode" env:"GRAPH_ENABLE_VAULT_MODE" desc:"Enable vault mode in addition to the regular graph service. Applicable only when the additional storage-users-vault service is running." introductionVersion:"Deledda"` Context context.Context `yaml:"-"` } @@ -52,6 +52,7 @@ type Spaces struct { UsersCacheTTL int `yaml:"users_cache_ttl" env:"GRAPH_SPACES_USERS_CACHE_TTL" desc:"Max TTL in seconds for the spaces users cache." introductionVersion:"pre5.0"` GroupsCacheTTL int `yaml:"groups_cache_ttl" env:"GRAPH_SPACES_GROUPS_CACHE_TTL" desc:"Max TTL in seconds for the spaces groups cache." introductionVersion:"pre5.0"` StorageUsersAddress string `yaml:"storage_users_address" env:"GRAPH_SPACES_STORAGE_USERS_ADDRESS" desc:"The address of the storage-users service." introductionVersion:"5.0"` + StorageUsersVaultAddress string `yaml:"storage_users_vault_address" env:"GRAPH_SPACES_STORAGE_USERS_VAULT_ADDRESS" desc:"The address of the storage-users-vault service. Applicable only when the EnableVaultMode is enabled." introductionVersion:"Deledda"` DefaultLanguage string `yaml:"default_language" env:"OCIS_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details." introductionVersion:"5.0"` TranslationPath string `yaml:"translation_path" env:"OCIS_TRANSLATION_PATH;GRAPH_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details." introductionVersion:"7.0.0"` } diff --git a/services/graph/pkg/config/defaults/defaultconfig.go b/services/graph/pkg/config/defaults/defaultconfig.go index 77078ff383a..eb9a5956680 100644 --- a/services/graph/pkg/config/defaults/defaultconfig.go +++ b/services/graph/pkg/config/defaults/defaultconfig.go @@ -68,10 +68,11 @@ func DefaultConfig() *config.Config { }, Reva: shared.DefaultRevaConfig(), Spaces: config.Spaces{ - StorageUsersAddress: "com.owncloud.api.storage-users", - WebDavBase: "https://localhost:9200", - WebDavPath: "/dav/spaces/", - DefaultQuota: "1000000000", + StorageUsersAddress: "com.owncloud.api.storage-users", + StorageUsersVaultAddress: "com.owncloud.api.storage-users-vault", + WebDavBase: "https://localhost:9200", + WebDavPath: "/dav/spaces/", + DefaultQuota: "1000000000", // 1 minute ExtendedSpacePropertiesCacheTTL: 60, // 1 minute diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 8969c960eaf..16223ecd280 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -399,7 +399,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx m.Route(options.Config.HTTP.Root, graphRoutes) - // Ini the Vault routes + // Initialize the Vault routes if options.Config.EnableVaultMode { m.Route("/vault/graph", func(r chi.Router) { r.Use(requireMFA) diff --git a/services/graph/pkg/service/v0/spacetemplates.go b/services/graph/pkg/service/v0/spacetemplates.go index d38e7bab7ae..9fe55493f36 100644 --- a/services/graph/pkg/service/v0/spacetemplates.go +++ b/services/graph/pkg/service/v0/spacetemplates.go @@ -55,9 +55,8 @@ func (g Graph) applySpaceTemplate(ctx context.Context, gwc gateway.GatewayAPICli func (g Graph) applyDefaultTemplate(ctx context.Context, gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, locale string) error { storageUsersAddress := g.config.Spaces.StorageUsersAddress - if middleware.IsVaultMode(ctx) { - // TODO add the environment variable - storageUsersAddress = storageUsersAddress + "-vault" + if g.config.EnableVaultMode && middleware.IsVaultMode(ctx) { + storageUsersAddress = g.config.Spaces.StorageUsersVaultAddress } mdc := metadata.NewCS3(g.config.Reva.Address, storageUsersAddress) mdc.SpaceRoot = root diff --git a/services/storage-users/pkg/config/config.go b/services/storage-users/pkg/config/config.go index 49cd8b42b44..7c1d062589f 100644 --- a/services/storage-users/pkg/config/config.go +++ b/services/storage-users/pkg/config/config.go @@ -45,7 +45,7 @@ type Config struct { MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;STORAGE_USERS_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services." introductionVersion:"5.0"` CliMaxAttemptsRenameFile int `yaml:"max_attempts_rename_file" env:"STORAGE_USERS_CLI_MAX_ATTEMPTS_RENAME_FILE" desc:"The maximum number of attempts to rename a file when a user restores a file to an existing destination with the same name. The minimum value is 100." introductionVersion:"5.0"` - EnableVaultMode bool `yaml:"enable_vault_mode" env:"STORAGE_USERS_ENABLE_VAULT_MODE" desc:"Enable vault mode for the storage-users service was run in addition to the regular storage-users service by owerrwiting the MountID to VaultStorageProviderID. Required the running the storage-users-vault additional service." introductionVersion:"Deledda"` + EnableVaultMode bool `yaml:"enable_vault_mode" env:"STORAGE_USERS_ENABLE_VAULT_MODE" desc:"Not applicable for use with the primary storage-users service. Used only when an additional storage-users service needs to be run in vault mode. Enabling the flag forces the storage-users service to run with a MountID set to VaultStorageProviderID." introductionVersion:"Deledda"` Context context.Context `yaml:"-"` } From 573c62f10950982ad11492cee1bb7a1e340545ca Mon Sep 17 00:00:00 2001 From: Roman Perekhod <2403905@gmail.com> Date: Wed, 29 Apr 2026 19:14:44 +0200 Subject: [PATCH 08/12] update create_home middleware --- ocis-pkg/mfa/mfa.go | 4 ++++ services/proxy/pkg/middleware/create_home.go | 6 +++--- services/storage-users/pkg/config/config.go | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ocis-pkg/mfa/mfa.go b/ocis-pkg/mfa/mfa.go index e75a46002a7..5770f408956 100644 --- a/ocis-pkg/mfa/mfa.go +++ b/ocis-pkg/mfa/mfa.go @@ -57,3 +57,7 @@ func SetHeader(r *http.Request, mfa bool) { r.Header.Set(MFAHeader, "false") } + +func IsMFAHeaderTrue(r *http.Request) bool { + return r.Header.Get(MFAHeader) == "true" +} diff --git a/services/proxy/pkg/middleware/create_home.go b/services/proxy/pkg/middleware/create_home.go index 6f009e7f6c9..22de58c27a9 100644 --- a/services/proxy/pkg/middleware/create_home.go +++ b/services/proxy/pkg/middleware/create_home.go @@ -10,6 +10,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/ocis-pkg/mfa" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" ctxpkg "github.com/owncloud/reva/v2/pkg/ctx" revactx "github.com/owncloud/reva/v2/pkg/ctx" @@ -95,9 +96,8 @@ func (m *createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { } // TODO Perekhod: Create the vault home based on User permission - if m.createVaultHome { - // Force MFA=true for vault home creation so provisioning succeeds even - // before the user has made a regular MFA-verified request. + if m.createVaultHome && mfa.IsMFAHeaderTrue(req) { + // Force MFA=true for vault home creation vctx := metadata.AppendToOutgoingContext(ctx, ctxpkg.MFAOutgoingHeader, "true") vaultKey := storagespace.FormatStorageID(utils.VaultStorageProviderID, u.GetId().GetOpaqueId()) diff --git a/services/storage-users/pkg/config/config.go b/services/storage-users/pkg/config/config.go index 7c1d062589f..a44d1a6bee2 100644 --- a/services/storage-users/pkg/config/config.go +++ b/services/storage-users/pkg/config/config.go @@ -217,7 +217,7 @@ type Events struct { TLSRootCaCertPath string `yaml:"tls_root_ca_cert_path" env:"OCIS_EVENTS_TLS_ROOT_CA_CERTIFICATE;STORAGE_USERS_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided STORAGE_USERS_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"pre5.0"` EnableTLS bool `yaml:"enable_tls" env:"OCIS_EVENTS_ENABLE_TLS;STORAGE_USERS_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"pre5.0"` NumConsumers int `yaml:"num_consumers" env:"STORAGE_USERS_EVENTS_NUM_CONSUMERS" desc:"The amount of concurrent event consumers to start. Event consumers are used for post-processing files. Multiple consumers increase parallelisation, but will also increase CPU and memory demands. The setting has no effect when the OCIS_ASYNC_UPLOADS is set to false. The default and minimum value is 1." introductionVersion:"pre5.0"` - ConsumerGroup string `yaml:"consumer_group" env:"STORAGE_USERS_EVENTS_CONSUMER_GROUP" desc:"The consumer group name to use for the event consumers. The consumer group name is used to identify the consumers." introductionVersion:"Deledda"` + ConsumerGroup string `yaml:"consumer_group" env:"STORAGE_USERS_EVENTS_CONSUMER_GROUP" desc:"The consumer group name to use for the event consumers to identify the unique group." introductionVersion:"Deledda"` AuthUsername string `yaml:"username" env:"OCIS_EVENTS_AUTH_USERNAME;STORAGE_USERS_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"5.0"` AuthPassword string `yaml:"password" env:"OCIS_EVENTS_AUTH_PASSWORD;STORAGE_USERS_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"5.0"` } From 82f04d79a55a0c943dd77b45d1cd7d818b8c8e6d Mon Sep 17 00:00:00 2001 From: Roman Perekhod Date: Mon, 4 May 2026 11:33:59 +0200 Subject: [PATCH 09/12] Update deployments/examples/ocis_full/vault-storage.yml Co-authored-by: Martin Update services/gateway/pkg/config/config.go Co-authored-by: Martin Update services/graph/pkg/config/config.go Co-authored-by: Martin Update services/graph/pkg/config/config.go Co-authored-by: Martin Update services/storage-users/pkg/config/config.go Co-authored-by: Martin Update services/proxy/pkg/config/config.go Co-authored-by: Martin Update services/storage-users/pkg/config/config.go Co-authored-by: Martin Update deployments/examples/ocis_full/vault-storage.yml Co-authored-by: Martin --- deployments/examples/ocis_full/vault-storage.yml | 7 ++++--- services/gateway/pkg/config/config.go | 2 +- services/graph/pkg/config/config.go | 4 ++-- services/proxy/pkg/config/config.go | 2 +- services/storage-users/pkg/config/config.go | 4 ++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/deployments/examples/ocis_full/vault-storage.yml b/deployments/examples/ocis_full/vault-storage.yml index d5ad6d3c093..7a8551d9748 100644 --- a/deployments/examples/ocis_full/vault-storage.yml +++ b/deployments/examples/ocis_full/vault-storage.yml @@ -8,7 +8,7 @@ services: GRAPH_ENABLE_VAULT_MODE: true storage-users-vault: - image: ${OCIS_DOCKER_IMAGE:-owncloud/ocis}:${OCIS_DOCKER_TAG:-latest} + image: ${OCIS_DOCKER_IMAGE}:${OCIS_DOCKER_TAG} networks: ocis-net: depends_on: @@ -30,8 +30,9 @@ services: OCIS_EVENTS_ENDPOINT: ocis:9233 OCIS_CACHE_STORE_NODES: ocis:9233 volumes: - - ocis-data:/var/lib/ocis - - ocis-config:/etc/ocis + # configure the .env file to use own paths instead of docker internal volumes + - ${OCIS_CONFIG_DIR:-ocis-config}:/etc/ocis + - ${OCIS_DATA_DIR:-ocis-data}:/var/lib/ocis logging: driver: ${LOG_DRIVER:-local} restart: always diff --git a/services/gateway/pkg/config/config.go b/services/gateway/pkg/config/config.go index e3af79687d8..2065ef2aebb 100644 --- a/services/gateway/pkg/config/config.go +++ b/services/gateway/pkg/config/config.go @@ -42,7 +42,7 @@ type Config struct { AuthServiceEndpoint string `yaml:"auth_service_endpoint" env:"GATEWAY_AUTH_SERVICE_ENDPOINT" desc:"The endpoint of the auth-service service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` StoragePublicLinkEndpoint string `yaml:"storage_public_link_endpoint" env:"GATEWAY_STORAGE_PUBLIC_LINK_ENDPOINT" desc:"The endpoint of the storage-publiclink service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` StorageUsersEndpoint string `yaml:"storage_users_endpoint" env:"GATEWAY_STORAGE_USERS_ENDPOINT" desc:"The endpoint of the storage-users service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` - StorageUsersVaultEndpoint string `yaml:"storage_users_vault_endpoint" env:"GATEWAY_STORAGE_USERS_VAULT_ENDPOINT" desc:"The endpoint of the storage-users-vault service. The storage-users-vault is additional storage-users service ran in vault mode. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"Deledda"` + StorageUsersVaultEndpoint string `yaml:"storage_users_vault_endpoint" env:"GATEWAY_STORAGE_USERS_VAULT_ENDPOINT" desc:"The endpoint of the storage-users-vault service. The storage-users-vault is an additional storage-users service that runs in vault mode. It can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"Deledda"` StorageSharesEndpoint string `yaml:"storage_shares_endpoint" env:"GATEWAY_STORAGE_SHARES_ENDPOINT" desc:"The endpoint of the storage-shares service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` AppRegistryEndpoint string `yaml:"app_registry_endpoint" env:"GATEWAY_APP_REGISTRY_ENDPOINT" desc:"The endpoint of the app-registry service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` OCMEndpoint string `yaml:"ocm_endpoint" env:"GATEWAY_OCM_ENDPOINT" desc:"The endpoint of the ocm service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"7.0.0"` diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index c4294a1b667..29071b86459 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -39,7 +39,7 @@ type Config struct { Validation Validation `yaml:"validation"` - EnableVaultMode bool `yaml:"enable_vault_mode" env:"GRAPH_ENABLE_VAULT_MODE" desc:"Enable vault mode in addition to the regular graph service. Applicable only when the additional storage-users-vault service is running." introductionVersion:"Deledda"` + EnableVaultMode bool `yaml:"enable_vault_mode" env:"GRAPH_ENABLE_VAULT_MODE" desc:"Enable vault mode in addition to the regular graph service. This only applies when the additional storage-users-vault service is running, which is a special configured storage-users service." introductionVersion:"Deledda"` Context context.Context `yaml:"-"` } @@ -52,7 +52,7 @@ type Spaces struct { UsersCacheTTL int `yaml:"users_cache_ttl" env:"GRAPH_SPACES_USERS_CACHE_TTL" desc:"Max TTL in seconds for the spaces users cache." introductionVersion:"pre5.0"` GroupsCacheTTL int `yaml:"groups_cache_ttl" env:"GRAPH_SPACES_GROUPS_CACHE_TTL" desc:"Max TTL in seconds for the spaces groups cache." introductionVersion:"pre5.0"` StorageUsersAddress string `yaml:"storage_users_address" env:"GRAPH_SPACES_STORAGE_USERS_ADDRESS" desc:"The address of the storage-users service." introductionVersion:"5.0"` - StorageUsersVaultAddress string `yaml:"storage_users_vault_address" env:"GRAPH_SPACES_STORAGE_USERS_VAULT_ADDRESS" desc:"The address of the storage-users-vault service. Applicable only when the EnableVaultMode is enabled." introductionVersion:"Deledda"` + StorageUsersVaultAddress string `yaml:"storage_users_vault_address" env:"GRAPH_SPACES_STORAGE_USERS_VAULT_ADDRESS" desc:"The address of the storage-users-vault service, a special configured storage-users service. Applicable only when 'GRAPH_ENABLE_VAULT_MODE' is enabled." introductionVersion:"Deledda"` DefaultLanguage string `yaml:"default_language" env:"OCIS_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details." introductionVersion:"5.0"` TranslationPath string `yaml:"translation_path" env:"OCIS_TRANSLATION_PATH;GRAPH_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details." introductionVersion:"7.0.0"` } diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go index 0d79203b907..c545fda70fc 100644 --- a/services/proxy/pkg/config/config.go +++ b/services/proxy/pkg/config/config.go @@ -48,7 +48,7 @@ type Config struct { ClaimSpaceManagement ClaimSpaceManagement `yaml:"claim_space_management"` MultiFactorAuthentication MFAConfig `yaml:"mfa"` MultiInstance MultiInstanceConfig `yaml:"multi_instance"` - CreateVaultHome bool `yaml:"create_vault_home" env:"PROXY_CREATE_VAULT_HOME" desc:"Set this to true to automatically create a new vault home for the user if it does not exist." introductionVersion:"Deledda"` + CreateVaultHome bool `yaml:"create_vault_home" env:"PROXY_CREATE_VAULT_HOME" desc:"Set this to true to automatically create a new vault home for the user if it does not exist. Only applicapable if the storage-users-vault service, a special configured storage-users service is configured." introductionVersion:"Deledda"` Context context.Context `json:"-" yaml:"-"` } diff --git a/services/storage-users/pkg/config/config.go b/services/storage-users/pkg/config/config.go index a44d1a6bee2..51859b7d5e5 100644 --- a/services/storage-users/pkg/config/config.go +++ b/services/storage-users/pkg/config/config.go @@ -45,7 +45,7 @@ type Config struct { MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;STORAGE_USERS_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services." introductionVersion:"5.0"` CliMaxAttemptsRenameFile int `yaml:"max_attempts_rename_file" env:"STORAGE_USERS_CLI_MAX_ATTEMPTS_RENAME_FILE" desc:"The maximum number of attempts to rename a file when a user restores a file to an existing destination with the same name. The minimum value is 100." introductionVersion:"5.0"` - EnableVaultMode bool `yaml:"enable_vault_mode" env:"STORAGE_USERS_ENABLE_VAULT_MODE" desc:"Not applicable for use with the primary storage-users service. Used only when an additional storage-users service needs to be run in vault mode. Enabling the flag forces the storage-users service to run with a MountID set to VaultStorageProviderID." introductionVersion:"Deledda"` + EnableVaultMode bool `yaml:"enable_vault_mode" env:"STORAGE_USERS_ENABLE_VAULT_MODE" desc:"Enabling the flag forces the storage-users service to run with a MountID set to VaultStorageProviderID. Not applicable for use with the primary storage-users service. Use only if an additional storage-users-vault service, a special configured storage-users service is configured." introductionVersion:"Deledda"` Context context.Context `yaml:"-"` } @@ -217,7 +217,7 @@ type Events struct { TLSRootCaCertPath string `yaml:"tls_root_ca_cert_path" env:"OCIS_EVENTS_TLS_ROOT_CA_CERTIFICATE;STORAGE_USERS_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided STORAGE_USERS_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"pre5.0"` EnableTLS bool `yaml:"enable_tls" env:"OCIS_EVENTS_ENABLE_TLS;STORAGE_USERS_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"pre5.0"` NumConsumers int `yaml:"num_consumers" env:"STORAGE_USERS_EVENTS_NUM_CONSUMERS" desc:"The amount of concurrent event consumers to start. Event consumers are used for post-processing files. Multiple consumers increase parallelisation, but will also increase CPU and memory demands. The setting has no effect when the OCIS_ASYNC_UPLOADS is set to false. The default and minimum value is 1." introductionVersion:"pre5.0"` - ConsumerGroup string `yaml:"consumer_group" env:"STORAGE_USERS_EVENTS_CONSUMER_GROUP" desc:"The consumer group name to use for the event consumers to identify the unique group." introductionVersion:"Deledda"` + ConsumerGroup string `yaml:"consumer_group" env:"STORAGE_USERS_EVENTS_CONSUMER_GROUP" desc:"The name of the consumer group to be used at the event to help consumers identify the unique group." introductionVersion:"Deledda"` AuthUsername string `yaml:"username" env:"OCIS_EVENTS_AUTH_USERNAME;STORAGE_USERS_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"5.0"` AuthPassword string `yaml:"password" env:"OCIS_EVENTS_AUTH_PASSWORD;STORAGE_USERS_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the ocis service which receives and delivers events between the services." introductionVersion:"5.0"` } From 3ddb02d79e351e6855faccf46eadef833fecfaef Mon Sep 17 00:00:00 2001 From: Roman Perekhod <2403905@gmail.com> Date: Mon, 4 May 2026 19:03:46 +0200 Subject: [PATCH 10/12] bump reva --- go.mod | 2 +- go.sum | 4 +- .../internal/grpc/interceptors/auth/auth.go | 4 ++ .../http/services/ocmd/notifications.go | 62 ++++++++++++++++- .../v2/internal/http/services/ocmd/ocm.go | 2 + .../http/services/owncloud/ocdav/copy.go | 2 +- .../http/services/owncloud/ocdav/move.go | 2 +- .../owncloud/reva/v2/pkg/conversions/role.go | 69 +++++++------------ .../owncloud/reva/v2/pkg/ctx/mfactx.go | 2 +- .../owncloud/reva/v2/pkg/mime/mime.go | 6 +- .../v2/pkg/storage/registry/spaces/spaces.go | 6 +- .../utils/decomposedfs/upload/upload.go | 2 +- vendor/modules.txt | 4 +- 13 files changed, 108 insertions(+), 59 deletions(-) diff --git a/go.mod b/go.mod index bfb55cedca5..3ea4a31791d 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/open-policy-agent/opa v1.12.3 github.com/orcaman/concurrent-map v1.0.0 github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 - github.com/owncloud/reva/v2 v2.0.0-20260422211312-0300dc8978e0 + github.com/owncloud/reva/v2 v2.0.0-20260504165655-6f2258091fbf github.com/pkg/errors v0.9.1 github.com/pkg/xattr v0.4.12 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index f385bc2b5bc..c120b64c2ed 100644 --- a/go.sum +++ b/go.sum @@ -742,8 +742,8 @@ github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HD github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 h1:JRidLTAKhnvyLMRtVtSF4lhBa0NSAOs6fof+d6JnKII= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245/go.mod h1:z61VMGAJRtR1nbgXWiNoCkxUXP1B3Je9rMuJbnGd+Og= -github.com/owncloud/reva/v2 v2.0.0-20260422211312-0300dc8978e0 h1:hhbhzWdBfMoXKLyFRkrdEggxGD3jarE4IAt/O/QRzrA= -github.com/owncloud/reva/v2 v2.0.0-20260422211312-0300dc8978e0/go.mod h1:+rCy6oGYb2/qs5gmQa8y/pHARw634vB73MZGDY2SBIQ= +github.com/owncloud/reva/v2 v2.0.0-20260504165655-6f2258091fbf h1:q/T/PWppIAQA+TAnP8RBdfLNkDossVGGi5bGF/prz2E= +github.com/owncloud/reva/v2 v2.0.0-20260504165655-6f2258091fbf/go.mod h1:oc3sbqju0T4B+ZwXjhe0DOy4916AiAMlJzO6AO7m8ps= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pablodz/inotifywaitgo v0.0.9 h1:njquRbBU7fuwIe5rEvtaniVBjwWzcpdUVptSgzFqZsw= diff --git a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go index deb8fa5b79a..4de00d7b334 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/auth.go @@ -157,6 +157,8 @@ func NewUnary(m map[string]interface{}, unprotected []string, tp trace.TracerPro // store user and scopes in context ctx = ctxpkg.ContextSetUser(ctx, u) ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) + // TODO: MFA enforcement should be moved to the individual service level, so each service can + // decide which endpoints require MFA and which are accessible without it. if conf.MFAEnabled { if mfav := metadata.ValueFromIncomingContext(ctx, ctxpkg.MFAOutgoingHeader); !slices.Contains(mfav, "true") { log.Warn().Str("user_id", u.Id.OpaqueId).Strs("mfa_values", mfav).Msg("MFA is required") @@ -252,6 +254,8 @@ func NewStream(m map[string]interface{}, unprotected []string, tp trace.TracerPr // store user and scopes in context ctx = ctxpkg.ContextSetUser(ctx, u) ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) + // TODO: MFA enforcement should be moved to the individual service level, so each service can + // decide which endpoints require MFA and which are accessible without it. if conf.MFAEnabled { if mfav := metadata.ValueFromIncomingContext(ctx, ctxpkg.MFAOutgoingHeader); !slices.Contains(mfav, "true") { log.Warn().Str("user_id", u.Id.OpaqueId).Strs("mfa_values", mfav).Msg("MFA is required") diff --git a/vendor/github.com/owncloud/reva/v2/internal/http/services/ocmd/notifications.go b/vendor/github.com/owncloud/reva/v2/internal/http/services/ocmd/notifications.go index 37fa1b709d5..86f1ec1c059 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/http/services/ocmd/notifications.go +++ b/vendor/github.com/owncloud/reva/v2/internal/http/services/ocmd/notifications.go @@ -28,6 +28,7 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/go-chi/render" "github.com/owncloud/reva/v2/pkg/appctx" @@ -43,7 +44,9 @@ const ( // var validate = validator.New() type notifHandler struct { - gatewaySelector *pool.Selector[gateway.GatewayAPIClient] + gatewaySelector *pool.Selector[gateway.GatewayAPIClient] + serviceAccountID string + serviceAccountSecret string } func (h *notifHandler) init(c *config) error { @@ -52,6 +55,8 @@ func (h *notifHandler) init(c *config) error { return err } h.gatewaySelector = gatewaySelector + h.serviceAccountID = c.ServiceAccountID + h.serviceAccountSecret = c.ServiceAccountSecret return nil } @@ -161,6 +166,7 @@ func (h *notifHandler) handleShareUnshared(ctx context.Context, req *notificatio return res.GetStatus(), nil } +// Current implementation supports only WebDAV protocol permissions update func (h *notifHandler) handleShareChangePermission(ctx context.Context, req *notificationRequest) (*rpc.Status, error) { gatewayClient, err := h.gatewaySelector.Next() if err != nil { @@ -171,17 +177,67 @@ func (h *notifHandler) handleShareChangePermission(ctx context.Context, req *not return nil, fmt.Errorf("error getting protocols from notification") } + // get the grantee user ID object + granteeUser, err := getUserIDFromOCMUser(req.Notification.Grantee) + if err != nil { + return nil, fmt.Errorf("error getting grantee user id: %w", err) + } + + // authenticate as a service account + authCtx, err := utils.GetServiceUserContextWithContext(ctx, gatewayClient, h.serviceAccountID, h.serviceAccountSecret) + if err != nil { + return nil, fmt.Errorf("error authenticating as service account: %w", err) + } + ctx = authCtx + o := &typesv1beta1.Opaque{} utils.AppendPlainToOpaque(o, "grantee", req.Notification.Grantee) utils.AppendPlainToOpaque(o, "resourceType", req.ResourceType) + getRes, err := gatewayClient.GetReceivedOCMShare(ctx, &ocm.GetReceivedOCMShareRequest{ + Opaque: utils.AppendJSONToOpaque(nil, "userid", granteeUser), + Ref: &ocm.ShareReference{ + Spec: &ocm.ShareReference_Id{ + Id: &ocm.ShareId{ + OpaqueId: req.ProviderId, + }, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("error getting received ocm share: %w", err) + } + if getRes.Status.Code != rpc.Code_CODE_OK { + return getRes.Status, nil + } + + share := getRes.Share + newProtocols := getProtocols(req.Notification.Protocols, o) + + var newWebdav *ocm.WebDAVProtocol + for _, p := range newProtocols { + if wd := p.GetWebdavOptions(); wd != nil { + newWebdav = wd + break + } + } + + if newWebdav != nil && newWebdav.Permissions != nil { + for _, p := range share.Protocols { + if wd := p.GetWebdavOptions(); wd != nil { + wd.Permissions = newWebdav.Permissions + break + } + } + } + res, err := gatewayClient.UpdateOCMCoreShare(ctx, &ocmcore.UpdateOCMCoreShareRequest{ OcmShareId: req.ProviderId, - Protocols: getProtocols(req.Notification.Protocols, o), + Protocols: share.Protocols, Opaque: o, }) if err != nil { - return nil, fmt.Errorf("error calling DeleteOCMCoreShare: %w", err) + return nil, fmt.Errorf("error calling UpdateOCMCoreShare: %w", err) } return res.GetStatus(), nil } diff --git a/vendor/github.com/owncloud/reva/v2/internal/http/services/ocmd/ocm.go b/vendor/github.com/owncloud/reva/v2/internal/http/services/ocmd/ocm.go index 2b849e02793..dc55ec90ce1 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/http/services/ocmd/ocm.go +++ b/vendor/github.com/owncloud/reva/v2/internal/http/services/ocmd/ocm.go @@ -37,6 +37,8 @@ type config struct { Prefix string `mapstructure:"prefix"` GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"` ExposeRecipientDisplayName bool `mapstructure:"expose_recipient_display_name"` + ServiceAccountID string `mapstructure:"service_account_id"` + ServiceAccountSecret string `mapstructure:"service_account_secret"` } func (c *config) ApplyDefaults() { diff --git a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/copy.go b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/copy.go index 737e010e2cb..f7e3c0d2de2 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/copy.go +++ b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/copy.go @@ -553,7 +553,7 @@ func (s *svc) executeSpacesCopy(ctx context.Context, w http.ResponseWriter, sele } func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, srcRef, dstRef *provider.Reference, log *zerolog.Logger, destInShareJail bool) *copy { - // restict copy from the vault + // restrict copy from the vault to outside of the vault. if destinationIsNotAllowed(srcRef, dstRef) { w.WriteHeader(http.StatusConflict) b, err := errors.Marshal(http.StatusBadRequest, "destination is not allowed", "", "") diff --git a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/move.go b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/move.go index f9a0e30999b..432b7d17a2e 100644 --- a/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/move.go +++ b/vendor/github.com/owncloud/reva/v2/internal/http/services/owncloud/ocdav/move.go @@ -141,7 +141,7 @@ func (s *svc) handleSpacesMove(w http.ResponseWriter, r *http.Request, srcSpaceI } func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Request, src, dst *provider.Reference, log zerolog.Logger) { - // restrict move from the vault + // restrict move from the vault to outside of the vault. if destinationIsNotAllowed(src, dst) { w.WriteHeader(http.StatusConflict) b, err := errors.Marshal(http.StatusBadRequest, "destination is not allowed", "", "") diff --git a/vendor/github.com/owncloud/reva/v2/pkg/conversions/role.go b/vendor/github.com/owncloud/reva/v2/pkg/conversions/role.go index 012c2fe512d..e4241564184 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/conversions/role.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/conversions/role.go @@ -53,6 +53,8 @@ const ( RoleSpaceEditorWithoutVersions = "spaceeditor-without-versions" // RoleSpaceEditorWithoutTrashbin grants editor permission without list/restore resources in trashbin on a space. RoleSpaceEditorWithoutTrashbin = "spaceeditor-without-trashbin" + // RoleSpaceEditorWithoutVersionsWithoutTrashbin grants editor permission without list/restore versions and without list/restore resources in trashbin on a space. + RoleSpaceEditorWithoutVersionsWithoutTrashbin = "spaceeditor-without-versions-without-trashbin" // RoleFileEditor grants editor permission on a single file. RoleFileEditor = "file-editor" // RoleFileEditorListGrants grants editor permission on a single file. @@ -183,6 +185,8 @@ func RoleFromName(name string) *Role { return NewSpaceEditorRole() case RoleSpaceEditorWithoutTrashbin: return NewSpaceEditorWithoutTrashbinRole() + case RoleSpaceEditorWithoutVersionsWithoutTrashbin: + return NewSpaceEditorWithoutVersionsWithoutTrashbinRole() case RoleFileEditor: return NewFileEditorRole() case RoleFileEditorListGrants: @@ -295,10 +299,10 @@ func NewEditorListGrantsWithVersionsRole() *Role { return role } -// NewSpaceEditorRole creates an editor role -func NewSpaceEditorRole() *Role { +// NewSpaceEditorWithoutVersionsWithoutTrashbinRole creates an editor role without list/restore versions and without list/restore resources in trashbin on a space. +func NewSpaceEditorWithoutVersionsWithoutTrashbinRole() *Role { return &Role{ - Name: RoleSpaceEditor, + Name: RoleSpaceEditorWithoutVersionsWithoutTrashbin, cS3ResourcePermissions: &provider.ResourcePermissions{ CreateContainer: true, Delete: true, @@ -307,12 +311,8 @@ func NewSpaceEditorRole() *Role { InitiateFileDownload: true, InitiateFileUpload: true, ListContainer: true, - ListFileVersions: true, ListGrants: true, - ListRecycle: true, Move: true, - RestoreFileVersion: true, - RestoreRecycleItem: true, Stat: true, }, ocsPermissions: PermissionRead | PermissionCreate | PermissionWrite | PermissionDelete, @@ -321,46 +321,29 @@ func NewSpaceEditorRole() *Role { // NewSpaceEditorWithoutVersionsRole creates an editor without list/restore versions role func NewSpaceEditorWithoutVersionsRole() *Role { - return &Role{ - Name: RoleSpaceEditorWithoutVersions, - cS3ResourcePermissions: &provider.ResourcePermissions{ - CreateContainer: true, - Delete: true, - GetPath: true, - GetQuota: true, - InitiateFileDownload: true, - InitiateFileUpload: true, - ListContainer: true, - ListGrants: true, - ListRecycle: true, - Move: true, - RestoreRecycleItem: true, - Stat: true, - }, - ocsPermissions: PermissionRead | PermissionCreate | PermissionWrite | PermissionDelete, - } + role := NewSpaceEditorWithoutVersionsWithoutTrashbinRole() + role.Name = RoleSpaceEditorWithoutVersions + role.cS3ResourcePermissions.ListRecycle = true + role.cS3ResourcePermissions.RestoreRecycleItem = true + return role } // NewSpaceEditorWithoutTrashbinRole creates an editor role without list/restore resources in trashbin on a space. func NewSpaceEditorWithoutTrashbinRole() *Role { - return &Role{ - Name: RoleSpaceEditorWithoutTrashbin, - cS3ResourcePermissions: &provider.ResourcePermissions{ - CreateContainer: true, - Delete: true, - GetPath: true, - GetQuota: true, - InitiateFileDownload: true, - InitiateFileUpload: true, - ListContainer: true, - ListFileVersions: true, - ListGrants: true, - Move: true, - RestoreFileVersion: true, - Stat: true, - }, - ocsPermissions: PermissionRead | PermissionCreate | PermissionWrite | PermissionDelete, - } + role := NewSpaceEditorWithoutVersionsWithoutTrashbinRole() + role.Name = RoleSpaceEditorWithoutTrashbin + role.cS3ResourcePermissions.ListFileVersions = true + role.cS3ResourcePermissions.RestoreFileVersion = true + return role +} + +// NewSpaceEditorRole creates an editor role +func NewSpaceEditorRole() *Role { + role := NewSpaceEditorWithoutVersionsRole() + role.Name = RoleSpaceEditor + role.cS3ResourcePermissions.ListFileVersions = true + role.cS3ResourcePermissions.RestoreFileVersion = true + return role } // NewFileEditorRole creates a file-editor role diff --git a/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go b/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go index 39b94b6f772..0de2cc89860 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go @@ -4,7 +4,7 @@ package ctx // service boundaries. The "autoprop-" prefix causes the metadata interceptor // (internal/grpc/interceptors/metadata) to forward it automatically at every // gRPC hop, so no manual re-forwarding is required. -// The const rgrpc.AutoPropPrefix causes the cycle import +// Using rgrpc.AutoPropPrefix here would cause a cyclic import. const MFAOutgoingHeader = "autoprop-mfa-authenticated" // The corresponding HTTP header set by the proxy is "X-Multi-Factor-Authentication". diff --git a/vendor/github.com/owncloud/reva/v2/pkg/mime/mime.go b/vendor/github.com/owncloud/reva/v2/pkg/mime/mime.go index 60d172e7e2b..f8cfe4fb30a 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/mime/mime.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/mime/mime.go @@ -76,6 +76,7 @@ var mimeTypes = map[string]string{ "atx": "application/vnd.antix.game-component", "au": "audio/basic", "avi": "video/x-msvideo", + "avif": "image/avif", "aw": "application/applixware", "azf": "application/vnd.airzip.filesecure.azf", "azs": "application/vnd.airzip.filesecure.azs", @@ -92,7 +93,7 @@ var mimeTypes = map[string]string{ "blb": "application/x-blorb", "blorb": "application/x-blorb", "bmi": "application/vnd.bmi", - "bmp": "image/x-ms-bmp", + "bmp": "image/bmp", "book": "application/vnd.framemaker", "box": "application/vnd.previewsystems.box", "boz": "application/x-bzip2", @@ -164,6 +165,7 @@ var mimeTypes = map[string]string{ "cpp": "text/x-c", "cpt": "application/mac-compactpro", "cr2": "image/x-canon-cr2", + "cr3": "image/x-canon-cr3", "crd": "application/x-mscardfile", "crl": "application/pkix-crl", "crt": "application/x-x509-ca-cert", @@ -439,6 +441,7 @@ var mimeTypes = map[string]string{ "jsonld": "application/ld+json", "jsonml": "application/jsonml+json", "jsx": "text/jsx", + "jxl": "image/jxl", "k25": "image/x-kodak-k25", "kar": "audio/midi", "karbon": "application/vnd.kde.karbon", @@ -791,6 +794,7 @@ var mimeTypes = map[string]string{ "rtf": "text/rtf", "rtx": "text/richtext", "run": "application/x-makeself", + "rw2": "image/x-panasonic-rw2", "s": "text/x-asm", "s3m": "audio/s3m", "saf": "application/vnd.yamaha.smaf-audio", diff --git a/vendor/github.com/owncloud/reva/v2/pkg/storage/registry/spaces/spaces.go b/vendor/github.com/owncloud/reva/v2/pkg/storage/registry/spaces/spaces.go index d2b0cbc7fb7..1ea58a8f1dc 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/storage/registry/spaces/spaces.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/storage/registry/spaces/spaces.go @@ -196,13 +196,13 @@ func (r *registry) GetProvider(ctx context.Context, space *providerpb.StorageSpa continue } - // Filter out vault spaces if no storageId is provided if space.GetRoot().GetStorageId() != "" { if space.GetRoot().GetStorageId() != provider.ProviderID { continue } } else { - if strings.HasPrefix(sc.MountPoint, "/vault/") { + // Filter out vault spaces if no storageId is provided + if provider.ProviderID == utils.VaultStorageProviderID { continue } } @@ -401,7 +401,7 @@ func (r *registry) findProvidersForFilter(ctx context.Context, filters []*provid continue } // Filter out vault spaces if no storageId is provided - if storageId == "" && strings.HasPrefix(sc.MountPoint, "/vault/") { + if storageId == "" && provider.ProviderID == utils.VaultStorageProviderID { continue } spacePath, err = sc.SpacePath(currentUser, space) diff --git a/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go b/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go index 6e91c8a409f..1a42f20878b 100644 --- a/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go +++ b/vendor/github.com/owncloud/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go @@ -238,7 +238,7 @@ func (session *OcisSession) FinishUploadDecomposed(ctx context.Context) error { if !session.store.async || session.info.Size == 0 { // handle postprocessing synchronously err = session.Finalize(ctx) - session.Cleanup(err != nil, true, true, true) + session.Cleanup(err != nil, err == nil, true, true) if err != nil { log.Error().Err(err).Msg("failed to upload") return err diff --git a/vendor/modules.txt b/vendor/modules.txt index d0d267e7264..7291d268d76 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1316,8 +1316,8 @@ github.com/orcaman/concurrent-map # github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 ## explicit; go 1.18 github.com/owncloud/libre-graph-api-go -# github.com/owncloud/reva/v2 v2.0.0-20260422211312-0300dc8978e0 -## explicit; go 1.24.0 +# github.com/owncloud/reva/v2 v2.0.0-20260504165655-6f2258091fbf +## explicit; go 1.25.0 github.com/owncloud/reva/v2/cmd/revad/internal/grace github.com/owncloud/reva/v2/cmd/revad/runtime github.com/owncloud/reva/v2/internal/grpc/interceptors/appctx From 81399ea5d97c1bab7ae9dbfa6f60180c664c7efb Mon Sep 17 00:00:00 2001 From: Roman Perekhod <2403905@gmail.com> Date: Tue, 5 May 2026 12:22:13 +0200 Subject: [PATCH 11/12] deduplicate the requireMFA middeware on vault routes. fix AppendPlainToOpaque. --- .gitignore | 1 - ocis-pkg/mfa/mfa.go | 2 +- services/graph/pkg/config/service.go | 2 +- services/graph/pkg/service/v0/drives.go | 2 +- services/graph/pkg/service/v0/service.go | 17 ++++++++++------- services/proxy/pkg/middleware/mfa.go | 11 ++++++----- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 1a50be5c2be..d300ada6cdb 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,6 @@ protogen/buf.sha1.lock /third-party-licenses # misc -.agents/ /tmp go.work go.work.sum diff --git a/ocis-pkg/mfa/mfa.go b/ocis-pkg/mfa/mfa.go index 5770f408956..843c24d844a 100644 --- a/ocis-pkg/mfa/mfa.go +++ b/ocis-pkg/mfa/mfa.go @@ -7,7 +7,7 @@ import ( "net/http" ) -// MFAHeader is the header to be used across grpc and http services +// MFAHeader is the header to be used across http services // to forward the access token. const MFAHeader = "X-Multi-Factor-Authentication" diff --git a/services/graph/pkg/config/service.go b/services/graph/pkg/config/service.go index 6e9ee82bb15..d1eac383f0b 100644 --- a/services/graph/pkg/config/service.go +++ b/services/graph/pkg/config/service.go @@ -2,5 +2,5 @@ package config // Service defines the available service configuration. type Service struct { - Name string `yaml:"name" env:"GRAPH_SERVICE_NAME" desc:"The name of the service." introductionVersion:"Deledda"` + Name string `yaml:"-"` } diff --git a/services/graph/pkg/service/v0/drives.go b/services/graph/pkg/service/v0/drives.go index bdb0e943414..d80d5a11fa5 100644 --- a/services/graph/pkg/service/v0/drives.go +++ b/services/graph/pkg/service/v0/drives.go @@ -770,7 +770,7 @@ func (g Graph) ListStorageSpacesWithFilters(ctx context.Context, filters []*stor // force vault storage space if vault mode is enabled if middleware.IsVaultMode(ctx) { - utils.AppendPlainToOpaque(lReq.Opaque, "storage_id", utils.VaultStorageProviderID) + lReq.Opaque = utils.AppendPlainToOpaque(lReq.Opaque, "storage_id", utils.VaultStorageProviderID) } gatewayClient, err := g.gatewaySelector.Next() diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 16223ecd280..30816eec51b 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -203,8 +203,6 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx requireAdmin = options.RequireAdminMiddleware } - requireMFA := graphm.RequireMFA(options.Logger) - drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector) if err != nil { return svc, err @@ -225,7 +223,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx return svc, err } - graphRoutes := func(r chi.Router) { + graphRoutes := func(r chi.Router, drivesRequireMFA func(http.Handler) http.Handler) { r.Use(middleware.StripSlashes) r.Route("/v1beta1", func(r chi.Router) { r.Route("/me", func(r chi.Router) { @@ -236,7 +234,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx }) }) r.Route("/drives", func(r chi.Router) { - r.With(requireMFA).Get("/", svc.GetAllDrives(APIVersion_1_Beta_1)) + r.With(drivesRequireMFA).Get("/", svc.GetAllDrives(APIVersion_1_Beta_1)) r.Post("/", svc.CreateDriveV1Beta1) r.Route("/{driveID}", func(r chi.Router) { r.Get("/", svc.GetSingleDriveV1Beta1) @@ -332,7 +330,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx }) }) r.Route("/drives", func(r chi.Router) { - r.With(requireMFA).Get("/", svc.GetAllDrives(APIVersion_1)) + r.With(drivesRequireMFA).Get("/", svc.GetAllDrives(APIVersion_1)) r.Post("/", svc.CreateDrive) r.Route("/{driveID}", func(r chi.Router) { r.Patch("/", svc.UpdateDrive) @@ -397,14 +395,19 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx }) } - m.Route(options.Config.HTTP.Root, graphRoutes) + requireMFA := graphm.RequireMFA(options.Logger) + blankMW := func(next http.Handler) http.Handler { return next } + + m.Route(options.Config.HTTP.Root, func(r chi.Router) { + graphRoutes(r, requireMFA) + }) // Initialize the Vault routes if options.Config.EnableVaultMode { m.Route("/vault/graph", func(r chi.Router) { r.Use(requireMFA) r.Use(graphm.VaultModeMiddleware()) - graphRoutes(r) + graphRoutes(r, blankMW) }) } diff --git a/services/proxy/pkg/middleware/mfa.go b/services/proxy/pkg/middleware/mfa.go index 15e77d5f974..26665419d10 100644 --- a/services/proxy/pkg/middleware/mfa.go +++ b/services/proxy/pkg/middleware/mfa.go @@ -62,9 +62,6 @@ func (m MultiFactorAuthentication) ServeHTTP(w http.ResponseWriter, req *http.Re mfa.SetHeader(req, false) claims := oidc.FromContext(req.Context()) - // TODO: - // Should we set the "acr" to the claims "X-Access-Token" for the "aud":["reva"]? - // Should we get the claims from the "X-Access-Token"? if claims == nil { // No OIDC claims — request was authenticated via a non-OIDC method @@ -115,7 +112,7 @@ func (m MultiFactorAuthentication) ServeHTTP(w http.ResponseWriter, req *http.Re } func (m MultiFactorAuthentication) readMFAFromStore(userID string) bool { - records, err := m.store.Read("mfa:" + userID) + records, err := m.store.Read(key(userID)) if err != nil || len(records) == 0 { return false } @@ -124,7 +121,7 @@ func (m MultiFactorAuthentication) readMFAFromStore(userID string) bool { func (m MultiFactorAuthentication) writeMFAToStore(userID string) { if err := m.store.Write(µstore.Record{ - Key: "mfa:" + userID, + Key: key(userID), Value: []byte("true"), Expiry: mfaStoreTTL, }); err != nil { @@ -141,3 +138,7 @@ func (m MultiFactorAuthentication) containsMFA(value string) bool { } return false } + +func key(userID string) string { + return "mfa:" + userID +} From 76bd16b305cacf49e3b8bae587b256f33d1a426c Mon Sep 17 00:00:00 2001 From: Roman Perekhod <2403905@gmail.com> Date: Wed, 6 May 2026 10:27:57 +0200 Subject: [PATCH 12/12] added the changlog and comments --- changelog/unreleased/enhancement-vault-storage.md | 15 +++++++++++++++ go.mod | 2 +- go.sum | 4 ++-- ocis-pkg/mfa/mfa.go | 1 + services/graph/pkg/middleware/vault.go | 3 +++ vendor/modules.txt | 2 +- 6 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 changelog/unreleased/enhancement-vault-storage.md diff --git a/changelog/unreleased/enhancement-vault-storage.md b/changelog/unreleased/enhancement-vault-storage.md new file mode 100644 index 00000000000..edea2537ec7 --- /dev/null +++ b/changelog/unreleased/enhancement-vault-storage.md @@ -0,0 +1,15 @@ +Enhancement: Add vault storage with MFA-protected access + +Added a dedicated vault storage that can be protected with MFA. A separate +`storage-users-vault` service instance runs in vault mode and serves +`/vault/users` and `/vault/projects` mount points with a dedicated +`VaultStorageProviderID`. The `graph` service gained a new vault mode +(`GRAPH_ENABLE_VAULT_MODE`) that serves the vault API under the `/vault` +prefix. The storage registry now routes vault-specific requests exclusively to +the vault storage provider, preventing accidental access to vault spaces when +no explicit storage ID is provided. + +MFA status is propagated through gRPC metadata +and forwarded in HTTP headers for WOPI/collaboration flows. + +https://github.com/owncloud/ocis/pull/12108 diff --git a/go.mod b/go.mod index 3ea4a31791d..5d8fd48a070 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/open-policy-agent/opa v1.12.3 github.com/orcaman/concurrent-map v1.0.0 github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 - github.com/owncloud/reva/v2 v2.0.0-20260504165655-6f2258091fbf + github.com/owncloud/reva/v2 v2.0.0-20260506065108-b350cd1e8ea1 github.com/pkg/errors v0.9.1 github.com/pkg/xattr v0.4.12 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index c120b64c2ed..b331174ec91 100644 --- a/go.sum +++ b/go.sum @@ -742,8 +742,8 @@ github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HD github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 h1:JRidLTAKhnvyLMRtVtSF4lhBa0NSAOs6fof+d6JnKII= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245/go.mod h1:z61VMGAJRtR1nbgXWiNoCkxUXP1B3Je9rMuJbnGd+Og= -github.com/owncloud/reva/v2 v2.0.0-20260504165655-6f2258091fbf h1:q/T/PWppIAQA+TAnP8RBdfLNkDossVGGi5bGF/prz2E= -github.com/owncloud/reva/v2 v2.0.0-20260504165655-6f2258091fbf/go.mod h1:oc3sbqju0T4B+ZwXjhe0DOy4916AiAMlJzO6AO7m8ps= +github.com/owncloud/reva/v2 v2.0.0-20260506065108-b350cd1e8ea1 h1:ps23cQ/9iLaj3Cd9gD6791QKRAcP1waM+xHAiywylao= +github.com/owncloud/reva/v2 v2.0.0-20260506065108-b350cd1e8ea1/go.mod h1:oc3sbqju0T4B+ZwXjhe0DOy4916AiAMlJzO6AO7m8ps= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pablodz/inotifywaitgo v0.0.9 h1:njquRbBU7fuwIe5rEvtaniVBjwWzcpdUVptSgzFqZsw= diff --git a/ocis-pkg/mfa/mfa.go b/ocis-pkg/mfa/mfa.go index 843c24d844a..f2016d50b3d 100644 --- a/ocis-pkg/mfa/mfa.go +++ b/ocis-pkg/mfa/mfa.go @@ -58,6 +58,7 @@ func SetHeader(r *http.Request, mfa bool) { r.Header.Set(MFAHeader, "false") } +// IsMFAHeaderTrue checks if the MFA header is set to "true". func IsMFAHeaderTrue(r *http.Request) bool { return r.Header.Get(MFAHeader) == "true" } diff --git a/services/graph/pkg/middleware/vault.go b/services/graph/pkg/middleware/vault.go index 5dfd3450f8f..687cd8b8b53 100644 --- a/services/graph/pkg/middleware/vault.go +++ b/services/graph/pkg/middleware/vault.go @@ -9,15 +9,18 @@ type key int const vaultModeKey key = iota +// SetVaultMode sets the vault mode in the context. func SetVaultMode(ctx context.Context, enabled bool) context.Context { return context.WithValue(ctx, vaultModeKey, enabled) } +// IsVaultMode checks if the vault mode is enabled in the context. func IsVaultMode(ctx context.Context) bool { val, ok := ctx.Value(vaultModeKey).(bool) return val && ok } +// VaultModeMiddleware is a middleware that sets the vault mode in the context. func VaultModeMiddleware() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/vendor/modules.txt b/vendor/modules.txt index 7291d268d76..591971a35e3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1316,7 +1316,7 @@ github.com/orcaman/concurrent-map # github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 ## explicit; go 1.18 github.com/owncloud/libre-graph-api-go -# github.com/owncloud/reva/v2 v2.0.0-20260504165655-6f2258091fbf +# github.com/owncloud/reva/v2 v2.0.0-20260506065108-b350cd1e8ea1 ## explicit; go 1.25.0 github.com/owncloud/reva/v2/cmd/revad/internal/grace github.com/owncloud/reva/v2/cmd/revad/runtime