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 }