github.com/cs3org/reva/v2@v2.27.7/internal/grpc/interceptors/auth/scope.go (about)

     1  // Copyright 2018-2021 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  package auth
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"strings"
    25  	"time"
    26  
    27  	appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
    28  	appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
    29  	authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
    30  	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
    31  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    32  	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    33  	collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
    34  	link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
    35  	ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
    36  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    37  	registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
    38  	"github.com/cs3org/reva/v2/pkg/appctx"
    39  	"github.com/cs3org/reva/v2/pkg/auth/scope"
    40  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    41  	"github.com/cs3org/reva/v2/pkg/errtypes"
    42  	statuspkg "github.com/cs3org/reva/v2/pkg/rgrpc/status"
    43  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    44  	"github.com/cs3org/reva/v2/pkg/storagespace"
    45  	"github.com/cs3org/reva/v2/pkg/token"
    46  	"github.com/cs3org/reva/v2/pkg/utils"
    47  	"google.golang.org/grpc/metadata"
    48  )
    49  
    50  const (
    51  	scopeDelimiter       = "#"
    52  	scopeCacheExpiration = 3600
    53  )
    54  
    55  func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[string]*authpb.Scope, user *userpb.User, gatewayAddr string, mgr token.Manager) error {
    56  	log := appctx.GetLogger(ctx)
    57  	client, err := pool.GetGatewayServiceClient(gatewayAddr)
    58  	if err != nil {
    59  		return err
    60  	}
    61  
    62  	if ref, ok := extractRef(req, tokenScope); ok {
    63  		// The request is for a storage reference. This can be the case for multiple scenarios:
    64  		// - If the path is not empty, the request might be coming from a share where the accessor is
    65  		//   trying to impersonate the owner, since the share manager doesn't know the
    66  		//   share path.
    67  		// - If the ID not empty, the request might be coming from
    68  		//   - a resource present inside a shared folder, or
    69  		//   - a share created for a lightweight account after the token was minted.
    70  		log.Info().Msgf("resolving storage reference to check token scope %s", ref.String())
    71  		for k := range tokenScope {
    72  			switch {
    73  			case strings.HasPrefix(k, "publicshare"):
    74  				if err = resolvePublicShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
    75  					return nil
    76  				}
    77  
    78  			case strings.HasPrefix(k, "share"):
    79  				if err = resolveUserShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
    80  					return nil
    81  				}
    82  
    83  			case strings.HasPrefix(k, "lightweight"):
    84  				if err = resolveLightweightScope(ctx, ref, tokenScope[k], user, client, mgr); err == nil {
    85  					return nil
    86  				}
    87  			case strings.HasPrefix(k, "ocmshare"):
    88  				if err = resolveOCMShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
    89  					return nil
    90  				}
    91  			}
    92  			log.Err(err).Interface("ref", ref).Interface("scope", k).Msg("error resolving reference under scope")
    93  		}
    94  
    95  	} else if ref, ok := extractShareRef(req); ok {
    96  		// It's a share ref
    97  		// The request might be coming from a share created for a lightweight account
    98  		// after the token was minted.
    99  		log.Info().Msgf("resolving share reference against received shares to verify token scope %+v", ref.String())
   100  		for k := range tokenScope {
   101  			if strings.HasPrefix(k, "lightweight") {
   102  				// Check if this ID is cached
   103  				key := "lw:" + user.Id.OpaqueId + scopeDelimiter + ref.GetId().OpaqueId
   104  				if _, err := scopeExpansionCache.Get(key); err == nil {
   105  					return nil
   106  				}
   107  
   108  				shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
   109  				if err != nil || shares.Status.Code != rpc.Code_CODE_OK {
   110  					log.Warn().Err(err).Msg("error listing received shares")
   111  					continue
   112  				}
   113  				for _, s := range shares.Shares {
   114  					shareKey := "lw:" + user.Id.OpaqueId + scopeDelimiter + s.Share.Id.OpaqueId
   115  					_ = scopeExpansionCache.SetWithExpire(shareKey, nil, scopeCacheExpiration*time.Second)
   116  
   117  					if ref.GetId() != nil && ref.GetId().OpaqueId == s.Share.Id.OpaqueId {
   118  						return nil
   119  					}
   120  					if key := ref.GetKey(); key != nil && (utils.UserEqual(key.Owner, s.Share.Owner) || utils.UserEqual(key.Owner, s.Share.Creator)) &&
   121  						utils.ResourceIDEqual(key.ResourceId, s.Share.ResourceId) && utils.GranteeEqual(key.Grantee, s.Share.Grantee) {
   122  						return nil
   123  					}
   124  				}
   125  			}
   126  		}
   127  	}
   128  
   129  	return errtypes.PermissionDenied(fmt.Sprintf("access to resource %+v not allowed within the assigned scope", req))
   130  }
   131  
   132  func resolveLightweightScope(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, user *userpb.User, client gateway.GatewayAPIClient, mgr token.Manager) error {
   133  	// Check if this ref is cached
   134  	key := "lw:" + user.Id.OpaqueId + scopeDelimiter + getRefKey(ref)
   135  	if _, err := scopeExpansionCache.Get(key); err == nil {
   136  		return nil
   137  	}
   138  
   139  	shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
   140  	if err != nil || shares.Status.Code != rpc.Code_CODE_OK {
   141  		return errtypes.InternalError("error listing received shares")
   142  	}
   143  
   144  	for _, share := range shares.Shares {
   145  		shareKey := "lw:" + user.Id.OpaqueId + scopeDelimiter + storagespace.FormatResourceID(share.Share.ResourceId)
   146  		_ = scopeExpansionCache.SetWithExpire(shareKey, nil, scopeCacheExpiration*time.Second)
   147  
   148  		if ref.ResourceId != nil && utils.ResourceIDEqual(share.Share.ResourceId, ref.ResourceId) {
   149  			return nil
   150  		}
   151  		if ok, err := checkIfNestedResource(ctx, ref, share.Share.ResourceId, client, mgr); err == nil && ok {
   152  			_ = scopeExpansionCache.SetWithExpire(key, nil, scopeCacheExpiration*time.Second)
   153  			return nil
   154  		}
   155  	}
   156  
   157  	return errtypes.PermissionDenied("request is not for a nested resource")
   158  }
   159  
   160  func resolvePublicShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
   161  	var share link.PublicShare
   162  	err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share)
   163  	if err != nil {
   164  		return err
   165  	}
   166  
   167  	if err := checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr); err == nil {
   168  		return nil
   169  	}
   170  
   171  	// Some services like wopi don't access the shared resource relative to the
   172  	// share root but instead relative to the shared resources parent.
   173  	return checkRelativeReference(ctx, ref, share.ResourceId, client)
   174  }
   175  
   176  func resolveOCMShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
   177  	var share ocmv1beta1.Share
   178  	if err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share); err != nil {
   179  		return err
   180  	}
   181  
   182  	// for ListOCMSharesRequest, the ref resource id is empty and we set path to . to indicate the root of the share
   183  	if ref.GetResourceId() == nil && ref.Path == "." {
   184  		ref.ResourceId = share.GetResourceId()
   185  	}
   186  
   187  	if err := checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr); err == nil {
   188  		return nil
   189  	}
   190  
   191  	// Some services like wopi don't access the shared resource relative to the
   192  	// share root but instead relative to the shared resources parent.
   193  	return checkRelativeReference(ctx, ref, share.ResourceId, client)
   194  }
   195  
   196  // checkRelativeReference checks if the shared resource is being accessed via a relative reference
   197  // e.g.:
   198  // storage: abcd, space: efgh
   199  // /root (id: efgh)
   200  // - New file.txt (id: ijkl) <- shared resource
   201  //
   202  // If the requested reference looks like this:
   203  // Reference{ResourceId: {StorageId: "abcd", SpaceId: "efgh"}, Path: "./New file.txt"}
   204  // then the request is considered relative and this function would return true.
   205  // Only references which are relative to the immediate parent of a resource are considered valid.
   206  func checkRelativeReference(ctx context.Context, requested *provider.Reference, sharedResourceID *provider.ResourceId, client gateway.GatewayAPIClient) error {
   207  	sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: sharedResourceID}})
   208  	if err != nil {
   209  		return err
   210  	}
   211  	if sRes.Status.Code != rpc.Code_CODE_OK {
   212  		return statuspkg.NewErrorFromCode(sRes.Status.Code, "auth interceptor")
   213  	}
   214  
   215  	sharedResource := sRes.Info
   216  
   217  	// Is this a shared space
   218  	if sharedResource.ParentId == nil {
   219  		// Is the requested resource part of the shared space?
   220  		if requested.ResourceId.StorageId != sharedResource.Id.StorageId || requested.ResourceId.SpaceId != sharedResource.Id.SpaceId {
   221  			return errtypes.PermissionDenied("space access forbidden via public link")
   222  		}
   223  	} else {
   224  		parentID := sharedResource.ParentId
   225  		parentID.StorageId = sharedResource.Id.StorageId
   226  
   227  		if !utils.ResourceIDEqual(parentID, requested.ResourceId) && utils.MakeRelativePath(sharedResource.Path) != requested.Path {
   228  			return errtypes.PermissionDenied("access forbidden via public link")
   229  		}
   230  	}
   231  
   232  	key := storagespace.FormatResourceID(sharedResourceID) + scopeDelimiter + getRefKey(requested)
   233  	_ = scopeExpansionCache.SetWithExpire(key, nil, scopeCacheExpiration*time.Second)
   234  	return nil
   235  }
   236  
   237  func resolveUserShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
   238  	var share collaboration.Share
   239  	err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	return checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr)
   245  }
   246  
   247  func checkCacheForNestedResource(ctx context.Context, ref *provider.Reference, resource *provider.ResourceId, client gateway.GatewayAPIClient, mgr token.Manager) error {
   248  	// Check if this ref is cached
   249  	key := storagespace.FormatResourceID(resource) + scopeDelimiter + getRefKey(ref)
   250  	if _, err := scopeExpansionCache.Get(key); err == nil {
   251  		return nil
   252  	}
   253  
   254  	if ok, err := checkIfNestedResource(ctx, ref, resource, client, mgr); err == nil && ok {
   255  		_ = scopeExpansionCache.SetWithExpire(key, nil, scopeCacheExpiration*time.Second)
   256  		return nil
   257  	}
   258  
   259  	return errtypes.PermissionDenied("request is not for a nested resource")
   260  }
   261  
   262  func checkIfNestedResource(ctx context.Context, ref *provider.Reference, parent *provider.ResourceId, client gateway.GatewayAPIClient, mgr token.Manager) (bool, error) {
   263  	// Since the resource ID is obtained from the scope, the current token
   264  	// has access to it.
   265  	statResponse, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: parent}})
   266  	if err != nil {
   267  		return false, err
   268  	}
   269  	if statResponse.GetStatus().GetCode() != rpc.Code_CODE_OK {
   270  		return false, statuspkg.NewErrorFromCode(statResponse.Status.Code, "auth interceptor")
   271  	}
   272  
   273  	pathResp, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: statResponse.GetInfo().GetId()})
   274  	if err != nil {
   275  		return false, err
   276  	}
   277  	if pathResp.Status.Code != rpc.Code_CODE_OK {
   278  		return false, statuspkg.NewErrorFromCode(pathResp.Status.Code, "auth interceptor")
   279  	}
   280  	parentPath := pathResp.Path
   281  
   282  	childPath := ref.GetPath()
   283  	if childPath != "" && childPath != "." && strings.HasPrefix(childPath, parentPath) {
   284  		// if the request is relative from the root, we can return directly
   285  		return true, nil
   286  	}
   287  
   288  	// The request is not relative to the root. We need to find out if the requested resource is child of the `parent` (coming from token scope)
   289  	// We mint a token as the owner of the public share and try to stat the reference
   290  	// TODO(ishank011): We need to find a better alternative to this
   291  	// NOTE: did somebody say service accounts? ...
   292  
   293  	var user *userpb.User
   294  	if statResponse.GetInfo().GetOwner().GetType() == userpb.UserType_USER_TYPE_SPACE_OWNER {
   295  		// fake a space owner user
   296  		user = &userpb.User{
   297  			Id: statResponse.GetInfo().GetOwner(),
   298  		}
   299  	} else {
   300  		userResp, err := client.GetUser(ctx, &userpb.GetUserRequest{UserId: statResponse.Info.Owner, SkipFetchingUserGroups: true})
   301  		if err != nil || userResp.Status.Code != rpc.Code_CODE_OK {
   302  			return false, err
   303  		}
   304  		user = userResp.User
   305  	}
   306  
   307  	scope, err := scope.AddOwnerScope(map[string]*authpb.Scope{})
   308  	if err != nil {
   309  		return false, err
   310  	}
   311  	token, err := mgr.MintToken(ctx, user, scope)
   312  	if err != nil {
   313  		return false, err
   314  	}
   315  	ctx = metadata.AppendToOutgoingContext(context.Background(), ctxpkg.TokenHeader, token)
   316  
   317  	childStat, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
   318  	if err != nil {
   319  		return false, err
   320  	}
   321  	if childStat.GetStatus().GetCode() == rpc.Code_CODE_NOT_FOUND && ref.GetPath() != "" && ref.GetPath() != "." {
   322  		// The resource does not seem to exist (yet?). We might be part of an initiate upload request.
   323  		// Stat the parent to get its path and check that against the root path.
   324  		childStat, err = client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: ref.GetResourceId()}})
   325  		if err != nil {
   326  			return false, err
   327  		}
   328  	}
   329  	if childStat.GetStatus().GetCode() != rpc.Code_CODE_OK {
   330  		return false, statuspkg.NewErrorFromCode(childStat.Status.Code, "auth interceptor")
   331  	}
   332  	pathResp, err = client.GetPath(ctx, &provider.GetPathRequest{ResourceId: childStat.GetInfo().GetId()})
   333  	if err != nil {
   334  		return false, err
   335  	}
   336  	if pathResp.GetStatus().GetCode() != rpc.Code_CODE_OK {
   337  		return false, statuspkg.NewErrorFromCode(pathResp.Status.Code, "auth interceptor")
   338  	}
   339  	childPath = pathResp.Path
   340  
   341  	return strings.HasPrefix(childPath, parentPath), nil
   342  
   343  }
   344  
   345  func extractRefFromListProvidersReq(v *registry.ListStorageProvidersRequest) (*provider.Reference, bool) {
   346  	ref := &provider.Reference{}
   347  	if v.Opaque != nil && v.Opaque.Map != nil {
   348  		if e, ok := v.Opaque.Map["storage_id"]; ok {
   349  			if ref.ResourceId == nil {
   350  				ref.ResourceId = &provider.ResourceId{}
   351  			}
   352  			ref.ResourceId.StorageId = string(e.Value)
   353  		}
   354  		if e, ok := v.Opaque.Map["space_id"]; ok {
   355  			if ref.ResourceId == nil {
   356  				ref.ResourceId = &provider.ResourceId{}
   357  			}
   358  			ref.ResourceId.SpaceId = string(e.Value)
   359  		}
   360  		if e, ok := v.Opaque.Map["opaque_id"]; ok {
   361  			if ref.ResourceId == nil {
   362  				ref.ResourceId = &provider.ResourceId{}
   363  			}
   364  			ref.ResourceId.OpaqueId = string(e.Value)
   365  		}
   366  		if e, ok := v.Opaque.Map["path"]; ok {
   367  			ref.Path = string(e.Value)
   368  		}
   369  	}
   370  	return ref, true
   371  }
   372  
   373  func extractRefForReaderRole(req interface{}) (*provider.Reference, bool) {
   374  	switch v := req.(type) {
   375  	// Read requests
   376  	case *registry.GetStorageProvidersRequest:
   377  		return v.GetRef(), true
   378  	case *registry.ListStorageProvidersRequest:
   379  		return extractRefFromListProvidersReq(v)
   380  	case *provider.StatRequest:
   381  		return v.GetRef(), true
   382  	case *provider.ListContainerRequest:
   383  		return v.GetRef(), true
   384  	case *provider.InitiateFileDownloadRequest:
   385  		return v.GetRef(), true
   386  
   387  	// App provider requests
   388  	case *appregistry.GetAppProvidersRequest:
   389  		return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
   390  	case *appprovider.OpenInAppRequest:
   391  		return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
   392  	case *gateway.OpenInAppRequest:
   393  		return v.GetRef(), true
   394  
   395  	// Locking
   396  	case *provider.GetLockRequest:
   397  		return v.GetRef(), true
   398  	case *provider.SetLockRequest:
   399  		return v.GetRef(), true
   400  	case *provider.RefreshLockRequest:
   401  		return v.GetRef(), true
   402  	case *provider.UnlockRequest:
   403  		return v.GetRef(), true
   404  
   405  	// OCM shares
   406  	case *ocmv1beta1.ListReceivedOCMSharesRequest:
   407  		return &provider.Reference{Path: "."}, true // we will try to stat the shared node
   408  
   409  	}
   410  
   411  	return nil, false
   412  
   413  }
   414  
   415  func extractRefForUploaderRole(req interface{}) (*provider.Reference, bool) {
   416  	switch v := req.(type) {
   417  	// Write Requests
   418  	case *registry.GetStorageProvidersRequest:
   419  		return v.GetRef(), true
   420  	case *registry.ListStorageProvidersRequest:
   421  		return extractRefFromListProvidersReq(v)
   422  	case *provider.StatRequest:
   423  		return v.GetRef(), true
   424  	case *provider.CreateContainerRequest:
   425  		return v.GetRef(), true
   426  	case *provider.TouchFileRequest:
   427  		return v.GetRef(), true
   428  	case *provider.InitiateFileUploadRequest:
   429  		return v.GetRef(), true
   430  
   431  	// App provider requests
   432  	case *appregistry.GetAppProvidersRequest:
   433  		return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
   434  	case *appprovider.OpenInAppRequest:
   435  		return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
   436  	case *gateway.OpenInAppRequest:
   437  		return v.GetRef(), true
   438  
   439  	// Locking
   440  	case *provider.GetLockRequest:
   441  		return v.GetRef(), true
   442  	case *provider.SetLockRequest:
   443  		return v.GetRef(), true
   444  	case *provider.RefreshLockRequest:
   445  		return v.GetRef(), true
   446  	case *provider.UnlockRequest:
   447  		return v.GetRef(), true
   448  	}
   449  
   450  	return nil, false
   451  
   452  }
   453  
   454  func extractRefForEditorRole(req interface{}) (*provider.Reference, bool) {
   455  	switch v := req.(type) {
   456  	// Remaining edit Requests
   457  	case *provider.DeleteRequest:
   458  		return v.GetRef(), true
   459  	case *provider.MoveRequest:
   460  		return v.GetSource(), true
   461  	case *provider.SetArbitraryMetadataRequest:
   462  		return v.GetRef(), true
   463  	case *provider.UnsetArbitraryMetadataRequest:
   464  		return v.GetRef(), true
   465  	}
   466  
   467  	return nil, false
   468  
   469  }
   470  
   471  func extractRef(req interface{}, tokenScope map[string]*authpb.Scope) (*provider.Reference, bool) {
   472  	var readPerm, uploadPerm, editPerm bool
   473  	for _, v := range tokenScope {
   474  		if v.Role == authpb.Role_ROLE_OWNER || v.Role == authpb.Role_ROLE_EDITOR || v.Role == authpb.Role_ROLE_VIEWER {
   475  			readPerm = true
   476  		}
   477  		if v.Role == authpb.Role_ROLE_OWNER || v.Role == authpb.Role_ROLE_EDITOR || v.Role == authpb.Role_ROLE_UPLOADER {
   478  			uploadPerm = true
   479  		}
   480  		if v.Role == authpb.Role_ROLE_OWNER || v.Role == authpb.Role_ROLE_EDITOR {
   481  			editPerm = true
   482  		}
   483  	}
   484  
   485  	if readPerm {
   486  		ref, ok := extractRefForReaderRole(req)
   487  		if ok {
   488  			return ref, true
   489  		}
   490  	}
   491  	if uploadPerm {
   492  		ref, ok := extractRefForUploaderRole(req)
   493  		if ok {
   494  			return ref, true
   495  		}
   496  	}
   497  	if editPerm {
   498  		ref, ok := extractRefForEditorRole(req)
   499  		if ok {
   500  			return ref, true
   501  		}
   502  	}
   503  
   504  	return nil, false
   505  }
   506  
   507  func extractShareRef(req interface{}) (*collaboration.ShareReference, bool) {
   508  	switch v := req.(type) {
   509  	case *collaboration.GetReceivedShareRequest:
   510  		return v.GetRef(), true
   511  	case *collaboration.UpdateReceivedShareRequest:
   512  		return &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: v.GetShare().GetShare().GetId()}}, true
   513  	}
   514  	return nil, false
   515  }
   516  
   517  func getRefKey(ref *provider.Reference) string {
   518  	if ref.GetPath() != "" {
   519  		return ref.Path
   520  	}
   521  
   522  	if ref.GetResourceId() != nil {
   523  		return storagespace.FormatResourceID(ref.ResourceId)
   524  	}
   525  
   526  	// on malicious request both path and rid could be empty
   527  	// we still should not panic
   528  	return ""
   529  }