[full-ci] revaBump-v2.40.1 (#1927)

* revaBump-v2.40.0

* adapt tests

* bring-#442

* adapt tests

* bring-#444

* ocm fixes

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* adapt tests

* adapt unit tests

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* revaUpdate-2.40.1

* update opencloud-version-4.0.0-rc.3

---------

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
Co-authored-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
Viktor Scharf
2025-11-28 17:34:12 +01:00
committed by GitHub
parent 043a5cf951
commit 2bdd98f5cf
37 changed files with 889 additions and 373 deletions

8
go.mod
View File

@@ -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

16
go.sum
View File

@@ -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=

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 <id>@<provider>")
}
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 {

View File

@@ -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() {

View File

@@ -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())

View File

@@ -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?://.+$';
}
/**

View File

@@ -2440,14 +2440,6 @@ class FeatureContext extends BehatVariablesContext {
],
"parameter" => []
],
[
"code" => "%identities_issuer_id_pattern%",
"function" => [
__NAMESPACE__ . '\TestHelpers\GraphHelper',
"getFederatedUserRegex"
],
"parameter" => []
],
[
"code" => "%uuidv4_pattern%",
"function" => [

View File

@@ -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');

View File

@@ -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"
}
]
}
}
}

View File

@@ -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%$"
}
}
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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"`
}

View File

@@ -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 <id>@<provider>")
}
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) {

View File

@@ -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 <id>@<provider>")
}
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 {

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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())

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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 }

View File

@@ -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 <opaque-id>@<idp> 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)
}

View File

@@ -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")
}

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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),
)
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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

8
vendor/modules.txt vendored
View File

@@ -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