diff --git a/go.mod b/go.mod index 2e48d3e36..362755a51 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/open-policy-agent/opa v1.10.1 github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76 - github.com/opencloud-eu/reva/v2 v2.39.3 + github.com/opencloud-eu/reva/v2 v2.40.1 github.com/opensearch-project/opensearch-go/v4 v4.5.0 github.com/orcaman/concurrent-map v1.0.0 github.com/pkg/errors v0.9.1 @@ -372,9 +372,9 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.etcd.io/etcd/api/v3 v3.6.5 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect - go.etcd.io/etcd/client/v3 v3.6.5 // indirect + go.etcd.io/etcd/api/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/v3 v3.6.6 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect diff --git a/go.sum b/go.sum index d1ca6d8ae..c2cf34190 100644 --- a/go.sum +++ b/go.sum @@ -963,8 +963,8 @@ github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9 h1:dIft github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9/go.mod h1:JWyDC6H+5oZRdUJUgKuaye+8Ph5hEs6HVzVoPKzWSGI= github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76 h1:vD/EdfDUrv4omSFjrinT8Mvf+8D7f9g4vgQ2oiDrVUI= github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q= -github.com/opencloud-eu/reva/v2 v2.39.3 h1:/9NW08Bpy1GaNAPo8HrlyT21Flj8uNnOUyWLud1ehGc= -github.com/opencloud-eu/reva/v2 v2.39.3/go.mod h1:kkGiMeEVR59VjDsmWIczWqRcwK8cy9ogTd/u802U3NI= +github.com/opencloud-eu/reva/v2 v2.40.1 h1:QwMkbGMhwDSwfk2WxbnTpIig2BugPBaVFjWcy2DSU3U= +github.com/opencloud-eu/reva/v2 v2.40.1/go.mod h1:DGH08n2mvtsQLkt8o15FV6m51FwSJJGhjR8Ty+iIJww= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -1275,12 +1275,12 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= -go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA= -go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= -go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8= -go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= -go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U= -go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= +go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ= +go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4= +go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs= +go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI= +go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec= +go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/pkg/version/version.go b/pkg/version/version.go index 7884912ba..4943af61c 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -16,7 +16,7 @@ var ( // LatestTag is the latest released version plus the dev meta version. // Will be overwritten by the release pipeline // Needs a manual change for every tagged release - LatestTag = "4.0.0-rc.2+dev" + LatestTag = "4.0.0-rc.3+dev" // Date indicates the build date. // This has been removed, it looks like you can only replace static strings with recent go versions diff --git a/services/graph/pkg/identity/backend.go b/services/graph/pkg/identity/backend.go index ab9ca462e..93684311d 100644 --- a/services/graph/pkg/identity/backend.go +++ b/services/graph/pkg/identity/backend.go @@ -133,6 +133,10 @@ func CreateUserModelFromCS3(u *cs3user.User) *libregraph.User { OnPremisesSamAccountName: u.GetUsername(), Id: &u.GetId().OpaqueId, } + if u.GetId().GetType() == cs3user.UserType_USER_TYPE_FEDERATED { + ocmUserId := u.GetId().GetOpaqueId() + "@" + u.GetId().GetIdp() + user.Id = &ocmUserId + } return user } diff --git a/services/graph/pkg/identity/cache/cache.go b/services/graph/pkg/identity/cache/cache.go index 3bd13bf44..fe1fd5b0c 100644 --- a/services/graph/pkg/identity/cache/cache.go +++ b/services/graph/pkg/identity/cache/cache.go @@ -2,6 +2,8 @@ package cache import ( "context" + "errors" + "strings" "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -133,6 +135,20 @@ func (cache IdentityCache) GetAcceptedUser(ctx context.Context, userid string) ( return *identity.CreateUserModelFromCS3(u), nil } +func getIDAndMeshProvider(user string) (id, provider string, err error) { + last := strings.LastIndex(user, "@") + if last == -1 { + return "", "", errors.New("not in the form @") + } + if len(user[:last]) == 0 { + return "", "", errors.New("empty id") + } + if len(user[last+1:]) == 0 { + return "", "", errors.New("empty provider") + } + return user[:last], user[last+1:], nil +} + func (cache IdentityCache) GetAcceptedCS3User(ctx context.Context, userid string) (*cs3User.User, error) { var user *cs3user.User if item := cache.users.Get(userid); item == nil { @@ -140,8 +156,14 @@ func (cache IdentityCache) GetAcceptedCS3User(ctx context.Context, userid string if err != nil { return nil, errorcode.New(errorcode.GeneralException, err.Error()) } + id, provider, err := getIDAndMeshProvider(userid) + if err != nil { + return nil, errorcode.New(errorcode.InvalidRequest, err.Error()) + } cs3UserID := &cs3User.UserId{ - OpaqueId: userid, + Idp: provider, + OpaqueId: id, + Type: cs3User.UserType_USER_TYPE_FEDERATED, } user, err = revautils.GetAcceptedUserWithContext(ctx, cs3UserID, gatewayClient) if err != nil { diff --git a/services/graph/pkg/service/v0/users_test.go b/services/graph/pkg/service/v0/users_test.go index c9ac5e174..5db85f90f 100644 --- a/services/graph/pkg/service/v0/users_test.go +++ b/services/graph/pkg/service/v0/users_test.go @@ -1354,6 +1354,7 @@ var _ = Describe("Users", func() { Username: "federated", Id: &userv1beta1.UserId{ OpaqueId: "federated", + Idp: "provider", Type: userv1beta1.UserType_USER_TYPE_FEDERATED, }, }, @@ -1377,7 +1378,7 @@ var _ = Describe("Users", func() { Expect(err).ToNot(HaveOccurred()) Expect(len(res.Value)).To(Equal(1)) - Expect(res.Value[0].GetId()).To(Equal("federated")) + Expect(res.Value[0].GetId()).To(Equal("federated@provider")) Expect(res.Value[0].GetUserType()).To(Equal("Federated")) }) It("does not list federated users when filtering for 'Member' users", func() { diff --git a/services/graph/pkg/service/v0/utils.go b/services/graph/pkg/service/v0/utils.go index 86de9919c..106c7b6ec 100644 --- a/services/graph/pkg/service/v0/utils.go +++ b/services/graph/pkg/service/v0/utils.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "fmt" "io" "net/http" "reflect" @@ -106,7 +107,8 @@ func userIdToIdentity(ctx context.Context, cache cache.IdentityCache, tennantId, // federatedIdToIdentity looks the user for the supplied id using the cache and returns it // as a libregraph.Identity -func federatedIdToIdentity(ctx context.Context, cache cache.IdentityCache, userID string) (libregraph.Identity, error) { +func federatedIdToIdentity(ctx context.Context, cache cache.IdentityCache, cs3UserID *cs3User.UserId) (libregraph.Identity, error) { + userID := fmt.Sprintf("%s@%s", cs3UserID.GetOpaqueId(), cs3UserID.GetIdp()) identity := libregraph.Identity{ Id: libregraph.PtrString(userID), LibreGraphUserType: libregraph.PtrString("Federated"), @@ -123,7 +125,7 @@ func federatedIdToIdentity(ctx context.Context, cache cache.IdentityCache, userI // as a libregraph.Identity. Skips the user lookup if the id type is USER_TYPE_SPACE_OWNER func cs3UserIdToIdentity(ctx context.Context, cache cache.IdentityCache, cs3UserID *cs3User.UserId) (libregraph.Identity, error) { if cs3UserID.GetType() == cs3User.UserType_USER_TYPE_FEDERATED { - return federatedIdToIdentity(ctx, cache, cs3UserID.GetOpaqueId()) + return federatedIdToIdentity(ctx, cache, cs3UserID) } if cs3UserID.GetType() != cs3User.UserType_USER_TYPE_SPACE_OWNER { return userIdToIdentity(ctx, cache, cs3UserID.GetTenantId(), cs3UserID.GetOpaqueId()) diff --git a/tests/acceptance/TestHelpers/GraphHelper.php b/tests/acceptance/TestHelpers/GraphHelper.php index 27432b658..bda24330d 100644 --- a/tests/acceptance/TestHelpers/GraphHelper.php +++ b/tests/acceptance/TestHelpers/GraphHelper.php @@ -120,13 +120,12 @@ class GraphHelper { } /** - * Federated users have a base64 encoded string of {remoteid}@{provider} as their id - * This regex matches only non empty base64 encoded strings + * Federated users have a string of {userid}@{provider} as their id * * @return string */ public static function getFederatedUserRegex(): string { - return '(?=(.{4})*$)[A-Za-z0-9+/]*={0,2}$'; + return '^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}@https?://.+$'; } /** diff --git a/tests/acceptance/bootstrap/FeatureContext.php b/tests/acceptance/bootstrap/FeatureContext.php index 09413a7b0..7f0cfcebe 100644 --- a/tests/acceptance/bootstrap/FeatureContext.php +++ b/tests/acceptance/bootstrap/FeatureContext.php @@ -2440,14 +2440,6 @@ class FeatureContext extends BehatVariablesContext { ], "parameter" => [] ], - [ - "code" => "%identities_issuer_id_pattern%", - "function" => [ - __NAMESPACE__ . '\TestHelpers\GraphHelper', - "getFederatedUserRegex" - ], - "parameter" => [] - ], [ "code" => "%uuidv4_pattern%", "function" => [ diff --git a/tests/acceptance/bootstrap/SharingNgContext.php b/tests/acceptance/bootstrap/SharingNgContext.php index 1229e78ba..6555a2702 100644 --- a/tests/acceptance/bootstrap/SharingNgContext.php +++ b/tests/acceptance/bootstrap/SharingNgContext.php @@ -327,12 +327,11 @@ class SharingNgContext implements Context { if ($shareType === "user") { $shareeId = $this->featureContext->getAttributeOfCreatedUser($sharee, 'id'); if ($federatedShare) { - $shareeId = ( - $this->featureContext->ocmContext->getAcceptedUserByName( - $user, - $sharee - ) - )['user_id']; + $federatedUser = $this->featureContext->ocmContext->getAcceptedUserByName( + $user, + $sharee + ); + $shareeId = $federatedUser['user_id'] . "@" . $federatedUser['idp']; } } elseif ($shareType === "group") { $shareeId = $this->featureContext->getAttributeOfCreatedGroup($sharee, 'id'); @@ -403,7 +402,11 @@ class SharingNgContext implements Context { if ($shareType === "user") { $shareeId = $this->featureContext->getAttributeOfCreatedUser($sharee, 'id'); if ($federatedShare) { - $shareeId = ($this->featureContext->ocmContext->getAcceptedUserByName($user, $sharee))['user_id']; + $federatedUser = $this->featureContext->ocmContext->getAcceptedUserByName( + $user, + $sharee + ); + $shareeId = $federatedUser['user_id'] . "@" . $federatedUser['idp']; } } elseif ($shareType === "group") { $shareeId = $this->featureContext->getAttributeOfCreatedGroup($sharee, 'id'); diff --git a/tests/acceptance/features/apiOcm/ocmEndpoint.feature b/tests/acceptance/features/apiOcm/ocmEndpoint.feature index ebd159c5b..1a2b8059e 100644 --- a/tests/acceptance/features/apiOcm/ocmEndpoint.feature +++ b/tests/acceptance/features/apiOcm/ocmEndpoint.feature @@ -26,7 +26,7 @@ Feature: ocm well-known URI "const": true }, "apiVersion": { - "const": "1.1.0" + "const": "1.2.0" }, "endPoint": { "pattern": "^%base_url%/ocm" @@ -73,10 +73,24 @@ Feature: ocm well-known URI }, "capabilities": { "type": "array", - "minItems": 1, - "maxItems": 1, + "minItems": 4, + "maxItems": 4, + "uniqueItems": true, "items": { - "const": "/invite-accepted" + "oneOf": [ + { + "const": "invites" + }, + { + "const": "webdav-uri" + }, + { + "const": "protocol-object" + }, + { + "const": "invite-wayf" + } + ] } } } diff --git a/tests/acceptance/features/apiOcm/searchFederationUsers.feature b/tests/acceptance/features/apiOcm/searchFederationUsers.feature index fc461a766..4b5158da2 100755 --- a/tests/acceptance/features/apiOcm/searchFederationUsers.feature +++ b/tests/acceptance/features/apiOcm/searchFederationUsers.feature @@ -68,7 +68,7 @@ Feature: search federation users }, "issuerAssignedId": { "type": "string", - "pattern": "^%identities_issuer_id_pattern%$" + "pattern": "^%federated_user_id_pattern%$" } } } @@ -130,7 +130,7 @@ Feature: search federation users }, "issuerAssignedId": { "type": "string", - "pattern": "^%identities_issuer_id_pattern%$" + "pattern": "^%federated_user_id_pattern%$" } } } @@ -198,7 +198,7 @@ Feature: search federation users }, "issuerAssignedId": { "type": "string", - "pattern": "^%identities_issuer_id_pattern%$" + "pattern": "^%federated_user_id_pattern%$" } } } @@ -260,7 +260,7 @@ Feature: search federation users }, "issuerAssignedId": { "type": "string", - "pattern": "^%identities_issuer_id_pattern%$" + "pattern": "^%federated_user_id_pattern%$" } } } @@ -484,7 +484,7 @@ Feature: search federation users }, "issuerAssignedId": { "type": "string", - "pattern": "^%identities_issuer_id_pattern%$" + "pattern": "^%uuidv4_pattern%$" } } } @@ -549,7 +549,7 @@ Feature: search federation users }, "issuerAssignedId": { "type": "string", - "pattern": "^%identities_issuer_id_pattern%$" + "pattern": "^%uuidv4_pattern%$" } } } @@ -696,7 +696,7 @@ Feature: search federation users }, "issuerAssignedId": { "type": "string", - "pattern": "^%identities_issuer_id_pattern%$" + "pattern": "^%uuidv4_pattern%$" } } } diff --git a/vendor/github.com/opencloud-eu/reva/v2/cmd/revad/runtime/runtime.go b/vendor/github.com/opencloud-eu/reva/v2/cmd/revad/runtime/runtime.go index 2ae269195..c0ebada1d 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/cmd/revad/runtime/runtime.go +++ b/vendor/github.com/opencloud-eu/reva/v2/cmd/revad/runtime/runtime.go @@ -62,16 +62,9 @@ func RunWithOptions(mainConf map[string]interface{}, pidFile string, opts ...Opt type coreConf struct { MaxCPUs string `mapstructure:"max_cpus"` - TracingEnabled bool `mapstructure:"tracing_enabled"` - TracingInsecure bool `mapstructure:"tracing_insecure"` - TracingExporter string `mapstructure:"tracing_exporter"` - TracingEndpoint string `mapstructure:"tracing_endpoint"` - TracingCollector string `mapstructure:"tracing_collector"` + TracesExporter string `mapstructure:"traces_exporter"` TracingServiceName string `mapstructure:"tracing_service_name"` - // TracingService specifies the service. i.e OpenCensus, OpenTelemetry, OpenTracing... - TracingService string `mapstructure:"tracing_service"` - GracefulShutdownTimeout int `mapstructure:"graceful_shutdown_timeout"` } @@ -149,20 +142,8 @@ func initServers(mainConf map[string]interface{}, log *zerolog.Logger, tp trace. } func initTracing(conf *coreConf) trace.TracerProvider { - if conf.TracingEnabled { - opts := []rtrace.Option{ - rtrace.WithExporter(conf.TracingExporter), - rtrace.WithEndpoint(conf.TracingEndpoint), - rtrace.WithCollector(conf.TracingCollector), - rtrace.WithServiceName(conf.TracingServiceName), - } - if conf.TracingEnabled { - opts = append(opts, rtrace.WithEnabled()) - } - if conf.TracingInsecure { - opts = append(opts, rtrace.WithInsecure()) - } - tp := rtrace.NewTracerProvider(opts...) + if conf.TracesExporter != "none" && conf.TracesExporter != "" { + tp := rtrace.NewTracerProvider(conf.TracingServiceName, conf.TracesExporter) rtrace.SetDefaultTracerProvider(tp) return tp } @@ -281,11 +262,9 @@ func parseCoreConfOrDie(v interface{}) *coreConf { // tracing defaults to enabled if not explicitly configured if v == nil { - c.TracingEnabled = true - c.TracingEndpoint = "localhost:6831" - } else if _, ok := v.(map[string]interface{})["tracing_enabled"]; !ok { - c.TracingEnabled = true - c.TracingEndpoint = "localhost:6831" + c.TracesExporter = "console" + } else if _, ok := v.(map[string]interface{})["traces_exporter"]; !ok { + c.TracesExporter = "console" } return c diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/ocminvitemanager/ocminvitemanager.go b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/ocminvitemanager/ocminvitemanager.go index 1f7286c15..70d4b4ecb 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/ocminvitemanager/ocminvitemanager.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/ocminvitemanager/ocminvitemanager.go @@ -174,9 +174,9 @@ func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInvite remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{ Token: req.InviteToken.GetToken(), RecipientProvider: s.conf.ProviderDomain, - // The UserID is only a string here. To not loose the IDP information we use the FederatedID encoding - // i.e. base64(UserID@IDP) - UserID: ocmuser.FederatedID(user.GetId(), "").GetOpaqueId(), + // The UserID is only a string here. To not lose the IDP information we use the LocalUserFederatedID encoding + // i.e. UserID@IDP + UserID: ocmuser.FormatOCMUser(user.GetId()), Email: user.GetMail(), Name: user.GetDisplayName(), }) @@ -217,6 +217,9 @@ func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInvite OpaqueId: remoteUser.UserID, } + // we need to use a unique identifier for federated users + remoteUserID = ocmuser.LocalUserFederatedID(remoteUserID, "") + if err := s.repo.AddRemoteUser(ctx, user.Id, &userpb.User{ Id: remoteUserID, Mail: remoteUser.Email, @@ -276,6 +279,8 @@ func (s *service) AcceptInvite(ctx context.Context, req *invitepb.AcceptInviteRe } remoteUser := req.GetRemoteUser() + // we need to use a unique identifier for federated users + remoteUser.Id = ocmuser.LocalUserFederatedID(remoteUser.Id, "") if err := s.repo.AddRemoteUser(ctx, token.GetUserId(), remoteUser); err != nil { if !errors.Is(err, invite.ErrUserAlreadyAccepted) { diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/ocmshareprovider/ocmshareprovider.go b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/ocmshareprovider/ocmshareprovider.go index e594b3c7a..77ca2b948 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/ocmshareprovider/ocmshareprovider.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/ocmshareprovider/ocmshareprovider.go @@ -324,11 +324,11 @@ func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareReq // 2.b replace outgoing user ids with ocm user ids // unpack the federated user id - shareWith := ocmuser.FormatOCMUser(ocmuser.RemoteID(req.GetGrantee().GetUserId())) + shareWith := ocmuser.FormatOCMUser(ocmuser.LocalUserFederatedID(req.GetGrantee().GetUserId(), "")) - // wrap the local user id in a federated user id - owner := ocmuser.FormatOCMUser(ocmuser.FederatedID(info.Owner, s.conf.ProviderDomain)) - sender := ocmuser.FormatOCMUser(ocmuser.FederatedID(user.Id, s.conf.ProviderDomain)) + // wrap the local user id in a local federated user id + owner := ocmuser.FormatOCMUser(ocmuser.LocalUserFederatedID(info.Owner, s.conf.ProviderDomain)) + sender := ocmuser.FormatOCMUser(ocmuser.LocalUserFederatedID(user.Id, s.conf.ProviderDomain)) newShareReq := &client.NewShareRequest{ ShareWith: shareWith, diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/client.go b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/client.go new file mode 100644 index 000000000..d6dc0e3f4 --- /dev/null +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/client.go @@ -0,0 +1,148 @@ +// Copyright 2018-2025 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocmd + +import ( + "context" + "crypto/tls" + "encoding/json" + "io" + "net/http" + "net/url" + "time" + + "github.com/opencloud-eu/reva/v2/internal/http/services/wellknown" + "github.com/opencloud-eu/reva/v2/pkg/appctx" + "github.com/opencloud-eu/reva/v2/pkg/errtypes" + "github.com/pkg/errors" +) + +// OCMClient is the client for an OCM provider. +type OCMClient struct { + client *http.Client +} + +// NewClient returns a new OCMClient. +func NewClient(timeout time.Duration, insecure bool) *OCMClient { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}, + } + return &OCMClient{ + client: &http.Client{ + Transport: tr, + Timeout: timeout, + }, + } +} + +// Discover returns the OCM discovery information for a remote endpoint. +// It tries /.well-known/ocm first, then falls back to /ocm-provider (legacy). +// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1ocm-provider/get +func (c *OCMClient) Discover(ctx context.Context, endpoint string) (*wellknown.OcmDiscoveryData, error) { + log := appctx.GetLogger(ctx) + + remoteurl, _ := url.JoinPath(endpoint, "/.well-known/ocm") + body, err := c.discover(ctx, remoteurl) + if err != nil || len(body) == 0 { + log.Debug().Err(err).Str("sender", remoteurl).Str("response", string(body)). + Msg("invalid or empty response, falling back to legacy discovery") + remoteurl, _ := url.JoinPath(endpoint, "/ocm-provider") // legacy discovery endpoint + + body, err = c.discover(ctx, remoteurl) + if err != nil || len(body) == 0 { + log.Warn().Err(err).Str("sender", remoteurl).Str("response", string(body)). + Msg("invalid or empty response") + return nil, errtypes.InternalError("Invalid response on OCM discovery") + } + } + + var disco wellknown.OcmDiscoveryData + err = json.Unmarshal(body, &disco) + if err != nil { + log.Warn().Err(err).Str("sender", remoteurl).Str("response", string(body)). + Msg("malformed response") + return nil, errtypes.InternalError("Invalid payload on OCM discovery") + } + + log.Debug().Str("sender", remoteurl).Any("response", disco).Msg("discovery response") + return &disco, nil +} + +func (c *OCMClient) discover(ctx context.Context, url string) ([]byte, error) { + log := appctx.GetLogger(ctx) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, errors.Wrap(err, "error creating OCM discovery request") + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.client.Do(req) + defer func() { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + }() + + if err != nil { + return nil, errors.Wrap(err, "error doing OCM discovery request") + } + defer func(body io.ReadCloser) { + err := body.Close() + if err != nil { + log.Warn().Err(err).Msg("error closing response body") + } + }(resp.Body) + if resp.StatusCode != http.StatusOK { + log.Warn().Str("sender", url).Int("status", resp.StatusCode).Msg("discovery returned") + return nil, errtypes.NewErrtypeFromHTTPStatusCode(resp.StatusCode, "Remote does not offer a valid OCM discovery endpoint") + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "malformed remote OCM discovery") + } + return body, nil +} + +// GetDirectoryService fetches a directory service listing from the given URL per OCM spec Appendix C. +func (c *OCMClient) GetDirectoryService(ctx context.Context, directoryURL string) (*DirectoryService, error) { + log := appctx.GetLogger(ctx) + + // TODO(@MahdiBaghbani): the discover() should be changed into a generic function that can be used to fetch any OCM endpoint. I'll do it in the security PR to minimize conflicts. + body, err := c.discover(ctx, directoryURL) + if err != nil { + return nil, errors.Wrap(err, "error fetching directory service") + } + + var dirService DirectoryService + if err := json.Unmarshal(body, &dirService); err != nil { + log.Warn().Err(err).Str("url", directoryURL).Str("response", string(body)).Msg("malformed directory service response") + return nil, errors.Wrap(err, "invalid directory service payload") + } + + // Validate required fields + if dirService.Federation == "" { + return nil, errtypes.InternalError("directory service missing required 'federation' field") + } + // Servers can be empty array, that's valid + + log.Debug().Str("url", directoryURL).Str("federation", dirService.Federation).Int("servers", len(dirService.Servers)).Msg("fetched directory service") + return &dirService, nil +} diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/invites.go b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/invites.go index 7e7a2d9bf..3a3ad00d9 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/invites.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/invites.go @@ -60,7 +60,7 @@ type acceptInviteRequest struct { Email string `json:"email"` } -// AcceptInvite informs avout an accepted invitation so that the users +// AcceptInvite informs about an accepted invitation so that the users // can initiate the OCM share creation. func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -73,7 +73,7 @@ func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) { } if req.Token == "" || req.UserID == "" || req.RecipientProvider == "" { - reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token, userID and recipiendProvider must not be null", nil) + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token, userID and recipientProvider must not be null", nil) return } @@ -146,7 +146,7 @@ func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) { } if err := json.NewEncoder(w).Encode(&user{ - UserID: ocmuser.FederatedID(acceptInviteResponse.UserId, "").GetOpaqueId(), + UserID: ocmuser.FormatOCMUser(acceptInviteResponse.UserId), Email: acceptInviteResponse.Email, Name: acceptInviteResponse.DisplayName, }); err != nil { diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/sepc.go b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/sepc.go new file mode 100644 index 000000000..913807120 --- /dev/null +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/sepc.go @@ -0,0 +1,33 @@ +// Copyright 2025 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocmd + +// DirectoryService represents a directory service listing per OCM spec Appendix C. +type DirectoryService struct { + Federation string `json:"federation"` + Servers []DirectoryServiceServer `json:"servers"` +} + +// DirectoryServiceServer represents a single OCM server in a directory service. +type DirectoryServiceServer struct { + DisplayName string `json:"displayName"` + URL string `json:"url"` + // Added after discovery, not in raw response + InviteAcceptDialog string `json:"inviteAcceptDialog,omitempty"` +} diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/shares.go b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/shares.go index 40bd26768..e97baee4f 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/shares.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/ocmd/shares.go @@ -126,7 +126,7 @@ func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { return } - shareWith, _, err := getIDAndMeshProvider(req.ShareWith) + shareWith, _, err := getLocalUserID(req.ShareWith) if err != nil { reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) return @@ -196,6 +196,22 @@ func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) } +func getLocalUserID(user string) (id, provider string, err error) { + idPart, provider, err := getIDAndMeshProvider(user) + if err != nil { + return "", "", err + } + + // Handle nested @ in idPart (e.g. "user@idp@provider") + if inner := strings.LastIndex(idPart, "@"); inner != -1 { + id = idPart[:inner] + } else { + id = idPart + } + + return id, provider, nil +} + func getUserIDFromOCMUser(user string) (*userpb.UserId, error) { id, idp, err := getIDAndMeshProvider(user) if err != nil { @@ -209,13 +225,18 @@ func getUserIDFromOCMUser(user string) (*userpb.UserId, error) { }, nil } -func getIDAndMeshProvider(user string) (string, string, error) { - // the user is in the form of dimitri@apiwise.nl - split := strings.Split(user, "@") - if len(split) < 2 { +func getIDAndMeshProvider(user string) (id, provider string, err error) { + last := strings.LastIndex(user, "@") + if last == -1 { return "", "", errors.New("not in the form @") } - return strings.Join(split[:len(split)-1], "@"), split[len(split)-1], nil + if len(user[:last]) == 0 { + return "", "", errors.New("empty id") + } + if len(user[last+1:]) == 0 { + return "", "", errors.New("empty provider") + } + return user[:last], user[last+1:], nil } func getCreateShareRequest(r *http.Request) (*createShareRequest, error) { diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go index 58f79d6cd..b0c0b8e28 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go @@ -244,13 +244,30 @@ func (h *Handler) mapUserIdsFederatedShare(ctx context.Context, gw gatewayv1beta } } +func getIDAndMeshProvider(user string) (id, provider string, err error) { + last := strings.LastIndex(user, "@") + if last == -1 { + return "", "", errors.New("not in the form @") + } + if len(user[:last]) == 0 { + return "", "", errors.New("empty id") + } + if len(user[last+1:]) == 0 { + return "", "", errors.New("empty provider") + } + return user[:last], user[last+1:], nil +} + func (h *Handler) mustGetRemoteUser(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, id string) *userIdentifiers { - s := strings.SplitN(id, "@", 2) - opaqueID, idp := s[0], s[1] + id, provider, err := getIDAndMeshProvider(id) + if err != nil { + return &userIdentifiers{} + } userRes, err := gw.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{ RemoteUserId: &userpb.UserId{ - Idp: idp, - OpaqueId: opaqueID, + Type: userpb.UserType_USER_TYPE_FEDERATED, + Idp: provider, + OpaqueId: id, }, }) if err != nil { diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/sciencemesh/sciencemesh.go b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/sciencemesh/sciencemesh.go index 7f05fa4ba..8de8c5177 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/sciencemesh/sciencemesh.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/sciencemesh/sciencemesh.go @@ -60,12 +60,15 @@ func (s *svc) Close() error { } type config struct { - Prefix string `mapstructure:"prefix"` - GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"` - ProviderDomain string `mapstructure:"provider_domain" validate:"required"` - MeshDirectoryURL string `mapstructure:"mesh_directory_url"` - OCMMountPoint string `mapstructure:"ocm_mount_point"` - Events EventOptions `mapstructure:"events"` + Prefix string `mapstructure:"prefix"` + GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"` + ProviderDomain string `mapstructure:"provider_domain" validate:"required"` + MeshDirectoryURL string `mapstructure:"mesh_directory_url"` + OCMMountPoint string `mapstructure:"ocm_mount_point"` + DirectoryServiceURLs string `mapstructure:"directory_service_urls"` + OCMClientTimeout int `mapstructure:"ocm_client_timeout"` + OCMClientInsecure bool `mapstructure:"ocm_client_insecure"` + Events EventOptions `mapstructure:"events"` } // EventOptions are the configurable options for events @@ -86,6 +89,9 @@ func (c *config) ApplyDefaults() { if c.OCMMountPoint == "" { c.OCMMountPoint = "/ocm" } + if c.OCMClientTimeout == 0 { + c.OCMClientTimeout = 10 + } c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc) } @@ -114,6 +120,11 @@ func (s *svc) routerInit() error { return err } + wayfHandler := new(wayfHandler) + if err := wayfHandler.init(s.conf); err != nil { + return err + } + s.router.Post("/generate-invite", tokenHandler.Generate) s.router.Get("/list-invite", tokenHandler.ListInvite) s.router.Post("/accept-invite", tokenHandler.AcceptInvite) @@ -122,6 +133,8 @@ func (s *svc) routerInit() error { s.router.Get("/list-providers", providersHandler.ListProviders) s.router.Post("/create-share", sharesHandler.CreateShare) s.router.Post("/open-in-app", appsHandler.OpenInApp) + s.router.Get("/federations", wayfHandler.GetFederations) + s.router.Post("/discover", wayfHandler.DiscoverProvider) return nil } @@ -130,7 +143,7 @@ func (s *svc) Prefix() string { } func (s *svc) Unprotected() []string { - return nil + return []string{"/federations", "/discover"} } func (s *svc) Handler() http.Handler { diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/sciencemesh/wayf.go b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/sciencemesh/wayf.go new file mode 100644 index 000000000..7b6a849b6 --- /dev/null +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/sciencemesh/wayf.go @@ -0,0 +1,259 @@ +// Copyright 2018-2024 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sciencemesh + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/opencloud-eu/reva/v2/internal/http/services/ocmd" + "github.com/opencloud-eu/reva/v2/internal/http/services/reqres" + "github.com/opencloud-eu/reva/v2/pkg/appctx" +) + +type wayfHandler struct { + directoryServices []ocmd.DirectoryService + ocmClient *ocmd.OCMClient +} + +type DiscoverRequest struct { + Domain string `json:"domain"` +} + +type DiscoverResponse struct { + InviteAcceptDialog string `json:"inviteAcceptDialog"` +} + +// makeAbsoluteURL takes a base URL and a path/URL and returns an absolute URL. +// If dialogURL is already absolute (has scheme and host), it returns it as-is. +// Otherwise, it joins the dialogURL with the baseURL to create an absolute URL. +func makeAbsoluteURL(baseURL, dialogURL string) (string, error) { + if dialogURL == "" { + return "", nil + } + + parsed, err := url.Parse(dialogURL) + if err == nil && parsed.Scheme != "" && parsed.Host != "" { + return dialogURL, nil + } + + return url.JoinPath(baseURL, dialogURL) +} + +func (h *wayfHandler) init(c *config) error { + log := appctx.GetLogger(context.Background()) + + // Create OCM client for discovery from config + h.ocmClient = ocmd.NewClient(time.Duration(c.OCMClientTimeout)*time.Second, c.OCMClientInsecure) + log.Debug(). + Int("timeout_seconds", c.OCMClientTimeout). + Bool("insecure", c.OCMClientInsecure). + Msg("Created OCM client for discovery") + + urls := strings.Fields(c.DirectoryServiceURLs) + if len(urls) == 0 { + log.Info().Msg("No directory service URLs configured, starting with empty list") + h.directoryServices = []ocmd.DirectoryService{} + return nil + } + + log.Debug().Int("url_count", len(urls)).Strs("urls", urls).Msg("Initializing WAYF handler with directory service URLs") + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + h.directoryServices = []ocmd.DirectoryService{} + discoveryErrors := 0 + validServersCount := 0 + fetchErrors := 0 + + for _, directoryURL := range urls { + log.Debug().Str("url", directoryURL).Msg("Fetching directory service") + + directoryService, err := h.ocmClient.GetDirectoryService(ctx, directoryURL) + if err != nil { + log.Info().Err(err).Str("url", directoryURL).Msg("Failed to fetch directory service, skipping") + fetchErrors++ + continue + } + + log.Debug().Str("federation", directoryService.Federation).Int("servers_count", len(directoryService.Servers)).Msg("Processing directory service") + + var validServers []ocmd.DirectoryServiceServer + for _, srv := range directoryService.Servers { + if srv.DisplayName == "" || srv.URL == "" { + log.Debug().Str("federation", directoryService.Federation). + Str("displayName", srv.DisplayName). + Str("url", srv.URL). + Msg("Skipping server with missing displayName or url") + continue + } + + log.Debug().Str("federation", directoryService.Federation).Str("server", srv.DisplayName).Str("url", srv.URL).Msg("Discovering server") + + // Discover inviteAcceptDialog from OCM endpoint + disco, err := h.ocmClient.Discover(ctx, srv.URL) + if err != nil { + log.Debug().Err(err). + Str("federation", directoryService.Federation). + Str("server", srv.DisplayName). + Str("url", srv.URL). + Msg("Failed to discover server, skipping") + discoveryErrors++ + continue + } + + inviteDialog := disco.InviteAcceptDialog + + if inviteDialog != "" { + absoluteURL, err := makeAbsoluteURL(srv.URL, inviteDialog) + if err != nil { + log.Debug().Err(err). + Str("federation", directoryService.Federation). + Str("server", srv.DisplayName). + Str("url", srv.URL). + Str("inviteDialog", disco.InviteAcceptDialog). + Msg("Failed to construct absolute URL, skipping server") + continue + } + if absoluteURL != inviteDialog { + log.Debug().Str("original", inviteDialog).Str("absolute", absoluteURL).Msg("Converted to absolute URL") + } + inviteDialog = absoluteURL + } + + validServers = append(validServers, ocmd.DirectoryServiceServer{ + DisplayName: srv.DisplayName, + URL: srv.URL, + InviteAcceptDialog: inviteDialog, + }) + validServersCount++ + + log.Debug(). + Str("federation", directoryService.Federation). + Str("server", srv.DisplayName). + Str("inviteAcceptDialog", inviteDialog). + Msg("Successfully discovered server") + } + + if len(validServers) > 0 { + h.directoryServices = append(h.directoryServices, ocmd.DirectoryService{ + Federation: directoryService.Federation, + Servers: validServers, + }) + log.Debug().Str("federation", directoryService.Federation).Int("valid_servers", len(validServers)).Msg("Added directory service with valid servers") + } else { + log.Info().Str("federation", directoryService.Federation). + Msg("Directory service has no valid servers, skipping entirely") + } + } + + log.Info(). + Int("directory_services", len(h.directoryServices)). + Int("valid_servers", validServersCount). + Int("fetch_errors", fetchErrors). + Int("discovery_errors", discoveryErrors). + Msg("WAYF handler initialization completed") + + return nil +} + +func (h *wayfHandler) GetFederations(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + if err := json.NewEncoder(w).Encode(h.directoryServices); err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error encoding response", err) + return + } +} + +func (h *wayfHandler) DiscoverProvider(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := appctx.GetLogger(ctx) + + var req DiscoverRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "Invalid request body", err) + return + } + + if req.Domain == "" { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "Domain is required", nil) + return + } + + domain := req.Domain + if !strings.HasPrefix(domain, "http://") && !strings.HasPrefix(domain, "https://") { + domain = "https://" + domain + } + + parsedURL, err := url.Parse(domain) + if err != nil || parsedURL.Host == "" { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "Invalid domain format", err) + return + } + + log.Debug().Str("domain", domain).Msg("Attempting OCM discovery") + disco, err := h.ocmClient.Discover(ctx, domain) + if err != nil { + log.Info().Err(err).Str("domain", domain).Msg("Discovery failed") + reqres.WriteError(w, r, reqres.APIErrorNotFound, + fmt.Sprintf("Provider at '%s' does not support OCM discovery", req.Domain), err) + return + } + + inviteDialog := disco.InviteAcceptDialog + + if inviteDialog == "" { + log.Info().Str("domain", domain).Msg("Provider does not provide invite accept dialog") + reqres.WriteError(w, r, reqres.APIErrorNotFound, + fmt.Sprintf("Provider at '%s' does not provide an invite accept dialog", req.Domain), nil) + return + } + + inviteDialog, err = makeAbsoluteURL(domain, inviteDialog) + if err != nil { + log.Info().Err(err).Str("domain", domain).Str("inviteDialog", disco.InviteAcceptDialog).Msg("Failed to construct invite accept dialog URL") + reqres.WriteError(w, r, reqres.APIErrorServerError, "Failed to construct invite accept dialog URL", err) + return + } + + response := DiscoverResponse{ + InviteAcceptDialog: inviteDialog, + } + + log.Info(). + Str("domain", req.Domain). + Str("inviteAcceptDialog", inviteDialog). + Msg("Discovery successful") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + if err := json.NewEncoder(w).Encode(response); err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "Error encoding response", err) + return + } +} diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/wellknown/ocm.go b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/wellknown/ocm.go index fab4af506..095114919 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/wellknown/ocm.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/wellknown/ocm.go @@ -27,25 +27,27 @@ import ( "github.com/opencloud-eu/reva/v2/pkg/appctx" ) -const OCMAPIVersion = "1.1.0" +const OCMAPIVersion = "1.2.0" type OcmProviderConfig struct { - OCMPrefix string `docs:"ocm;The prefix URL where the OCM API is served." mapstructure:"ocm_prefix"` - Endpoint string `docs:"This host's full URL. If it's not configured, it is assumed OCM is not available." mapstructure:"endpoint"` - Provider string `docs:"reva;A friendly name that defines this service." mapstructure:"provider"` - WebdavRoot string `docs:"/remote.php/dav/ocm;The root URL of the WebDAV endpoint to serve OCM shares." mapstructure:"webdav_root"` - WebappRoot string `docs:"/external/sciencemesh;The root URL to serve Web apps via OCM." mapstructure:"webapp_root"` - EnableWebapp bool `docs:"false;Whether web apps are enabled in OCM shares." mapstructure:"enable_webapp"` - EnableDatatx bool `docs:"false;Whether data transfers are enabled in OCM shares." mapstructure:"enable_datatx"` + OCMPrefix string `docs:"ocm;The prefix URL where the OCM API is served." mapstructure:"ocm_prefix"` + Endpoint string `docs:"This host's full URL. If it's not configured, it is assumed OCM is not available." mapstructure:"endpoint"` + Provider string `docs:"reva;A friendly name that defines this service." mapstructure:"provider"` + WebdavRoot string `docs:"/remote.php/dav/ocm;The root URL of the WebDAV endpoint to serve OCM shares." mapstructure:"webdav_root"` + WebappRoot string `docs:"/external/sciencemesh;The root URL to serve Web apps via OCM." mapstructure:"webapp_root"` + InviteAcceptDialog string `docs:"/open-cloud-mesh/accept-invite;The frontend URL where to land when receiving an invitation" mapstructure:"invite_accept_dialog"` + EnableWebapp bool `docs:"false;Whether web apps are enabled in OCM shares." mapstructure:"enable_webapp"` + EnableDatatx bool `docs:"false;Whether data transfers are enabled in OCM shares." mapstructure:"enable_datatx"` } type OcmDiscoveryData struct { - Enabled bool `json:"enabled" xml:"enabled"` - APIVersion string `json:"apiVersion" xml:"apiVersion"` - Endpoint string `json:"endPoint" xml:"endPoint"` - Provider string `json:"provider" xml:"provider"` - ResourceTypes []resourceTypes `json:"resourceTypes" xml:"resourceTypes"` - Capabilities []string `json:"capabilities" xml:"capabilities"` + Enabled bool `json:"enabled" xml:"enabled"` + APIVersion string `json:"apiVersion" xml:"apiVersion"` + Endpoint string `json:"endPoint" xml:"endPoint"` + Provider string `json:"provider" xml:"provider"` + ResourceTypes []resourceTypes `json:"resourceTypes" xml:"resourceTypes"` + Capabilities []string `json:"capabilities" xml:"capabilities"` + InviteAcceptDialog string `json:"inviteAcceptDialog" xml:"inviteAcceptDialog"` } type resourceTypes struct { @@ -77,6 +79,9 @@ func (c *OcmProviderConfig) ApplyDefaults() { if c.WebappRoot[len(c.WebappRoot)-1:] != "/" { c.WebappRoot += "/" } + if c.InviteAcceptDialog == "" { + c.InviteAcceptDialog = "/open-cloud-mesh/accept-invite" + } } func (h *wkocmHandler) init(c *OcmProviderConfig) { @@ -123,12 +128,13 @@ func (h *wkocmHandler) init(c *OcmProviderConfig) { ShareTypes: []string{"user"}, // so far we only support `user` Protocols: rtProtos, // expose the protocols as per configuration }} - // for now we hardcode the capabilities, as this is currently only advisory - d.Capabilities = []string{"/invite-accepted"} + // for now, we hardcoded the capabilities, as this is currently only advisory + d.Capabilities = []string{"invites", "webdav-uri", "protocol-object", "invite-wayf"} + d.InviteAcceptDialog, _ = url.JoinPath(c.Endpoint, c.InviteAcceptDialog) h.data = d } -// This handler implements the OCM discovery endpoint specified in +// Ocm handles the OCM discovery endpoint specified in // https://cs3org.github.io/OCM-API/docs.html?repo=OCM-API&user=cs3org#/paths/~1ocm-provider/get func (h *wkocmHandler) Ocm(w http.ResponseWriter, r *http.Request) { log := appctx.GetLogger(r.Context()) diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/events/events.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/events/events.go index 1b41df0ef..31047333b 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/events/events.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/events/events.go @@ -85,7 +85,19 @@ type ( // group defines the service type: One group will get exactly one copy of a event that is emitted // NOTE: uses reflect on initialization func Consume(s Consumer, group string, evs ...Unmarshaller) (<-chan Event, error) { - c, err := s.Consume(MainQueueName, events.WithGroup(group)) + return ConsumeWithOptions(s, ConsumeOptions{Group: group}, evs...) +} + +// ConsumeWithOptions returns a channel that will get all events that match the given evs +// opts defines the options for the consumer +func ConsumeWithOptions(s Consumer, opts ConsumeOptions, evs ...Unmarshaller) (<-chan Event, error) { + o := []events.ConsumeOption{ + events.WithGroup(opts.Group), + } + if !opts.AutoAck { + o = append(o, events.WithAutoAck(false, opts.AckWait)) + } + c, err := s.Consume(MainQueueName, o...) if err != nil { return nil, err } @@ -126,7 +138,18 @@ func Consume(s Consumer, group string, evs ...Unmarshaller) (<-chan Event, error // ConsumeAll allows consuming all events. Note that unmarshalling must be done manually in this case, therefore Event.Event will always be of type []byte func ConsumeAll(s Consumer, group string) (<-chan Event, error) { - c, err := s.Consume(MainQueueName, events.WithGroup(group)) + return ConsumeAllWithOptions(s, ConsumeOptions{Group: group}) +} + +// ConsumeAllWithOptions allows consuming all events. Note that unmarshalling must be done manually in this case, therefore Event.Event will always be of type []byte +func ConsumeAllWithOptions(s Consumer, opts ConsumeOptions) (<-chan Event, error) { + o := []events.ConsumeOption{ + events.WithGroup(opts.Group), + } + if !opts.AutoAck { + o = append(o, events.WithAutoAck(false, opts.AckWait)) + } + c, err := s.Consume(MainQueueName, o...) if err != nil { return nil, err } diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/events/options.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/events/options.go new file mode 100644 index 000000000..634cbc644 --- /dev/null +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/events/options.go @@ -0,0 +1,12 @@ +package events + +import ( + "time" +) + +// ConsumeOptions contains all the options which can be provided when consuming events +type ConsumeOptions struct { + Group string + AutoAck bool + AckWait time.Duration +} diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/group/manager/ldap/ldap.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/group/manager/ldap/ldap.go index e4e025866..33e01f3a3 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/group/manager/ldap/ldap.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/group/manager/ldap/ldap.go @@ -22,6 +22,7 @@ import ( "context" "fmt" "strconv" + "strings" grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" @@ -331,7 +332,11 @@ func (m *manager) ldapEntryToGroup(entry *ldap.Entry) (*grouppb.Group, error) { func (m *manager) ldapEntryToGroupID(entry *ldap.Entry) (*grouppb.GroupId, error) { var id string if m.c.LDAPIdentity.Group.Schema.IDIsOctetString { - rawValue := entry.GetEqualFoldRawAttributeValue(m.c.LDAPIdentity.Group.Schema.ID) + attribute := m.c.LDAPIdentity.Group.Schema.ID + rawValue := entry.GetEqualFoldRawAttributeValue(attribute) + if strings.EqualFold(attribute, "objectguid") { + rawValue = ldapIdentity.SwapObjectGUIDBytes(rawValue) + } if value, err := uuid.FromBytes(rawValue); err == nil { id = value.String() } else { @@ -350,13 +355,16 @@ func (m *manager) ldapEntryToGroupID(entry *ldap.Entry) (*grouppb.GroupId, error func (m *manager) ldapEntryToUserID(entry *ldap.Entry) (*userpb.UserId, error) { var uid string if m.c.LDAPIdentity.User.Schema.IDIsOctetString { - rawValue := entry.GetEqualFoldRawAttributeValue(m.c.LDAPIdentity.User.Schema.ID) - var value uuid.UUID - var err error - if value, err = uuid.FromBytes(rawValue); err != nil { + attribute := m.c.LDAPIdentity.User.Schema.ID + rawValue := entry.GetEqualFoldRawAttributeValue(attribute) + if strings.EqualFold(attribute, "objectguid") { + rawValue = ldapIdentity.SwapObjectGUIDBytes(rawValue) + } + if value, err := uuid.FromBytes(rawValue); err == nil { + uid = value.String() + } else { return nil, err } - uid = value.String() } else { uid = entry.GetEqualFoldAttributeValue(m.c.LDAPIdentity.User.Schema.ID) } diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/micro/ocdav/option.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/micro/ocdav/option.go index c294239fb..37e3a6fa7 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/micro/ocdav/option.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/micro/ocdav/option.go @@ -31,7 +31,6 @@ import ( "github.com/rs/zerolog" "go-micro.dev/v4/broker" "go.opentelemetry.io/otel/trace" - "google.golang.org/grpc/credentials" ) // Option defines a single option function. @@ -52,10 +51,7 @@ type Options struct { FavoriteManager favorite.Manager GatewaySelector pool.Selectable[gateway.GatewayAPIClient] - TracingEnabled bool - TracingInsecure bool - TracingEndpoint string - TracingTransportCredentials credentials.TransportCredentials + TracesExporter string TraceProvider trace.TracerProvider @@ -233,33 +229,10 @@ func LockSystem(val ocdav.LockSystem) Option { } } -// Tracing enables tracing -// Deprecated: use WithTracingEndpoint and WithTracingEnabled, Collector is unused -func Tracing(endpoint, collector string) Option { +// WithTracesExporter option +func WithTracesExporter(exporter string) Option { return func(o *Options) { - o.TracingEnabled = true - o.TracingEndpoint = endpoint - } -} - -// WithTracingEnabled option -func WithTracingEnabled(enabled bool) Option { - return func(o *Options) { - o.TracingEnabled = enabled - } -} - -// WithTracingEndpoint option -func WithTracingEndpoint(endpoint string) Option { - return func(o *Options) { - o.TracingEndpoint = endpoint - } -} - -// WithTracingInsecure option -func WithTracingInsecure() Option { - return func(o *Options) { - o.TracingInsecure = true + o.TracesExporter = exporter } } @@ -269,13 +242,6 @@ func WithTracingExporter(exporter string) Option { return func(o *Options) {} } -// WithTracingTransportCredentials option -func WithTracingTransportCredentials(v credentials.TransportCredentials) Option { - return func(o *Options) { - o.TracingTransportCredentials = v - } -} - // WithTraceProvider option func WithTraceProvider(provider trace.TracerProvider) Option { return func(o *Options) { diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/micro/ocdav/service.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/micro/ocdav/service.go index 2eeef548f..0553772a3 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/micro/ocdav/service.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/micro/ocdav/service.go @@ -91,20 +91,7 @@ func Service(opts ...Option) (micro.Service, error) { tp := sopts.TraceProvider if tp == nil { - topts := []rtrace.Option{ - rtrace.WithEndpoint(sopts.TracingEndpoint), - rtrace.WithServiceName(sopts.Name), - } - if sopts.TracingEnabled { - topts = append(topts, rtrace.WithEnabled()) - } - if sopts.TracingInsecure { - topts = append(topts, rtrace.WithInsecure()) - } - if sopts.TracingTransportCredentials != nil { - topts = append(topts, rtrace.WithTransportCredentials(sopts.TracingTransportCredentials)) - } - tp = rtrace.NewTracerProvider(topts...) + tp = rtrace.NewTracerProvider(sopts.Name, sopts.TracesExporter) } if err := useMiddlewares(r, &sopts, revaService, tp); err != nil { return nil, err @@ -189,10 +176,7 @@ func useMiddlewares(r *chi.Mux, sopts *Options, svc global.Service, tp trace.Tra } // tracing - tm := func(h http.Handler) http.Handler { return h } - if sopts.TracingEnabled { - tm = traceHandler(tp, "ocdav") - } + tm := traceHandler(tp, "ocdav") // metrics pm := func(h http.Handler) http.Handler { return h } diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/ocm/user/user.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/ocm/user/user.go index 9be8add2b..10b44cdff 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/ocm/user/user.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/ocm/user/user.go @@ -1,39 +1,44 @@ package user import ( - "encoding/base64" "fmt" + "net/url" "strings" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" ) -// FederatedID creates a federated user id by +// LocalUserFederatedID creates a federated id for local users by // 1. stripping the protocol from the domain and -// 2. base64 encoding the opaque id with the domain to get a unique identifier that cannot collide with other users -func FederatedID(id *userpb.UserId, domain string) *userpb.UserId { - opaqueId := base64.URLEncoding.EncodeToString([]byte(id.OpaqueId + "@" + id.Idp)) - return &userpb.UserId{ - Type: userpb.UserType_USER_TYPE_FEDERATED, - Idp: domain, - OpaqueId: opaqueId, +// 2. if the domain is different from the idp, add the idp to the opaque id +func LocalUserFederatedID(id *userpb.UserId, domain string) *userpb.UserId { + if u, err := url.Parse(domain); err == nil && u.Host != "" { + domain = u.Host } + u := &userpb.UserId{ + Type: userpb.UserType_USER_TYPE_FEDERATED, + Idp: id.GetIdp(), + OpaqueId: id.GetOpaqueId(), + } + + if domain != "" && id.GetIdp() != domain { + if id.GetIdp() != "" { + u.OpaqueId = id.GetOpaqueId() + "@" + id.GetIdp() + } + u.Idp = domain + } + return u } -// RemoteID creates a remote user id by -// 1. decoding the base64 encoded opaque id -// 2. splitting the opaque id at the last @ to get the opaque id and the domain -func RemoteID(id *userpb.UserId) *userpb.UserId { +// DecodeRemoteUserFederatedID decodes opaque id into remote user's federated id by +// splitting the opaque id at the last @ to get the opaque id and the domain +func DecodeRemoteUserFederatedID(id *userpb.UserId) *userpb.UserId { remoteId := &userpb.UserId{ Type: userpb.UserType_USER_TYPE_PRIMARY, Idp: id.Idp, OpaqueId: id.OpaqueId, } - bytes, err := base64.URLEncoding.DecodeString(id.GetOpaqueId()) - if err != nil { - return remoteId - } - remote := string(bytes) + remote := id.OpaqueId last := strings.LastIndex(remote, "@") if last == -1 { return remoteId @@ -46,5 +51,8 @@ func RemoteID(id *userpb.UserId) *userpb.UserId { // FormatOCMUser formats a user id in the form of @ used by the OCM API in shareWith, owner and creator fields func FormatOCMUser(u *userpb.UserId) string { + if u.Idp == "" { + return u.OpaqueId + } return fmt.Sprintf("%s@%s", u.OpaqueId, u.Idp) } diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/tree/assimilation.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/tree/assimilation.go index 6db899f5f..b21f2050e 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/tree/assimilation.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/tree/assimilation.go @@ -328,6 +328,10 @@ func (t *Tree) HandleFileDelete(path string, sendSSE bool) error { t.log.Error().Err(err).Str("path", path).Msg("could not purge metadata") } + if err = t.setDirty(filepath.Dir(path), true); err != nil { + t.log.Error().Err(err).Str("path", path).Msg("could not set dirty flag") + } + if !sendSSE { return nil } @@ -705,6 +709,16 @@ assimilate: attributes.SetInt64(prefixes.BlobsizeAttr, fi.Size()) attributes.SetInt64(prefixes.TypeAttr, int64(provider.ResourceType_RESOURCE_TYPE_FILE)) n = node.New(spaceID, id, parentID, filepath.Base(path), fi.Size(), blobID, provider.ResourceType_RESOURCE_TYPE_FILE, nil, t.lookup) + n.SpaceRoot = &node.Node{BaseNode: node.BaseNode{SpaceID: spaceID, ID: spaceID}} + + prevBlobSize, err := previousAttribs.Int64(prefixes.BlobsizeAttr) + if err == nil && prevBlobSize != fi.Size() { + // file size changed, trigger propagation of tree size changes + err = t.Propagate(context.Background(), n, fi.Size()-prevBlobSize) + if err != nil { + t.log.Error().Err(err).Str("path", path).Msg("could not propagate tree size changes") + } + } } attributes.SetTime(prefixes.MTimeAttr, fi.ModTime()) @@ -772,6 +786,15 @@ assimilate: return nil, nil, errors.Wrap(err, "failed to set attributes") } + // clear the status attribute if it was set before, if there was any upload to this file in progress + // it needs notice that this file was changes meanwhile. + if _, ok := previousAttribs[prefixes.StatusPrefix]; ok { + err = t.lookup.MetadataBackend().Remove(context.Background(), bn, prefixes.StatusPrefix, false) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to clear status attribute") + } + + } if err := t.lookup.CacheID(context.Background(), spaceID, id, path); err != nil { t.log.Error().Err(err).Str("spaceID", spaceID).Str("id", id).Str("path", path).Msg("could not cache id") } diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/upload/upload.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/upload/upload.go index dc418a2cc..f1be9b869 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/upload/upload.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/upload/upload.go @@ -35,6 +35,7 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/golang-jwt/jwt/v5" "github.com/pkg/errors" + "github.com/rogpeppe/go-internal/lockedfile" tusd "github.com/tus/tusd/v2/pkg/handler" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -307,11 +308,13 @@ func (session *DecomposedFsSession) Finalize(ctx context.Context) (err error) { } // lock the node before writing the blob - unlock, err := session.store.lu.MetadataBackend().Lock(revisionNode) + lockedNode, err := lockedfile.OpenFile(session.store.lu.MetadataBackend().LockfilePath(revisionNode), os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } - defer func() { _ = unlock() }() + defer func() { + _ = lockedNode.Close() + }() isProcessing := revisionNode.IsProcessing(ctx) var procssingID string @@ -322,12 +325,18 @@ func (session *DecomposedFsSession) Finalize(ctx context.Context) (err error) { // another upload on this node is in progress or has finished since we started if !isProcessing || procssingID != session.ID() { versionID := revisionNode.ID + node.RevisionIDDelimiter + session.MTime().UTC().Format(time.RFC3339Nano) - revisionNode, err = node.ReadNode(ctx, session.store.lu, session.SpaceID(), versionID, false, spaceRoot, false) - if err != nil { - return fmt.Errorf("failed to read revision node %s for upload finalization: %w", versionID, err) - } - if !revisionNode.Exists { - return fmt.Errorf("revision node %s for upload finalization does not exist", versionID) + // There should be a revision node (created by the other upload that finished before us), read it and upload our blob there. + existingRevisionNode, err := node.ReadNode(ctx, session.store.lu, session.SpaceID(), versionID, false, spaceRoot, false) + if err != nil || !existingRevisionNode.Exists { + // The revision node has not been created. Likely because the file on disk was modified externally and re-assilimated (watchfs == true) + // Let's create the revision node now and upload the blob to it. + revisionNode, err = session.createRevisionNodeForUpload(ctx, revisionNode, session.MTime().UTC().Format(time.RFC3339Nano), lockedNode) + if err != nil { + appctx.GetLogger(ctx).Debug().Err(err).Str("versionID", session.MTime().UTC().Format(time.RFC3339Nano)).Msg("failed to create revision node for upload finalization") + return err + } + } else { + revisionNode = existingRevisionNode } // lock this node as well, before writing the blob revisionNodeUnlock, err := session.store.lu.MetadataBackend().Lock(revisionNode) @@ -349,6 +358,47 @@ func (session *DecomposedFsSession) Finalize(ctx context.Context) (err error) { return nil } +func (session *DecomposedFsSession) createRevisionNodeForUpload(ctx context.Context, baseNode *node.Node, rev string, lockedNode *lockedfile.File) (*node.Node, error) { + versionID := baseNode.ID + node.RevisionIDDelimiter + rev + log := appctx.GetLogger(ctx) + _, err := session.store.tp.CreateRevision(ctx, baseNode, rev, lockedNode) + if err != nil { + log.Error().Err(err).Str("versionID", versionID).Msg("failed to create revision node for upload") + return nil, err + } + + // FIXME: We already calculated the checksums in FinishUpload, we should maybe pass them via the session instead of recalculating them here + sha1h, md5h, adler32h, err := node.CalculateChecksums(ctx, session.binPath()) + if err != nil { + return nil, err + } + + // update checksums + attrs := node.Attributes{ + prefixes.ChecksumPrefix + "sha1": sha1h.Sum(nil), + prefixes.ChecksumPrefix + "md5": md5h.Sum(nil), + prefixes.ChecksumPrefix + "adler32": adler32h.Sum(nil), + } + revisionNode, err := node.ReadNode(ctx, session.store.lu, session.SpaceID(), versionID, false, baseNode.SpaceRoot, false) + if err == nil { + mtime := session.MTime() + attrs.SetString(prefixes.BlobIDAttr, session.ID()) + attrs.SetInt64(prefixes.BlobsizeAttr, session.Size()) + revisionNode.BlobID = session.ID() + revisionNode.Blobsize = session.Size() + + err = session.store.lu.TimeManager().OverrideMtime(ctx, revisionNode, &attrs, mtime) + if err != nil { + return nil, errors.Wrap(err, "Decomposedfs: failed to set the mtime") + } + err = revisionNode.SetXattrsWithContext(ctx, attrs, false) + if err != nil { + return nil, errors.Wrap(err, "Decomposedfs: failed to set node attributes") + } + } + return revisionNode, err +} + func checkHash(expected string, h hash.Hash) error { shash := hex.EncodeToString(h.Sum(nil)) if expected != shash { @@ -389,6 +439,7 @@ func (session *DecomposedFsSession) Cleanup(revertNodeMetadata, cleanBin, cleanI if !revisionNode.Exists { sublog.Error().Str("versionID", versionID).Msg("revision node does not exist") + return } // restore the revision @@ -400,6 +451,7 @@ func (session *DecomposedFsSession) Cleanup(revertNodeMetadata, cleanBin, cleanI if err := session.store.tp.RestoreRevision(ctx, revisionNode, n, mtime); err != nil { sublog.Error().Err(err).Str("versionID", versionID).Msg("restoring revision node failed") + return } if err := os.RemoveAll(revisionNode.InternalPath()); err != nil { diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/trace/options.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/trace/options.go deleted file mode 100644 index fbd8b8237..000000000 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/trace/options.go +++ /dev/null @@ -1,68 +0,0 @@ -package trace - -import "google.golang.org/grpc/credentials" - -// Options for trace -type Options struct { - Enabled bool - Insecure bool - Exporter string - Collector string - Endpoint string - ServiceName string - TransportCredentials credentials.TransportCredentials -} - -// Option for trace -type Option func(o *Options) - -// WithEnabled option -func WithEnabled() Option { - return func(o *Options) { - o.Enabled = true - } -} - -// WithExporter option -// Deprecated: unused -func WithExporter(v string) Option { - return func(o *Options) { - o.Exporter = v - } -} - -// WithInsecure option -func WithInsecure() Option { - return func(o *Options) { - o.Insecure = true - } -} - -// WithCollector option -// Deprecated: unused -func WithCollector(v string) Option { - return func(o *Options) { - o.Collector = v - } -} - -// WithEndpoint option -func WithEndpoint(v string) Option { - return func(o *Options) { - o.Endpoint = v - } -} - -// WithServiceName option -func WithServiceName(v string) Option { - return func(o *Options) { - o.ServiceName = v - } -} - -// WithTransportCredentials option -func WithTransportCredentials(v credentials.TransportCredentials) Option { - return func(o *Options) { - o.TransportCredentials = v - } -} diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/trace/trace.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/trace/trace.go index f4bfcb9d9..bd7bdb76d 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/trace/trace.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/trace/trace.go @@ -20,21 +20,17 @@ package trace import ( "context" - "fmt" "sync" + "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/credentials/insecure" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" ) var ( @@ -48,28 +44,42 @@ type revaDefaultTracerProvider struct { initialized bool } -// NewTracerProvider returns a new TracerProvider, configure for the specified service -func NewTracerProvider(opts ...Option) trace.TracerProvider { - options := Options{} +// NewTracerProvider returns a new TracerProvider, configured for the specified service +func NewTracerProvider(serviceName, exporter string) trace.TracerProvider { + var tp *sdktrace.TracerProvider + if exporter == "otlp" { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() - for _, o := range opts { - o(&options) + exporter, err := otlptracegrpc.New(ctx) + if err != nil { + return nil + } + resources, err := resource.New( + context.Background(), + resource.WithFromEnv(), // Reads OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME + resource.WithAttributes( + attribute.String("service.name", serviceName), + attribute.String("library.language", "go"), + ), + ) + if err != nil { + return nil + } + + tp = sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithBatcher(exporter), + sdktrace.WithResource(resources), + ) + } else { + tp = sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.NeverSample()), + ) } - if options.TransportCredentials == nil { - options.TransportCredentials = credentials.NewClientTLSFromCert(nil, "") - } - - if !options.Enabled { - return noop.NewTracerProvider() - } - - // default to 'reva' as service name if not set - if options.ServiceName == "" { - options.ServiceName = "reva" - } - - return getOtlpTracerProvider(options) + SetDefaultTracerProvider(tp) + return tp } // SetDefaultTracerProvider sets the default trace provider @@ -80,21 +90,6 @@ func SetDefaultTracerProvider(tp trace.TracerProvider) { defaultProvider.initialized = true } -// InitDefaultTracerProvider initializes a global default jaeger TracerProvider at a package level. -// -// Deprecated: Use NewTracerProvider and SetDefaultTracerProvider to properly initialize a tracer provider with options -func InitDefaultTracerProvider(collector, endpoint string) { - defaultProvider.mutex.Lock() - defer defaultProvider.mutex.Unlock() - if !defaultProvider.initialized { - SetDefaultTracerProvider(getOtlpTracerProvider(Options{ - Endpoint: endpoint, - ServiceName: "reva default otlp provider", - Insecure: true, - })) - } -} - // DefaultProvider returns the "global" default TracerProvider // Currently used by the pool to get the global tracer func DefaultProvider() trace.TracerProvider { @@ -102,42 +97,3 @@ func DefaultProvider() trace.TracerProvider { defer defaultProvider.mutex.RUnlock() return otel.GetTracerProvider() } - -// getOtelTracerProvider returns a new TracerProvider, configure for the specified service -func getOtlpTracerProvider(options Options) trace.TracerProvider { - transportCredentials := options.TransportCredentials - if options.Insecure { - transportCredentials = insecure.NewCredentials() - } - conn, err := grpc.NewClient(options.Endpoint, - grpc.WithTransportCredentials(transportCredentials), - ) - if err != nil { - panic(fmt.Errorf("failed to create gRPC connection to endpoint: %w", err)) - } - exporter, err := otlptracegrpc.New( - context.Background(), - otlptracegrpc.WithGRPCConn(conn), - ) - - if err != nil { - panic(err) - } - - resources, err := resource.New( - context.Background(), - resource.WithAttributes( - attribute.String("service.name", options.ServiceName), - attribute.String("library.language", "go"), - ), - ) - if err != nil { - panic(err) - } - - return sdktrace.NewTracerProvider( - sdktrace.WithSampler(sdktrace.AlwaysSample()), - sdktrace.WithBatcher(exporter), - sdktrace.WithResource(resources), - ) -} diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/user/manager/ldap/ldap.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/user/manager/ldap/ldap.go index a0ad3a617..015628729 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/user/manager/ldap/ldap.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/user/manager/ldap/ldap.go @@ -22,6 +22,7 @@ import ( "context" "fmt" "strconv" + "strings" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" "github.com/go-ldap/ldap/v3" @@ -270,7 +271,11 @@ func (m *manager) ldapEntryToUser(entry *ldap.Entry) (*userpb.User, error) { func (m *manager) ldapEntryToUserID(entry *ldap.Entry) (*userpb.UserId, error) { var uid string if m.c.LDAPIdentity.User.Schema.IDIsOctetString { - rawValue := entry.GetEqualFoldRawAttributeValue(m.c.LDAPIdentity.User.Schema.ID) + attribute := m.c.LDAPIdentity.User.Schema.ID + rawValue := entry.GetEqualFoldRawAttributeValue(attribute) + if strings.EqualFold(attribute, "objectguid") { + rawValue = ldapIdentity.SwapObjectGUIDBytes(rawValue) + } if value, err := uuid.FromBytes(rawValue); err == nil { uid = value.String() } else { diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/ldap/identity.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/ldap/identity.go index a310a8ff6..efdc4a20c 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/ldap/identity.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/ldap/identity.go @@ -405,15 +405,19 @@ func (i *Identity) GetLDAPUserGroups(ctx context.Context, lc ldap.Client, userEn // FIXME 1. use the memberof or members attribute of a user to get the groups // FIXME 2. ook up the id for each group var groupID string + attribute := i.Group.Schema.ID if i.Group.Schema.IDIsOctetString { - raw := entry.GetEqualFoldRawAttributeValue(i.Group.Schema.ID) - value, err := uuid.FromBytes(raw) + rawValue := entry.GetEqualFoldRawAttributeValue(attribute) + if strings.EqualFold(attribute, "objectguid") { + rawValue = SwapObjectGUIDBytes(rawValue) + } + value, err := uuid.FromBytes(rawValue) if err != nil { return nil, err } groupID = value.String() } else { - groupID = entry.GetEqualFoldAttributeValue(i.Group.Schema.ID) + groupID = entry.GetEqualFoldAttributeValue(attribute) } groups = append(groups, groupID) @@ -556,17 +560,26 @@ func filterEscapeAttribute(attribute string, binary bool, id string) (string, er return escaped, nil } +// swapObjectGUIDBytes converts between AD's mixed-endian objectGUID format and standard UUID byte order +// AD stores objectGUID with mixed endianness 🤪 - swap first 3 components +func SwapObjectGUIDBytes(value []byte) []byte { + if len(value) != 16 { + return value + } + return []byte{ + value[3], value[2], value[1], value[0], // First component (4 bytes) - reverse + value[5], value[4], // Second component (2 bytes) - reverse + value[7], value[6], // Third component (2 bytes) - reverse + value[8], value[9], value[10], value[11], value[12], value[13], value[14], value[15], // Last 8 bytes - keep as-is + } +} + func filterEscapeBinaryUUID(attribute string, value uuid.UUID) string { bytes := value[:] // AD stores objectGUID with mixed endianness 🤪 - swap first 3 components if strings.EqualFold(attribute, "objectguid") { - bytes = []byte{ - value[3], value[2], value[1], value[0], // First component (4 bytes) - reverse - value[5], value[4], // Second component (2 bytes) - reverse - value[7], value[6], // Third component (2 bytes) - reverse - value[8], value[9], value[10], value[11], value[12], value[13], value[14], value[15], // Last 8 bytes - keep as-is - } + bytes = SwapObjectGUIDBytes(bytes) } var filtered strings.Builder @@ -656,10 +669,18 @@ func (i *Identity) getUserFindFilter(query, tenantID string) string { for _, attr := range searchAttrs { filter = fmt.Sprintf("%s(%s=%s)", filter, attr, squery) } - // substring search for UUID is not possible - filter = fmt.Sprintf("(|%s(%s=%s))", filter, i.User.Schema.ID, ldap.EscapeFilter(query)) + if i.Group.Schema.IDIsOctetString { + // try parsing query as uuid + idFilter, err := filterEscapeAttribute(i.User.Schema.ID, i.User.Schema.IDIsOctetString, query) + if err == nil { + filter = fmt.Sprintf("%s(%s=%s)", filter, i.User.Schema.ID, idFilter) + } + } else { + // substring search for UUID is not possible + filter = fmt.Sprintf("%s(%s=%s)", filter, i.User.Schema.ID, ldap.EscapeFilter(query)) + } - return fmt.Sprintf("(&%s(objectclass=%s)%s%s)", + return fmt.Sprintf("(&%s(objectclass=%s)%s(|%s))", i.User.Filter, i.User.Objectclass, i.tenantFilter(tenantID), @@ -687,8 +708,16 @@ func (i *Identity) getGroupFindFilter(query string) string { for _, attr := range searchAttrs { filter = fmt.Sprintf("%s(%s=%s)", filter, attr, squery) } - // substring search for UUID is not possible - filter = fmt.Sprintf("%s(%s=%s)", filter, i.Group.Schema.ID, ldap.EscapeFilter(query)) + if i.Group.Schema.IDIsOctetString { + // try parsing query as uuid + idFilter, err := filterEscapeAttribute(i.Group.Schema.ID, i.Group.Schema.IDIsOctetString, query) + if err == nil { + filter = fmt.Sprintf("%s(%s=%s)", filter, i.Group.Schema.ID, idFilter) + } + } else { + // substring search for UUID is not possible + filter = fmt.Sprintf("%s(%s=%s)", filter, i.Group.Schema.ID, ldap.EscapeFilter(query)) + } return fmt.Sprintf("(&%s(objectclass=%s)(|%s))", i.Group.Filter, diff --git a/vendor/go.etcd.io/etcd/api/v3/version/version.go b/vendor/go.etcd.io/etcd/api/v3/version/version.go index 9e7bc64c1..0a58729cf 100644 --- a/vendor/go.etcd.io/etcd/api/v3/version/version.go +++ b/vendor/go.etcd.io/etcd/api/v3/version/version.go @@ -26,7 +26,7 @@ import ( var ( // MinClusterVersion is the min cluster version this etcd binary is compatible with. MinClusterVersion = "3.0.0" - Version = "3.6.5" + Version = "3.6.6" APIVersion = "unknown" // Git SHA Value will be set during build diff --git a/vendor/modules.txt b/vendor/modules.txt index 0bff50601..e76ede160 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1370,7 +1370,7 @@ github.com/opencloud-eu/icap-client # github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76 ## explicit; go 1.18 github.com/opencloud-eu/libre-graph-api-go -# github.com/opencloud-eu/reva/v2 v2.39.3 +# github.com/opencloud-eu/reva/v2 v2.40.1 ## explicit; go 1.24.1 github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace github.com/opencloud-eu/reva/v2/cmd/revad/runtime @@ -2246,7 +2246,7 @@ go.etcd.io/bbolt go.etcd.io/bbolt/errors go.etcd.io/bbolt/internal/common go.etcd.io/bbolt/internal/freelist -# go.etcd.io/etcd/api/v3 v3.6.5 +# go.etcd.io/etcd/api/v3 v3.6.6 ## explicit; go 1.24 go.etcd.io/etcd/api/v3/authpb go.etcd.io/etcd/api/v3/etcdserverpb @@ -2255,7 +2255,7 @@ go.etcd.io/etcd/api/v3/mvccpb go.etcd.io/etcd/api/v3/v3rpc/rpctypes go.etcd.io/etcd/api/v3/version go.etcd.io/etcd/api/v3/versionpb -# go.etcd.io/etcd/client/pkg/v3 v3.6.5 +# go.etcd.io/etcd/client/pkg/v3 v3.6.6 ## explicit; go 1.24 go.etcd.io/etcd/client/pkg/v3/fileutil go.etcd.io/etcd/client/pkg/v3/logutil @@ -2264,7 +2264,7 @@ go.etcd.io/etcd/client/pkg/v3/tlsutil go.etcd.io/etcd/client/pkg/v3/transport go.etcd.io/etcd/client/pkg/v3/types go.etcd.io/etcd/client/pkg/v3/verify -# go.etcd.io/etcd/client/v3 v3.6.5 +# go.etcd.io/etcd/client/v3 v3.6.6 ## explicit; go 1.24 go.etcd.io/etcd/client/v3 go.etcd.io/etcd/client/v3/credentials