Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ protogen/buf.sha1.lock
/third-party-licenses

# misc
.agents/
/tmp
go.work
go.work.sum
.env
.envrc
CLAUDE.md
.claude/
GEMINI.md
Comment thread
2403905 marked this conversation as resolved.
.agents/
2 changes: 0 additions & 2 deletions .make/go.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
2403905 marked this conversation as resolved.
Expand All @@ -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)
7 changes: 6 additions & 1 deletion deployments/examples/ocis_full/.env
Original file line number Diff line number Diff line change
Expand Up @@ -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 ##

Expand Down Expand Up @@ -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:-}
37 changes: 37 additions & 0 deletions deployments/examples/ocis_full/vault-storage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
services:
ocis:
environment:
OCIS_MFA_ENABLED: true
NATS_NATS_HOST: 0.0.0.0
SETTINGS_GRPC_ADDR: ocis:9191
PROXY_CREATE_VAULT_HOME: true
GRAPH_ENABLE_VAULT_MODE: true

storage-users-vault:
image: ${OCIS_DOCKER_IMAGE:-owncloud/ocis}:${OCIS_DOCKER_TAG:-latest}
Comment thread
2403905 marked this conversation as resolved.
Outdated
networks:
ocis-net:
depends_on:
ocis:
condition: service_started
command: ["storage-users", "server"]
environment:
OCIS_LOG_LEVEL: debug
OCIS_GATEWAY_GRPC_ADDR: ocis:9142
STORAGE_USERS_ENABLE_VAULT_MODE: true
STORAGE_USERS_SERVICE_NAME: storage-users-vault
STORAGE_USERS_GRPC_ADDR: storage-users-vault:9170
STORAGE_USERS_HTTP_ADDR: storage-users-vault:9168
STORAGE_USERS_DATA_SERVER_URL: http://storage-users-vault:9168/data
STORAGE_USERS_DEBUG_ADDR: storage-users-vault:9169
STORAGE_USERS_OCIS_ROOT: /var/lib/ocis/storage/users-vault
STORAGE_USERS_EVENTS_CONSUMER_GROUP: vault-dcfs
MICRO_REGISTRY_ADDRESS: ocis:9233
OCIS_EVENTS_ENDPOINT: ocis:9233
OCIS_CACHE_STORE_NODES: ocis:9233
volumes:
- ocis-data:/var/lib/ocis
- ocis-config:/etc/ocis
Comment thread
2403905 marked this conversation as resolved.
Outdated
logging:
driver: ${LOG_DRIVER:-local}
restart: always
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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-20260422211312-0300dc8978e0
github.com/pkg/errors v0.9.1
github.com/pkg/xattr v0.4.12
github.com/prometheus/client_golang v1.23.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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-20260422211312-0300dc8978e0 h1:hhbhzWdBfMoXKLyFRkrdEggxGD3jarE4IAt/O/QRzrA=
github.com/owncloud/reva/v2 v2.0.0-20260422211312-0300dc8978e0/go.mod h1:+rCy6oGYb2/qs5gmQa8y/pHARw634vB73MZGDY2SBIQ=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pablodz/inotifywaitgo v0.0.9 h1:njquRbBU7fuwIe5rEvtaniVBjwWzcpdUVptSgzFqZsw=
Expand Down
4 changes: 4 additions & 0 deletions services/collaboration/pkg/connector/contentconnector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Copy link
Copy Markdown
Contributor Author

@2403905 2403905 Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jvillafanez Please use mfa.SetHeader(httpReq, true) instead.

}
return httpReq, nil
}

Expand Down
8 changes: 8 additions & 0 deletions services/collaboration/pkg/middleware/wopicontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()).
Expand Down
6 changes: 6 additions & 0 deletions services/collaboration/pkg/service/grpc/v0/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"net/url"
"path"
"slices"
"strconv"
"strings"

Expand All @@ -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"
Expand Down Expand Up @@ -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 != "" {
Expand Down
16 changes: 16 additions & 0 deletions services/gateway/pkg/revaconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,22 @@ func spacesProviders(cfg *config.Config, logger log.Logger) map[string]map[strin
},
},
},
"com.owncloud.api.storage-users-vault": {
// Use the dedicated storage provider for vault
"providerid": utils.VaultStorageProviderID,
"spaces": map[string]interface{}{
"personal": map[string]interface{}{
// The mount point must have the "vault/" prefix to be picked up by the vault storage provider
"mount_point": "/vault/users",
"path_template": "/vault/users/{{.Space.Owner.Id.OpaqueId}}",
},
"project": map[string]interface{}{
// The mount point must have the "vault/" prefix to be picked up by the vault storage provider
"mount_point": "/vault/projects",
"path_template": "/vault/projects/{{.Space.Name}}",
},
},
},
cfg.StorageSharesEndpoint: {
"providerid": utils.ShareStorageProviderID,
"spaces": map[string]interface{}{
Expand Down
2 changes: 2 additions & 0 deletions services/graph/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type Config struct {

Validation Validation `yaml:"validation"`

EnableVaultMode bool `yaml:"enable_vault_mode" env:"GRAPH_ENABLE_VAULT_MODE" desc:"Enable vault mode for the graph service runned in addition to the regular graph service. Required the running the storage-users-vault additional service." introductionVersion:"Deledda"`
Comment thread
2403905 marked this conversation as resolved.
Outdated

Context context.Context `yaml:"-"`
}

Expand Down
2 changes: 1 addition & 1 deletion services/graph/pkg/config/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package config

// Service defines the available service configuration.
type Service struct {
Name string `yaml:"-"`
Name string `yaml:"name" env:"GRAPH_SERVICE_NAME" desc:"The name of the service." introductionVersion:"Deledda"`
Comment thread
2403905 marked this conversation as resolved.
Outdated
}
9 changes: 9 additions & 0 deletions services/graph/pkg/middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}
Expand Down
23 changes: 23 additions & 0 deletions services/graph/pkg/middleware/mfa.go
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
2403905 marked this conversation as resolved.
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)
})
}
}
27 changes: 27 additions & 0 deletions services/graph/pkg/middleware/vault.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package middleware

import (
"context"
"net/http"
)

type key int

const vaultModeKey key = iota

func SetVaultMode(ctx context.Context, enabled bool) context.Context {
return context.WithValue(ctx, vaultModeKey, enabled)
}

func IsVaultMode(ctx context.Context) bool {
val, ok := ctx.Value(vaultModeKey).(bool)
return val && ok
}

func VaultModeMiddleware() func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r.WithContext(SetVaultMode(r.Context(), true)))
})
}
}
19 changes: 13 additions & 6 deletions services/graph/pkg/service/v0/driveitems.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down
38 changes: 38 additions & 0 deletions services/graph/pkg/service/v0/driveitems_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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{
Expand Down
Loading