chore: bump reva to latest main (#3642)

This commit is contained in:
Michael Barz authored and GitHub committed 2026-10-02 15:53:08 +02:00
1 parent c10b106f1b
commit a3255422b8
13 files changed
+554 -55

No files matched your search

+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
@@ -123,7 +123,7 @@ Feature: moving/renaming file using file id
And we save it into "FILEID"
When user "Alice" renames file with id "<<FILEID>>" 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 "<<FILEID>>" 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 "<<FILEID>>" 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:
@@ -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)
@@ -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 {
@@ -0,0 +1,61 @@
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
// 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}
}
@@ -0,0 +1,306 @@
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
// 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")
}
@@ -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"
+65 -4
View File
@@ -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
+16
View File
@@ -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
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
// 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()
}
+16 -23
View File
@@ -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
+2 -1
View File
@@ -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