diff --git a/.gitignore b/.gitignore index 33e55761570..d300ada6cdb 100644 --- a/.gitignore +++ b/.gitignore @@ -64,4 +64,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/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/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..7a8551d9748 --- /dev/null +++ b/deployments/examples/ocis_full/vault-storage.yml @@ -0,0 +1,38 @@ +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}:${OCIS_DOCKER_TAG} + 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: + # 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/go.mod b/go.mod index 669816983e9..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-20260324082555-823c2f1c2593 + 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 b7c43bda1ae..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-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-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 e75a46002a7..f2016d50b3d 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" @@ -57,3 +57,8 @@ 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/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/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/services/gateway/pkg/config/config.go b/services/gateway/pkg/config/config.go index 92f44c19f4e..2065ef2aebb 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 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/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 44dcda07348..7ac686cb98d 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 }, }, }, + cfg.StorageUsersVaultEndpoint: { + // 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..29071b86459 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 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:"-"` } @@ -50,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, 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/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/middleware/auth.go b/services/graph/pkg/middleware/auth.go index 3e382ec8d17..e2847a06ec0 100644 --- a/services/graph/pkg/middleware/auth.go +++ b/services/graph/pkg/middleware/auth.go @@ -93,6 +93,15 @@ 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. + mfaVal := "false" + if mfa.Has(ctx) { + mfaVal = "true" + } + ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAOutgoingHeader, mfaVal) + next.ServeHTTP(w, r.WithContext(ctx)) }) } 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..687cd8b8b53 --- /dev/null +++ b/services/graph/pkg/middleware/vault.go @@ -0,0 +1,30 @@ +package middleware + +import ( + "context" + "net/http" +) + +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) { + 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..73b101b52a4 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. @@ -154,13 +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"), + }, + } - res, err := gatewayClient.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{ - Filters: filters, - }) + // force vault storage space if vault mode is enabled + if middleware.IsVaultMode(ctx) { + listReq.Opaque = utils.AppendPlainToOpaque(listReq.Opaque, "storage_id", utils.VaultStorageProviderID) + } + + 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/drives.go b/services/graph/pkg/service/v0/drives.go index c4657b32054..d80d5a11fa5 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) { + lReq.Opaque = 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..30816eec51b 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -223,9 +223,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, drivesRequireMFA func(http.Handler) http.Handler) { 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 +234,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(drivesRequireMFA).Get("/", svc.GetAllDrives(APIVersion_1_Beta_1)) r.Post("/", svc.CreateDriveV1Beta1) r.Route("/{driveID}", func(r chi.Router) { r.Get("/", svc.GetSingleDriveV1Beta1) @@ -331,7 +330,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx }) }) r.Route("/drives", func(r chi.Router) { - r.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) @@ -394,8 +393,24 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx }) }) }) + } + + 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, blankMW) + }) + } + _ = 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") return nil 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..9fe55493f36 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 g.config.EnableVaultMode && middleware.IsVaultMode(ctx) { + storageUsersAddress = g.config.Spaces.StorageUsersVaultAddress + } + 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..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), @@ -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..c545fda70fc 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. 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/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..22de58c27a9 100644 --- a/services/proxy/pkg/middleware/create_home.go +++ b/services/proxy/pkg/middleware/create_home.go @@ -10,9 +10,12 @@ 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" "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" ) @@ -28,6 +31,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{}, } } @@ -38,6 +42,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. } @@ -50,21 +55,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 { - 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 || 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)) } client, err := m.revaGatewaySelector.Next() @@ -87,6 +94,33 @@ func (m *createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { 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 && 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()) + 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) + + cpsRes, err := client.CreateHome(vctx, 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.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.Store(vaultKey, struct{}{}) + default: + m.logger.Error().Interface("userID", u.GetId().GetOpaqueId()).Interface("status", cpsRes.GetStatus()).Msg("vault personal space creation failed") + } + } + } } m.next.ServeHTTP(w, req) diff --git a/services/proxy/pkg/middleware/mfa.go b/services/proxy/pkg/middleware/mfa.go index f41c0b97019..26665419d10 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 @@ -49,10 +63,32 @@ func (m MultiFactorAuthentication) ServeHTTP(w http.ResponseWriter, req *http.Re claims := oidc.FromContext(req.Context()) + 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 +99,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(key(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: key(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 @@ -74,3 +138,7 @@ func (m MultiFactorAuthentication) containsMFA(value string) bool { } return false } + +func key(userID string) string { + return "mfa:" + userID +} diff --git a/services/proxy/pkg/middleware/options.go b/services/proxy/pkg/middleware/options.go index 503273a564e..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. @@ -69,6 +73,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 @@ -215,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) { @@ -243,6 +256,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..51859b7d5e5 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:"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:"-"` } @@ -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 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"` } 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/config.go b/services/storage-users/pkg/revaconfig/config.go index 2f47e4c5fda..f5f08fff599 100644 --- a/services/storage-users/pkg/revaconfig/config.go +++ b/services/storage-users/pkg/revaconfig/config.go @@ -116,11 +116,25 @@ 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 { + // 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{}) + 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/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/services/thumbnails/pkg/service/grpc/v0/service.go b/services/thumbnails/pkg/service/grpc/v0/service.go index 44e16a9a935..888178b334b 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" @@ -28,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. @@ -140,7 +142,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 +225,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 +274,17 @@ 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) + + // 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(ctxpkg.MFAOutgoingHeader); ok && v != "" { + outCtx = metadata.AppendToOutgoingContext(outCtx, ctxpkg.MFAOutgoingHeader, v) + } + } ref, err := storagespace.ParseReference(path) if err != nil { @@ -289,7 +300,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..d770494f05f 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,16 @@ func (s CS3) Get(ctx context.Context, path string) (io.ReadCloser, error) { } ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, auth) + + // 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.MFAOutgoingHeader); ok && v != "" { + ctx = metadata.AppendToOutgoingContext(ctx, revactx.MFAOutgoingHeader, v) + } + } + 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..ed116336cd0 100644 --- a/services/webdav/pkg/service/v0/service.go +++ b/services/webdav/pkg/service/v0/service.go @@ -21,9 +21,11 @@ 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" + "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" @@ -228,7 +230,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 +328,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 +378,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 +423,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 +499,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(mfa.MFAHeader) == "true" { + mfaVal = "true" + } + return gmmetadata.Set(r.Context(), revactx.MFAOutgoingHeader, 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..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 @@ -20,6 +20,7 @@ package auth import ( "context" + "slices" "sync" "time" @@ -27,6 +28,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 +38,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" ) @@ -62,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) { @@ -154,6 +157,14 @@ 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") + return mfaResponse(ctx, req, info) + } + } span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId)) @@ -243,6 +254,14 @@ 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") + return status.Errorf(codes.PermissionDenied, "MFA required to access vault storage") + } + } wrapped := newWrappedServerStream(ctx, ss) span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId)) 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..98cd97e3a82 --- /dev/null +++ b/vendor/github.com/owncloud/reva/v2/internal/grpc/interceptors/auth/mfa.go @@ -0,0 +1,73 @@ +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 + 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/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/interceptors/auth/auth.go b/vendor/github.com/owncloud/reva/v2/internal/http/interceptors/auth/auth.go index f7b494c684d..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 @@ -354,6 +354,14 @@ 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 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/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/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 307bed917f2..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,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 { + // 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", "", "") + 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..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,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 to outside of 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/auth/manager/oidc/oidc.go b/vendor/github.com/owncloud/reva/v2/pkg/auth/manager/oidc/oidc.go index bbe950dc191..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 @@ -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,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/juliangruber/go-intersect" - "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "golang.org/x/oauth2" ) 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/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 new file mode 100644 index 00000000000..0de2cc89860 --- /dev/null +++ b/vendor/github.com/owncloud/reva/v2/pkg/ctx/mfactx.go @@ -0,0 +1,11 @@ +package ctx + +// 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. +// 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". +const MFAHeader = "X-Multi-Factor-Authentication" 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/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 ac586e96d39..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 @@ -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 } + + if space.GetRoot().GetStorageId() != "" { + if space.GetRoot().GetStorageId() != provider.ProviderID { + continue + } + } else { + // Filter out vault spaces if no storageId is provided + if provider.ProviderID == utils.VaultStorageProviderID { + 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 == "" && provider.ProviderID == utils.VaultStorageProviderID { + 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..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 @@ -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, @@ -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/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..591971a35e3 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-20260324082555-823c2f1c2593 -## explicit; go 1.24.0 +# 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 github.com/owncloud/reva/v2/internal/grpc/interceptors/appctx