From a3255422b80e08f6acc193d7e60d1a1de67ca5f0 Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Fri, 2 Oct 2026 15:53:08 +0200 Subject: [PATCH] chore: bump reva to latest main (#3642) --- go.mod | 2 +- go.sum | 4 +- .../moveByFileId.feature | 6 +- .../services/authprovider/authprovider.go | 6 +- .../grpc/services/gateway/authprovider.go | 60 ++-- .../v2/pkg/auth/manager/guestlinks/errors.go | 61 ++++ .../pkg/auth/manager/guestlinks/guestlinks.go | 306 ++++++++++++++++++ .../reva/v2/pkg/auth/manager/loader/loader.go | 1 + .../reva/v2/pkg/conversions/role.go | 69 +++- .../reva/v2/pkg/errtypes/errtypes.go | 16 + .../reva/v2/pkg/rgrpc/status/innererror.go | 36 +++ .../reva/v2/pkg/rgrpc/status/status.go | 39 +-- vendor/modules.txt | 3 +- 13 files changed, 554 insertions(+), 55 deletions(-) create mode 100644 vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/errors.go create mode 100644 vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/guestlinks.go create mode 100644 vendor/github.com/opencloud-eu/reva/v2/pkg/rgrpc/status/innererror.go diff --git a/go.mod b/go.mod index 6507675e29..271b78b785 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/open-policy-agent/opa v1.19.1 github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 github.com/opencloud-eu/libre-graph-api-go v1.0.8 - github.com/opencloud-eu/reva/v2 v2.50.1-0.20261001091108-11d87fb6b985 + github.com/opencloud-eu/reva/v2 v2.50.1-0.20261002054620-6fb86feb3aa2 github.com/opensearch-project/opensearch-go/v4 v4.7.3 github.com/orcaman/concurrent-map v1.0.0 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 9c80b54c64..bf6466df2f 100644 --- a/go.sum +++ b/go.sum @@ -761,8 +761,8 @@ github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+l github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI= github.com/opencloud-eu/libre-graph-api-go v1.0.8 h1:VTkFLkLNna+T4RN6JUZnVQ1bqVAfPtMeORXbFzEYGKs= github.com/opencloud-eu/libre-graph-api-go v1.0.8/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU= -github.com/opencloud-eu/reva/v2 v2.50.1-0.20261001091108-11d87fb6b985 h1:9NpeJKVzjzMKhiTaBPz6SqtRiv6k67E5wKPPc0zg/8M= -github.com/opencloud-eu/reva/v2 v2.50.1-0.20261001091108-11d87fb6b985/go.mod h1:ngLsDakKnt8zCFCeULhuSZFESGy5NIuSq5/JGeOhPdU= +github.com/opencloud-eu/reva/v2 v2.50.1-0.20261002054620-6fb86feb3aa2 h1:ROTURL0i5+PSIjHB3nW59oroSfLM/bFcJEZq9Hb9GWA= +github.com/opencloud-eu/reva/v2 v2.50.1-0.20261002054620-6fb86feb3aa2/go.mod h1:ngLsDakKnt8zCFCeULhuSZFESGy5NIuSq5/JGeOhPdU= github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI= github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= diff --git a/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature b/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature index 30f1bc2bda..1b1be785ac 100644 --- a/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature +++ b/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature @@ -123,7 +123,7 @@ Feature: moving/renaming file using file id And we save it into "FILEID" When user "Alice" renames file with id "<>" to "renamed.txt" inside space "project-space" Then the HTTP status code should be "502" - And the value of the item "/d:error/s:message" in the response about user "Alice" should be "move:error: not supported: cannot move across spaces" + And the value of the item "/d:error/s:message" in the response about user "Alice" should be "move: error: not supported: cannot move across spaces" And for user "Alice" folder "/" of the space "Personal" should contain these files: | textfile.txt | But for user "Alice" folder "/" of the space "project-space" should not contain these files: @@ -283,7 +283,7 @@ Feature: moving/renaming file using file id And we save it into "FILEID" When user "Alice" renames file with id "<>" to "/renamedSecondProjectSpacetextfile.txt" inside space "first-project-space" Then the HTTP status code should be "502" - And the value of the item "/d:error/s:message" in the response about user "Alice" should be "move:error: not supported: cannot move across spaces" + And the value of the item "/d:error/s:message" in the response about user "Alice" should be "move: error: not supported: cannot move across spaces" And for user "Alice" folder "/" of the space "first-project-space" should contain these files: | firstProjectSpacetextfile.txt | And for user "Alice" folder "/" of the space "second-project-space" should contain these files: @@ -358,7 +358,7 @@ Feature: moving/renaming file using file id And we save it into "FILEID" When user "Alice" renames file with id "<>" to "/renamed.txt" inside space "Personal" Then the HTTP status code should be "502" - And the value of the item "/d:error/s:message" in the response about user "Alice" should be "move:error: not supported: cannot move across spaces" + And the value of the item "/d:error/s:message" in the response about user "Alice" should be "move: error: not supported: cannot move across spaces" And for user "Alice" folder "/" of the space "project-space" should contain these files: | textfile.txt | But for user "Alice" folder "/" of the space "Personal" should not contain these files: diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/authprovider/authprovider.go b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/authprovider/authprovider.go index 483c9ee66a..6065e78360 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/authprovider/authprovider.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/authprovider/authprovider.go @@ -139,8 +139,12 @@ func (s *service) Authenticate(ctx context.Context, req *provider.AuthenticateRe u, scope, err := s.authmgr.Authenticate(ctx, username, password) if err != nil { log.Debug().Str("client_id", username).Err(err).Msg("authsvc: error in Authenticate") + st := status.NewStatusFromErrType(ctx, "authsvc: error in Authenticate", err) + if entry := status.InnerErrorFromErr(err); entry != nil { + st.InnerError = entry + } return &provider.AuthenticateResponse{ - Status: status.NewStatusFromErrType(ctx, "authsvc: error in Authenticate", err), + Status: st, }, nil } log.Info().Msgf("user %s authenticated", u.Id) diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/gateway/authprovider.go b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/gateway/authprovider.go index afbbc897c9..116205b53c 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/gateway/authprovider.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/gateway/authprovider.go @@ -57,25 +57,9 @@ func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest ClientId: req.ClientId, ClientSecret: req.ClientSecret, } - res, err := c.Authenticate(ctx, authProviderReq) - switch { - case err != nil: - return &gateway.AuthenticateResponse{ - Status: status.NewInternal(ctx, fmt.Sprintf("gateway: error calling Authenticate for type: %s", req.Type)), - }, nil - case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: - fallthrough - case res.Status.Code == rpc.Code_CODE_UNAUTHENTICATED: - fallthrough - case res.Status.Code == rpc.Code_CODE_NOT_FOUND: - // normal failures, no need to log - return &gateway.AuthenticateResponse{ - Status: res.Status, - }, nil - case res.Status.Code != rpc.Code_CODE_OK: - return &gateway.AuthenticateResponse{ - Status: status.NewInternal(ctx, fmt.Sprintf("error authenticating credentials to auth provider for type: %s", req.Type)), - }, nil + res, callErr := c.Authenticate(ctx, authProviderReq) + if resp, done := translateProviderAuthenticateResult(ctx, req.Type, res, callErr); done { + return resp, nil } // validate valid userId @@ -109,7 +93,7 @@ func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest return res, nil } - if scope, ok := res.TokenScope["user"]; s.c.DisableHomeCreationOnLogin || !ok || scope.Role != authpb.Role_ROLE_OWNER || res.User.Id.Type == userpb.UserType_USER_TYPE_FEDERATED { + if scope, ok := res.TokenScope["user"]; s.c.DisableHomeCreationOnLogin || !ok || scope.Role != authpb.Role_ROLE_OWNER || res.User.Id.Type == userpb.UserType_USER_TYPE_FEDERATED || res.User.Id.Type == userpb.UserType_USER_TYPE_GUEST { gwRes := &gateway.AuthenticateResponse{ Status: status.NewOK(ctx), User: res.User, @@ -151,6 +135,42 @@ func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest return gwRes, nil } +// translateProviderAuthenticateResult inspects the result of calling the auth +// provider's Authenticate RPC and decides whether the gateway should return +// early with a translated response. +// +// Normal authentication failure statuses (CODE_UNAUTHENTICATED, +// CODE_PERMISSION_DENIED, CODE_NOT_FOUND, CODE_UNAVAILABLE) are passed +// through unchanged, preserving their Message, Trace and InnerError. Any +// other unexpected application status is turned into CODE_INTERNAL. +// +// returns done == true if the caller should return resp immediately. +func translateProviderAuthenticateResult(ctx context.Context, authType string, res *authpb.AuthenticateResponse, callErr error) (resp *gateway.AuthenticateResponse, done bool) { + switch { + case callErr != nil: + return &gateway.AuthenticateResponse{ + Status: status.NewInternal(ctx, fmt.Sprintf("gateway: error calling Authenticate for type: %s", authType)), + }, true + case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: + fallthrough + case res.Status.Code == rpc.Code_CODE_UNAUTHENTICATED: + fallthrough + case res.Status.Code == rpc.Code_CODE_NOT_FOUND: + fallthrough + case res.Status.Code == rpc.Code_CODE_UNAVAILABLE: + // normal failures, no need to log + return &gateway.AuthenticateResponse{ + Status: res.Status, + }, true + case res.Status.Code != rpc.Code_CODE_OK: + return &gateway.AuthenticateResponse{ + Status: status.NewInternal(ctx, fmt.Sprintf("error authenticating credentials to auth provider for type: %s", authType)), + }, true + } + + return nil, false +} + func (s *svc) WhoAmI(ctx context.Context, req *gateway.WhoAmIRequest) (*gateway.WhoAmIResponse, error) { u, _, err := s.tokenmgr.DismantleToken(ctx, req.Token) if err != nil { diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/errors.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/errors.go new file mode 100644 index 0000000000..dd49648e33 --- /dev/null +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/errors.go @@ -0,0 +1,61 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + +package guestlinks + +import ( + "encoding/json" + + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" +) + +const ( + innerErrorType = "opencloud_guest_link_error" + reasonSessionExpired = "session_expired" +) + +// errSessionExpired is returned when JWT expiry is the *only* validation +// failure. It implements errtypes.IsInvalidCredentials (-> CODE_UNAUTHENTICATED) +// and status.StatusInnerErrorProvider so that a safe, versioned JSON detail +// is attached to rpc.Status.InnerError. +type errSessionExpired struct { + shareID string +} + +func (e *errSessionExpired) Error() string { + return "guestlinks: session expired" +} + +// IsInvalidCredentials implements the errtypes.IsInvalidCredentials interface. +func (e *errSessionExpired) IsInvalidCredentials() {} + +// innerErrorPayload is the JSON payload shape for the guest-link session +// expired InnerError. +type innerErrorPayload struct { + Type string `json:"type"` + Reason string `json:"reason"` + ShareID string `json:"share_id"` +} + +// StatusInnerError implements status.StatusInnerErrorProvider. +func (e *errSessionExpired) StatusInnerError() *types.OpaqueEntry { + payload := innerErrorPayload{ + Type: innerErrorType, + Reason: reasonSessionExpired, + ShareID: e.shareID, + } + // encoding a static, safe struct: an error here can only happen on + // programmer error (e.g. unmarshalable field), never in practice. + b, err := json.Marshal(payload) + if err != nil { + return nil + } + return &types.OpaqueEntry{ + Decoder: "json", + Value: b, + } +} + +func newSessionExpiredError(shareID string) error { + return &errSessionExpired{shareID: shareID} +} diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/guestlinks.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/guestlinks.go new file mode 100644 index 0000000000..a8c63a233d --- /dev/null +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/guestlinks.go @@ -0,0 +1,306 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + +// Package guestlinks implements a Reva auth.Manager that authenticates +// OpenCloud guest-link sessions. +// +// It validates an OpenCloud guest-session JWT (issued by the OpenCloud +// proxy), re-validates the JWT's anchor collaborative share through the +// gateway GetShare API using a freshly minted service-account token, and +// returns the synthetic guest identity persisted as the share's grantee. +// +// See opencloud issue #3070 for the full design rationale. +package guestlinks + +import ( + "context" + "errors" + "net/mail" + "time" + + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/go-viper/mapstructure/v2" + "github.com/golang-jwt/jwt/v5" + "github.com/rs/zerolog" + + "github.com/opencloud-eu/reva/v2/pkg/auth" + "github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry" + "github.com/opencloud-eu/reva/v2/pkg/auth/scope" + "github.com/opencloud-eu/reva/v2/pkg/errtypes" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + "github.com/opencloud-eu/reva/v2/pkg/share" + "github.com/opencloud-eu/reva/v2/pkg/utils" +) + +const ( + jwtLeeway = 30 * time.Second + maxShareIDLength = 512 +) + +func init() { + registry.Register("guestlinks", New) +} + +// guestClaims are the required custom JWT claims of a guest-session token. +type guestClaims struct { + // the attribute in the JWT is called permissionId (for consistency with the + // graph API), for us it really is the "shareId" + ShareID string `json:"permissionId"` + jwt.RegisteredClaims +} + +// config holds the guestlinks auth manager configuration. +type config struct { + GatewayAddr string `mapstructure:"gateway_addr"` + JWTSecret string `mapstructure:"jwt_secret"` + ServiceAccountID string `mapstructure:"service_account_id"` + ServiceAccountSecret string `mapstructure:"service_account_secret"` +} + +func (c *config) validate() error { + switch { + case c.GatewayAddr == "": + return errors.New("guestlinks: gateway_addr must not be empty") + case c.JWTSecret == "": + return errors.New("guestlinks: jwt_secret must not be empty") + case c.ServiceAccountID == "": + return errors.New("guestlinks: service_account_id must not be empty") + case c.ServiceAccountSecret == "": + return errors.New("guestlinks: service_account_secret must not be empty") + } + return nil +} + +type manager struct { + c *config + log *zerolog.Logger +} + +func parseConfig(m map[string]any) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + return nil, errors.New("guestlinks: error decoding conf: " + err.Error()) + } + return c, nil +} + +// New returns a new guestlinks auth.Manager. +func New(m map[string]any, log *zerolog.Logger) (auth.Manager, error) { + mgr := &manager{log: log} + if err := mgr.Configure(m); err != nil { + return nil, err + } + return mgr, nil +} + +// Configure parses and validates the manager configuration. +func (m *manager) Configure(ml map[string]any) error { + c, err := parseConfig(ml) + if err != nil { + return err + } + if err := c.validate(); err != nil { + return err + } + m.c = c + return nil +} + +// Authenticate implements auth.Manager. +// +// clientID must be empty as the guestlinks credential is carried entirely in +// clientSecret, which must is the raw guest-session JWT. +func (m *manager) Authenticate(ctx context.Context, clientID, clientSecret string) (*userpb.User, map[string]*authpb.Scope, error) { + if clientID != "" { + m.logOutcome("invalid", "non-empty client_id") + return nil, nil, errtypes.InvalidCredentials("non-empty client_id") + } + + shareID, err := m.validateToken(clientSecret) + if err != nil { + if _, ok := errors.AsType[*errSessionExpired](err); ok { + m.logOutcome("expired", "token expired") + } else { + m.logOutcome("invalid", "token validation failed") + } + return nil, nil, err + } + + foundShare, err := m.lookupShare(ctx, shareID) + if err != nil { + return nil, nil, err + } + + u, err := guestUserFromShare(foundShare) + if err != nil { + m.logOutcome("share-invalid", "share grantee invalid") + return nil, nil, errtypes.InvalidCredentials("share grantee invalid") + } + + sc, err := scope.AddOwnerScope(nil) + if err != nil { + m.logOutcome("internal", "error building scope") + return nil, nil, errtypes.InternalError("guestlinks: error building token scope: " + err.Error()) + } + + m.logOutcome("success", "") + return u, sc, nil +} + +// validateToken validates the JWT contained in raw and returns the +// validated share_id. +// +// If the *only* validation failure is expiry, the returned error is a +// *errSessionExpired (still carrying the validated share_id), per the +// expiry-only classification rules. Any other validation failure is +// returned as a generic errtypes.InvalidCredentials with no detail. +func (m *manager) validateToken(raw string) (shareID string, err error) { + if raw == "" { + return "", errtypes.InvalidCredentials("empty token") + } + + claims := &guestClaims{} + parser := jwt.NewParser( + jwt.WithValidMethods([]string{"HS256"}), + jwt.WithoutClaimsValidation(), + ) + _, parseErr := parser.ParseWithClaims(raw, claims, func(t *jwt.Token) (any, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok || t.Method.Alg() != "HS256" { + return nil, errors.New("unexpected signing method") + } + return []byte(m.c.JWTSecret), nil + }) + if parseErr != nil { + return "", errtypes.InvalidCredentials("signature/algorithm/parse error") + } + + if claims.ShareID == "" || len(claims.ShareID) > maxShareIDLength { + return "", errtypes.InvalidCredentials("missing/malformed share_id") + } + if claims.IssuedAt == nil || claims.ExpiresAt == nil { + return "", errtypes.InvalidCredentials("missing iat/exp") + } + + now := time.Now() + + // iat in the future (beyond leeway) is structurally invalid. + if claims.IssuedAt.After(now.Add(jwtLeeway)) { + return "", errtypes.InvalidCredentials("invalid iat") + } + + // Everything but expiry has been validated at this point. Now check + // expiry last, so we can classify an expiry-only failure. + if now.After(claims.ExpiresAt.Add(jwtLeeway)) { + return claims.ShareID, newSessionExpiredError(claims.ShareID) + } + + return claims.ShareID, nil +} + +// Reads and re-validates the share associated with the authentication token +// using the service account. +func (m *manager) lookupShare(ctx context.Context, shareID string) (*collaboration.Share, error) { + gwc, err := pool.GetGatewayServiceClient(m.c.GatewayAddr) + if err != nil { + m.logOutcome("unavailable", "error getting gateway client") + return nil, errtypes.Unavailable("guestlinks: error getting gateway client: " + err.Error()) + } + + saCtx, err := utils.GetServiceUserContextWithContext(ctx, gwc, m.c.ServiceAccountID, m.c.ServiceAccountSecret) + if err != nil { + m.logOutcome("unavailable", "error minting service account token") + return nil, errtypes.Unavailable("guestlinks: error authenticating service account: " + err.Error()) + } + + getShareRes, err := gwc.GetShare(saCtx, &collaboration.GetShareRequest{ + Ref: &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{OpaqueId: shareID}, + }, + }, + }) + if err != nil { + m.logOutcome("unavailable", "error calling GetShare") + return nil, errtypes.Unavailable("guestlinks: error calling GetShare: " + err.Error()) + } + + switch getShareRes.GetStatus().GetCode() { + case rpc.Code_CODE_OK: + // fall through + case rpc.Code_CODE_NOT_FOUND, rpc.Code_CODE_PERMISSION_DENIED, rpc.Code_CODE_UNAUTHENTICATED: + m.logOutcome("share-invalid", "share not found/inaccessible") + return nil, errtypes.InvalidCredentials("share not found/inaccessible") + case rpc.Code_CODE_UNAVAILABLE: + m.logOutcome("unavailable", "share provider unavailable") + return nil, errtypes.Unavailable("guestlinks: share provider unavailable") + default: + m.logOutcome("internal", "unexpected GetShare status") + return nil, errtypes.InternalError("guestlinks: unexpected GetShare status: " + getShareRes.GetStatus().GetCode().String()) + } + + foundShare := getShareRes.GetShare() + if foundShare == nil { + m.logOutcome("share-invalid", "nil share") + return nil, errtypes.InvalidCredentials("nil share") + } + if share.IsExpired(foundShare) { + m.logOutcome("share-invalid", "share expired") + return nil, errtypes.InvalidCredentials("share expired") + } + + return foundShare, nil +} + +// guestUserFromShare builds the synthetic guest user from the persisted +// share grantee. It never trusts JWT claims for identity data. +func guestUserFromShare(s *collaboration.Share) (*userpb.User, error) { + grantee := s.GetGrantee() + if grantee.GetType() != storageprovider.GranteeType_GRANTEE_TYPE_USER { + return nil, errors.New("guestlinks: grantee is not a user") + } + + uid := grantee.GetUserId() + if uid == nil || uid.GetOpaqueId() == "" { + return nil, errors.New("guestlinks: incomplete grantee user id") + } + if uid.GetType() != userpb.UserType_USER_TYPE_GUEST { + return nil, errors.New("guestlinks: grantee is not a guest user") + } + + addr, err := mail.ParseAddress(uid.GetOpaqueId()) + if err != nil || addr.Name != "" || addr.Address != uid.GetOpaqueId() { + return nil, errors.New("guestlinks: grantee opaque id is not a bare email address") + } + email := uid.GetOpaqueId() + + u := &userpb.User{ + Id: &userpb.UserId{ + Idp: uid.GetIdp(), + OpaqueId: uid.GetOpaqueId(), + Type: userpb.UserType_USER_TYPE_GUEST, + TenantId: uid.GetTenantId(), + ExternalIdentities: uid.GetExternalIdentities(), + }, + Username: email, + DisplayName: email, + } + + // TODO: OpenCloud usually, attaches a user role to the token, how can we do that here? + + return u, nil +} + +func (m *manager) logOutcome(outcome, detail string) { + if m.log == nil { + return + } + ev := m.log.Debug() + if outcome != "success" { + ev = m.log.Info() + } + ev.Str("outcome", outcome).Str("detail", detail).Msg("guestlinks: authenticate outcome") +} diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/loader/loader.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/loader/loader.go index a77d18352b..1a78eeb4d8 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/loader/loader.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/loader/loader.go @@ -22,6 +22,7 @@ import ( // Load core authentication managers. _ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/appauth" _ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/demo" + _ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks" _ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/impersonator" _ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/json" _ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/ldap" diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/conversions/role.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/conversions/role.go index 92f49ea825..13eff0898e 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/conversions/role.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/conversions/role.go @@ -52,16 +52,25 @@ const ( RoleEditorWithVersions = "editor-with-versions" // RoleEditorListGrants grants editor permission on a resource, including folders. RoleEditorListGrants = "editor-list-grants" + // RoleEditorListGrantsWithVersions grants editor permission on a resource, including folders, and list versions. + RoleEditorListGrantsWithVersions = "editor-list-grants-with-versions" // RoleSpaceEditor grants editor permission on a space. RoleSpaceEditor = "spaceeditor" // RoleSpaceEditorWithoutVersions grants editor permission without list/restore versions on a space. RoleSpaceEditorWithoutVersions = "spaceeditor-without-versions" + // RoleSpaceEditorWithoutTrashbin grants editor permission without list/restore resources in trashbin on a space. + RoleSpaceEditorWithoutTrashbin = "spaceeditor-without-trashbin" + // RoleSpaceEditorWithoutVersionsWithoutTrashbin grants editor permission without list/restore versions + // and without list/restore resources in trashbin on a space. + RoleSpaceEditorWithoutVersionsWithoutTrashbin = "spaceeditor-without-versions-without-trashbin" // RoleFileEditor grants editor permission on a single file. RoleFileEditor = "file-editor" // RoleFileEditorWithVersions grants editor permission on a single file, including list/restore versions. RoleFileEditorWithVersions = "file-editor-with-versions" // RoleFileEditorListGrants grants editor permission on a single file. RoleFileEditorListGrants = "file-editor-list-grants" + // RoleFileEditorListGrantsWithVersions grants editor permission on a single file and list versions. + RoleFileEditorListGrantsWithVersions = "file-editor-list-grants-with-versions" // RoleCoowner grants co-owner permissions on a resource. RoleCoowner = "coowner" // RoleEditorLite grants permission to upload and download to a resource. @@ -186,14 +195,24 @@ func RoleFromName(name string) *Role { return NewEditorWithVersionsRole() case RoleEditorListGrants: return NewEditorListGrantsRole() + case RoleEditorListGrantsWithVersions: + return NewEditorListGrantsWithVersionsRole() + case RoleSpaceEditorWithoutVersions: + return NewSpaceEditorWithoutVersionsRole() case RoleSpaceEditor: return NewSpaceEditorRole() + case RoleSpaceEditorWithoutTrashbin: + return NewSpaceEditorWithoutTrashbinRole() + case RoleSpaceEditorWithoutVersionsWithoutTrashbin: + return NewSpaceEditorWithoutVersionsWithoutTrashbinRole() case RoleFileEditor: return NewFileEditorRole() case RoleFileEditorWithVersions: return NewFileEditorWithVersionsRole() case RoleFileEditorListGrants: return NewFileEditorListGrantsRole() + case RoleFileEditorListGrantsWithVersions: + return NewFileEditorListGrantsWithVersionsRole() case RoleUploader: return NewUploaderRole() case RoleManager: @@ -311,6 +330,15 @@ func NewEditorListGrantsRole() *Role { return role } +// NewEditorListGrantsWithVersionsRole creates an editor role that can list the invited people +// and the file versions of a resource, including folders. +func NewEditorListGrantsWithVersionsRole() *Role { + role := NewEditorListGrantsRole() + role.Name = RoleEditorListGrantsWithVersions + role.cS3ResourcePermissions.ListFileVersions = true + return role +} + // NewEditorWithVersionsRole creates an editor role including list/restore versions. `sharing` indicates if sharing permission should be added func NewEditorWithVersionsRole() *Role { role := NewEditorRole() @@ -346,8 +374,18 @@ func NewSpaceEditorRole() *Role { // NewSpaceEditorWithoutVersionsRole creates an editor without list/restore versions role func NewSpaceEditorWithoutVersionsRole() *Role { + role := NewSpaceEditorWithoutVersionsWithoutTrashbinRole() + role.Name = RoleSpaceEditorWithoutVersions + role.cS3ResourcePermissions.ListRecycle = true + role.cS3ResourcePermissions.RestoreRecycleItem = true + return role +} + +// NewSpaceEditorWithoutVersionsWithoutTrashbinRole creates an editor role without list/restore +// versions and without list/restore resources in the trashbin on a space. +func NewSpaceEditorWithoutVersionsWithoutTrashbinRole() *Role { return &Role{ - Name: RoleSpaceEditorWithoutVersions, + Name: RoleSpaceEditorWithoutVersionsWithoutTrashbin, cS3ResourcePermissions: &provider.ResourcePermissions{ CreateContainer: true, Delete: true, @@ -357,15 +395,23 @@ func NewSpaceEditorWithoutVersionsRole() *Role { InitiateFileUpload: true, ListContainer: true, ListGrants: true, - ListRecycle: true, Move: true, - RestoreRecycleItem: true, Stat: true, }, ocsPermissions: PermissionRead | PermissionCreate | PermissionWrite | PermissionDelete, } } +// NewSpaceEditorWithoutTrashbinRole creates an editor role without list/restore resources +// in the trashbin on a space. +func NewSpaceEditorWithoutTrashbinRole() *Role { + role := NewSpaceEditorWithoutVersionsWithoutTrashbinRole() + role.Name = RoleSpaceEditorWithoutTrashbin + role.cS3ResourcePermissions.ListFileVersions = true + role.cS3ResourcePermissions.RestoreFileVersion = true + return role +} + // NewFileEditorRole creates a file-editor role func NewFileEditorRole() *Role { p := PermissionRead | PermissionWrite @@ -392,6 +438,15 @@ func NewFileEditorListGrantsRole() *Role { return role } +// NewFileEditorListGrantsWithVersionsRole creates a file-editor role that can list the invited +// people and the file versions of a single file. +func NewFileEditorListGrantsWithVersionsRole() *Role { + role := NewFileEditorListGrantsRole() + role.Name = RoleFileEditorListGrantsWithVersions + role.cS3ResourcePermissions.ListFileVersions = true + return role +} + // NewFileEditorWithVersionsRole creates a file-editor role including list/restore versions func NewFileEditorWithVersionsRole() *Role { role := NewFileEditorRole() @@ -626,8 +681,11 @@ func RoleFromResourcePermissions(rp *provider.ResourcePermissions, islink bool) rp.InitiateFileDownload { r.ocsPermissions |= PermissionRead } + // A role without trashbin access has no RestoreRecycleItem, so writing had to be + // inferred from Delete instead - otherwise the *WithoutTrashbin space editor roles + // come back without PermissionWrite and the web frontend renders them read-only. if rp.InitiateFileUpload && - rp.RestoreRecycleItem { + (rp.RestoreRecycleItem || (rp.Delete && !rp.ListRecycle)) { r.ocsPermissions |= PermissionWrite } if rp.Stat && @@ -647,6 +705,9 @@ func RoleFromResourcePermissions(rp *provider.ResourcePermissions, islink bool) r.Name = RoleEditor if rp.ListGrants { r.Name = RoleEditorListGrants + if rp.ListFileVersions { + r.Name = RoleEditorListGrantsWithVersions + } } if rp.RemoveGrant { r.Name = RoleManager diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/errtypes/errtypes.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/errtypes/errtypes.go index 3b349f82f4..e34fb56a68 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/errtypes/errtypes.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/errtypes/errtypes.go @@ -216,96 +216,112 @@ func (e Unavailable) IsUnavailable() {} // to specify that a resource is not found. type IsNotFound interface { IsNotFound() + error } // IsAlreadyExists is the interface to implement // to specify that a resource already exists. type IsAlreadyExists interface { IsAlreadyExists() + error } // IsInternalError is the interface to implement // to specify that there was some internal error type IsInternalError interface { IsInternalError() + error } // IsUserRequired is the interface to implement // to specify that a user is required. type IsUserRequired interface { IsUserRequired() + error } // IsInvalidCredentials is the interface to implement // to specify that credentials were wrong. type IsInvalidCredentials interface { IsInvalidCredentials() + error } // IsNotSupported is the interface to implement // to specify that an action is not supported. type IsNotSupported interface { IsNotSupported() + error } // IsPermissionDenied is the interface to implement // to specify that an action is denied. type IsPermissionDenied interface { IsPermissionDenied() + error } // IsLocked is the interface to implement // to specify that a resource is locked. type IsLocked interface { IsLocked() + error } // IsAborted is the interface to implement // to specify that a request was aborted. type IsAborted interface { IsAborted() + error } // IsPreconditionFailed is the interface to implement // to specify that a precondition failed. type IsPreconditionFailed interface { IsPreconditionFailed() + error } // IsPartialContent is the interface to implement // to specify that the client request has partial data. type IsPartialContent interface { IsPartialContent() + error } // IsBadRequest is the interface to implement // to specify that the server cannot or will not process the request. type IsBadRequest interface { IsBadRequest() + error } // IsChecksumMismatch is the interface to implement // to specify that a checksum does not match. type IsChecksumMismatch interface { IsChecksumMismatch() + error } // IsInsufficientStorage is the interface to implement // to specify that there is insufficient storage. type IsInsufficientStorage interface { IsInsufficientStorage() + error } // IsTooEarly is the interface to implement // to specify that there is some not finished job over resource is still in process. type IsTooEarly interface { IsTooEarly() + error } // IsUnavailable is the interface to implement to specify that a backend service is // temporarily unavailable and the caller should retry. type IsUnavailable interface { IsUnavailable() + error } // NewErrtypeFromStatus maps a rpc status to an errtype diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/rgrpc/status/innererror.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/rgrpc/status/innererror.go new file mode 100644 index 0000000000..721be300a5 --- /dev/null +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/rgrpc/status/innererror.go @@ -0,0 +1,36 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "errors" + + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" +) + +// StatusInnerErrorProvider is an opt-in interface for error types that carry +// safe, already-encoded failure details meant to be transported in +// rpc.Status.InnerError. +type StatusInnerErrorProvider interface { + // StatusInnerError returns an already encoded OpaqueEntry (decoder and + // value) to be attached to rpc.Status.InnerError, or nil if no detail + // should be attached. + StatusInnerError() *types.OpaqueEntry +} + +// InnerErrorFromErr looks for an error implementing StatusInnerErrorProvider +// in err's chain (using errors.As, so wrapped typed errors are supported) +// and returns the safe entry it provides. +func InnerErrorFromErr(err error) *types.OpaqueEntry { + if err == nil { + return nil + } + + var provider StatusInnerErrorProvider + if !errors.As(err, &provider) { + return nil + } + + return provider.StatusInnerError() +} diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/rgrpc/status/status.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/rgrpc/status/status.go index 07eb32e253..0143be7fe4 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/rgrpc/status/status.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/rgrpc/status/status.go @@ -175,35 +175,28 @@ func NewStatusFromErrType(ctx context.Context, msg string, err error) *rpc.Statu switch e := err.(type) { case nil: return NewOK(ctx) - case errtypes.NotFound: - return NewNotFound(ctx, msg+": "+err.Error()) case errtypes.IsNotFound: - return NewNotFound(ctx, msg+": "+err.Error()) - case errtypes.AlreadyExists: - return NewAlreadyExists(ctx, err, msg+": "+err.Error()) - case errtypes.InvalidCredentials: - return NewPermissionDenied(ctx, e, msg+": "+err.Error()) + return NewNotFound(ctx, msg+": "+e.Error()) + case errtypes.IsAlreadyExists: + return NewAlreadyExists(ctx, e, msg+": "+e.Error()) case errtypes.IsInvalidCredentials: - // TODO this maps badly - return NewUnauthenticated(ctx, err, msg+": "+err.Error()) - case errtypes.PermissionDenied: - return NewPermissionDenied(ctx, e, msg+": "+err.Error()) - case errtypes.Locked: + return NewUnauthenticated(ctx, e, msg+": "+e.Error()) + case errtypes.IsPermissionDenied: + return NewPermissionDenied(ctx, e, msg+": "+e.Error()) + case errtypes.IsLocked: // FIXME a locked error returns the current lockid // FIXME use NewAborted as per the rpc code docs - return NewLocked(ctx, msg+": "+err.Error()) - case errtypes.Aborted: - return NewAborted(ctx, e, msg+": "+err.Error()) - case errtypes.PreconditionFailed: - return NewFailedPrecondition(ctx, e, msg+": "+err.Error()) + return NewLocked(ctx, msg+": "+e.Error()) + case errtypes.IsAborted: + return NewAborted(ctx, e, msg+": "+e.Error()) + case errtypes.IsPreconditionFailed: + return NewFailedPrecondition(ctx, e, msg+": "+e.Error()) case errtypes.IsNotSupported: - return NewUnimplemented(ctx, err, msg+":"+err.Error()) - case errtypes.BadRequest: - return NewInvalid(ctx, msg+":"+err.Error()) - case errtypes.Unavailable: - return NewUnavailable(ctx, msg+": "+err.Error()) + return NewUnimplemented(ctx, e, msg+": "+e.Error()) + case errtypes.IsBadRequest: + return NewInvalid(ctx, msg+": "+e.Error()) case errtypes.IsUnavailable: - return NewUnavailable(ctx, msg+": "+err.Error()) + return NewUnavailable(ctx, msg+": "+e.Error()) } // map GRPC status codes coming from the auth middleware diff --git a/vendor/modules.txt b/vendor/modules.txt index 9c3fd0610d..bd32502f4e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1362,7 +1362,7 @@ github.com/opencloud-eu/icap-client # github.com/opencloud-eu/libre-graph-api-go v1.0.8 ## explicit; go 1.23 github.com/opencloud-eu/libre-graph-api-go -# github.com/opencloud-eu/reva/v2 v2.50.1-0.20261001091108-11d87fb6b985 +# github.com/opencloud-eu/reva/v2 v2.50.1-0.20261002054620-6fb86feb3aa2 ## explicit; go 1.26.0 github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace github.com/opencloud-eu/reva/v2/cmd/revad/runtime @@ -1472,6 +1472,7 @@ github.com/opencloud-eu/reva/v2/pkg/appctx github.com/opencloud-eu/reva/v2/pkg/auth github.com/opencloud-eu/reva/v2/pkg/auth/manager/appauth github.com/opencloud-eu/reva/v2/pkg/auth/manager/demo +github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks github.com/opencloud-eu/reva/v2/pkg/auth/manager/impersonator github.com/opencloud-eu/reva/v2/pkg/auth/manager/json github.com/opencloud-eu/reva/v2/pkg/auth/manager/ldap