github.com/cs3org/reva/v2@v2.27.7/tests/integration/grpc/ocm_invitation_test.go (about) 1 // Copyright 2018-2023 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 grpc_test 20 21 import ( 22 "bytes" 23 "context" 24 "encoding/base64" 25 "encoding/json" 26 "fmt" 27 "net/http" 28 "os" 29 30 gatewaypb "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" 31 userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 32 invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" 33 ocmproviderpb "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" 34 rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" 35 typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" 36 "google.golang.org/grpc/metadata" 37 38 "github.com/cs3org/reva/v2/pkg/auth/scope" 39 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 40 "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" 41 "github.com/cs3org/reva/v2/pkg/token" 42 "github.com/cs3org/reva/v2/pkg/token/manager/jwt" 43 "github.com/cs3org/reva/v2/pkg/utils" 44 "github.com/cs3org/reva/v2/pkg/utils/list" 45 46 . "github.com/onsi/ginkgo/v2" 47 . "github.com/onsi/gomega" 48 ) 49 50 type generateInviteResponse struct { 51 Token string `json:"token"` 52 Description string `json:"descriptions"` 53 Expiration uint64 `json:"expiration"` 54 InviteLink string `json:"invite_link"` 55 } 56 57 func ctxWithAuthToken(tokenManager token.Manager, user *userpb.User) context.Context { 58 ctx := context.Background() 59 scope, err := scope.AddOwnerScope(nil) 60 Expect(err).ToNot(HaveOccurred()) 61 tkn, err := tokenManager.MintToken(ctx, user, scope) 62 Expect(err).ToNot(HaveOccurred()) 63 ctx = ctxpkg.ContextSetToken(ctx, tkn) 64 ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn) 65 ctx = ctxpkg.ContextSetUser(ctx, user) 66 return ctx 67 } 68 69 func ocmUserEqual(u1, u2 *userpb.User) bool { 70 return utils.UserEqual(u1.Id, u2.Id) && u1.DisplayName == u2.DisplayName && u1.Mail == u2.Mail 71 } 72 73 var _ = Describe("ocm invitation workflow", func() { 74 var ( 75 err error 76 revads = map[string]*Revad{} 77 78 variables = map[string]string{} 79 80 ctxEinstein context.Context 81 ctxMarie context.Context 82 cernboxgw gatewaypb.GatewayAPIClient 83 cesnetgw gatewaypb.GatewayAPIClient 84 cernbox = &ocmproviderpb.ProviderInfo{ 85 Name: "cernbox", 86 FullName: "CERNBox", 87 Description: "CERNBox provides cloud data storage to all CERN users.", 88 Organization: "CERN", 89 Domain: "cernbox.cern.ch", 90 Homepage: "https://cernbox.web.cern.ch", 91 Services: []*ocmproviderpb.Service{ 92 { 93 Endpoint: &ocmproviderpb.ServiceEndpoint{ 94 Type: &ocmproviderpb.ServiceType{ 95 Name: "OCM", 96 Description: "CERNBox Open Cloud Mesh API", 97 }, 98 Name: "CERNBox - OCM API", 99 Path: "http://127.0.0.1:19001/ocm/", 100 IsMonitored: true, 101 }, 102 Host: "127.0.0.1:19001", 103 ApiVersion: "0.0.1", 104 }, 105 }, 106 } 107 inviteTokenFile string 108 einstein = &userpb.User{ 109 Id: &userpb.UserId{ 110 OpaqueId: "4c510ada-c86b-4815-8820-42cdf82c3d51", 111 Idp: "https://cernbox.cern.ch", 112 Type: userpb.UserType_USER_TYPE_PRIMARY, 113 }, 114 Username: "einstein", 115 Mail: "einstein@cern.ch", 116 DisplayName: "Albert Einstein", 117 } 118 federatedEinstein = &userpb.User{ 119 Id: &userpb.UserId{ 120 Type: userpb.UserType_USER_TYPE_FEDERATED, 121 Idp: "cernbox.cern.ch", 122 OpaqueId: base64.URLEncoding.EncodeToString([]byte("4c510ada-c86b-4815-8820-42cdf82c3d51@https://cernbox.cern.ch")), 123 }, 124 Username: "einstein", 125 Mail: "einstein@cern.ch", 126 DisplayName: "Albert Einstein", 127 } 128 marie = &userpb.User{ 129 Id: &userpb.UserId{ 130 OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", 131 Idp: "https://cesnet.cz", 132 Type: userpb.UserType_USER_TYPE_PRIMARY, 133 }, 134 Username: "marie", 135 Mail: "marie@cesnet.cz", 136 DisplayName: "Marie Curie", 137 } 138 federatedMarie = &userpb.User{ 139 Id: &userpb.UserId{ 140 Type: userpb.UserType_USER_TYPE_FEDERATED, 141 Idp: "cesnet.cz", 142 OpaqueId: base64.URLEncoding.EncodeToString([]byte("f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c@https://cesnet.cz")), 143 }, 144 Username: "marie", 145 Mail: "marie@cesnet.cz", 146 DisplayName: "Marie Curie", 147 } 148 ) 149 150 for _, driver := range []string{"json"} { 151 152 JustBeforeEach(func() { 153 tokenManager, err := jwt.New(map[string]interface{}{"secret": "changemeplease"}) 154 Expect(err).ToNot(HaveOccurred()) 155 ctxEinstein = ctxWithAuthToken(tokenManager, einstein) 156 ctxMarie = ctxWithAuthToken(tokenManager, marie) 157 variables["ocm_driver"] = driver 158 revads, err = startRevads([]RevadConfig{ 159 { 160 Name: "cernboxgw", 161 Config: "ocm-server-cernbox-grpc.toml", 162 Files: map[string]string{ 163 "providers": "ocm-providers.demo.json", 164 }, 165 }, 166 { 167 Name: "cernboxhttp", 168 Config: "ocm-server-cernbox-http.toml", 169 }, 170 { 171 Name: "cesnetgw", 172 Config: "ocm-server-cesnet-grpc.toml", 173 Files: map[string]string{ 174 "providers": "ocm-providers.demo.json", 175 }, 176 }, 177 { 178 Name: "cesnethttp", 179 Config: "ocm-server-cesnet-http.toml", 180 }, 181 }, variables) 182 Expect(err).ToNot(HaveOccurred()) 183 cernboxgw, err = pool.GetGatewayServiceClient(revads["cernboxgw"].GrpcAddress) 184 Expect(err).ToNot(HaveOccurred()) 185 cesnetgw, err = pool.GetGatewayServiceClient(revads["cesnetgw"].GrpcAddress) 186 Expect(err).ToNot(HaveOccurred()) 187 cernbox.Services[0].Endpoint.Path = "http://" + revads["cernboxhttp"].GrpcAddress + "/ocm" 188 }) 189 190 AfterEach(func() { 191 for _, r := range revads { 192 Expect(r.Cleanup(CurrentGinkgoTestDescription().Failed)).To(Succeed()) 193 } 194 Expect(os.RemoveAll(inviteTokenFile)).To(Succeed()) 195 }) 196 197 Describe("einstein and marie do not know each other", func() { 198 var cleanup func() 199 BeforeEach(func() { 200 variables, cleanup, err = initData(driver, nil, nil) 201 Expect(err).ToNot(HaveOccurred()) 202 }) 203 204 AfterEach(func() { 205 cleanup() 206 }) 207 208 Context("einstein generates a token", func() { 209 It("will complete the workflow ", func() { 210 invitationTknRes, err := cernboxgw.GenerateInviteToken(ctxEinstein, &invitepb.GenerateInviteTokenRequest{}) 211 Expect(err).ToNot(HaveOccurred()) 212 Expect(invitationTknRes.Status.Code).To(Equal(rpc.Code_CODE_OK)) 213 Expect(invitationTknRes.InviteToken).ToNot(BeNil()) 214 forwardRes, err := cesnetgw.ForwardInvite(ctxMarie, &invitepb.ForwardInviteRequest{ 215 OriginSystemProvider: cernbox, 216 InviteToken: invitationTknRes.InviteToken, 217 }) 218 Expect(err).ToNot(HaveOccurred()) 219 Expect(forwardRes.Status.Code).To(Equal(rpc.Code_CODE_OK)) 220 221 Expect(forwardRes.DisplayName).To(Equal(einstein.DisplayName)) 222 Expect(forwardRes.Email).To(Equal(einstein.Mail)) 223 Expect(utils.UserEqual(forwardRes.UserId, federatedEinstein.Id)).To(BeTrue()) 224 225 usersRes1, err := cernboxgw.FindAcceptedUsers(ctxEinstein, &invitepb.FindAcceptedUsersRequest{}) 226 Expect(err).ToNot(HaveOccurred()) 227 Expect(usersRes1.Status.Code).To(Equal(rpc.Code_CODE_OK)) 228 Expect(usersRes1.AcceptedUsers).To(HaveLen(1)) 229 info1 := usersRes1.AcceptedUsers[0] 230 Expect(ocmUserEqual(info1, federatedMarie)).To(BeTrue()) 231 232 usersRes2, err := cesnetgw.FindAcceptedUsers(ctxMarie, &invitepb.FindAcceptedUsersRequest{}) 233 Expect(err).ToNot(HaveOccurred()) 234 Expect(usersRes2.Status.Code).To(Equal(rpc.Code_CODE_OK)) 235 Expect(usersRes2.AcceptedUsers).To(HaveLen(1)) 236 info2 := usersRes2.AcceptedUsers[0] 237 Expect(ocmUserEqual(info2, federatedEinstein)).To(BeTrue()) 238 }) 239 240 }) 241 }) 242 243 Describe("an invitation workflow has been already completed between einstein and marie", func() { 244 var cleanup func() 245 BeforeEach(func() { 246 variables, cleanup, err = initData(driver, nil, map[string][]*userpb.User{ 247 einstein.Id.OpaqueId: {federatedMarie}, 248 marie.Id.OpaqueId: {federatedEinstein}, 249 }) 250 Expect(err).ToNot(HaveOccurred()) 251 }) 252 253 AfterEach(func() { 254 cleanup() 255 }) 256 257 Context("marie accepts a new invite token generated by einstein", func() { 258 It("fails with already exists code", func() { 259 inviteTknRes, err := cernboxgw.GenerateInviteToken(ctxEinstein, &invitepb.GenerateInviteTokenRequest{}) 260 Expect(err).ToNot(HaveOccurred()) 261 Expect(inviteTknRes.Status.Code).To(Equal(rpc.Code_CODE_OK)) 262 263 forwardRes, err := cesnetgw.ForwardInvite(ctxMarie, &invitepb.ForwardInviteRequest{ 264 InviteToken: inviteTknRes.InviteToken, 265 OriginSystemProvider: cernbox, 266 }) 267 Expect(err).ToNot(HaveOccurred()) 268 Expect(forwardRes.Status.Code).To(Equal(rpc.Code_CODE_ALREADY_EXISTS)) 269 }) 270 }) 271 }) 272 273 Describe("marie accepts an expired token", func() { 274 expiredToken := &invitepb.InviteToken{ 275 Token: "token", 276 UserId: einstein.Id, 277 Expiration: &typesv1beta1.Timestamp{ 278 Seconds: 0, 279 }, 280 Description: "expired token", 281 } 282 283 var cleanup func() 284 BeforeEach(func() { 285 variables, cleanup, err = initData(driver, []*invitepb.InviteToken{expiredToken}, nil) 286 Expect(err).ToNot(HaveOccurred()) 287 }) 288 289 AfterEach(func() { 290 cleanup() 291 }) 292 293 It("will not complete the invitation workflow", func() { 294 forwardRes, err := cesnetgw.ForwardInvite(ctxMarie, &invitepb.ForwardInviteRequest{ 295 InviteToken: expiredToken, 296 OriginSystemProvider: cernbox, 297 }) 298 Expect(err).ToNot(HaveOccurred()) 299 Expect(forwardRes.Status.Code).To(Equal(rpc.Code_CODE_INVALID_ARGUMENT)) 300 }) 301 }) 302 303 Describe("marie accept a not existing token", func() { 304 var cleanup func() 305 BeforeEach(func() { 306 variables, cleanup, err = initData(driver, nil, nil) 307 Expect(err).ToNot(HaveOccurred()) 308 }) 309 310 AfterEach(func() { 311 cleanup() 312 }) 313 314 It("will not complete the invitation workflow", func() { 315 forwardRes, err := cesnetgw.ForwardInvite(ctxMarie, &invitepb.ForwardInviteRequest{ 316 InviteToken: &invitepb.InviteToken{ 317 Token: "not-existing-token", 318 }, 319 OriginSystemProvider: cernbox, 320 }) 321 Expect(err).ToNot(HaveOccurred()) 322 Expect(forwardRes.Status.Code).To(Equal(rpc.Code_CODE_NOT_FOUND)) 323 }) 324 }) 325 326 Context("clients use the http endpoints exposed by sciencemesh", func() { 327 var ( 328 cesnetURL string 329 cernboxURL string 330 tknMarie, tknEinstein string 331 token string 332 ) 333 334 var cleanup func() 335 BeforeEach(func() { 336 variables, cleanup, err = initData(driver, nil, nil) 337 Expect(err).ToNot(HaveOccurred()) 338 }) 339 340 AfterEach(func() { 341 cleanup() 342 }) 343 344 JustBeforeEach(func() { 345 cesnetURL = revads["cesnethttp"].GrpcAddress 346 cernboxURL = revads["cernboxhttp"].GrpcAddress 347 348 var ok bool 349 tknMarie, ok = ctxpkg.ContextGetToken(ctxMarie) 350 Expect(ok).To(BeTrue()) 351 tknEinstein, ok = ctxpkg.ContextGetToken(ctxEinstein) 352 Expect(ok).To(BeTrue()) 353 354 tknRes, err := cernboxgw.GenerateInviteToken(ctxEinstein, &invitepb.GenerateInviteTokenRequest{}) 355 Expect(err).ToNot(HaveOccurred()) 356 Expect(tknRes.Status.Code).To(Equal(rpc.Code_CODE_OK)) 357 token = tknRes.InviteToken.Token 358 }) 359 360 acceptInvite := func(revaToken, domain, provider, token string) int { 361 d, err := json.Marshal(map[string]string{ 362 "token": token, 363 "providerDomain": provider, 364 }) 365 Expect(err).ToNot(HaveOccurred()) 366 req, err := http.NewRequestWithContext(context.TODO(), http.MethodPost, fmt.Sprintf("http://%s/sciencemesh/accept-invite", domain), bytes.NewReader(d)) 367 Expect(err).ToNot(HaveOccurred()) 368 req.Header.Set("x-access-token", revaToken) 369 req.Header.Set("content-type", "application/json") 370 371 res, err := http.DefaultClient.Do(req) 372 Expect(err).ToNot(HaveOccurred()) 373 defer res.Body.Close() 374 375 return res.StatusCode 376 } 377 378 type remoteUser struct { 379 DisplayName string `json:"display_name"` 380 Idp string `json:"idp"` 381 UserID string `json:"user_id"` 382 Mail string `json:"mail"` 383 } 384 385 remoteToCs3User := func(u *remoteUser) *userpb.User { 386 return &userpb.User{ 387 Id: &userpb.UserId{ 388 Idp: u.Idp, 389 OpaqueId: u.UserID, 390 }, 391 DisplayName: u.DisplayName, 392 Mail: u.Mail, 393 } 394 } 395 396 findAccepted := func(revaToken, domain string) ([]*remoteUser, int) { 397 req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, fmt.Sprintf("http://%s/sciencemesh/find-accepted-users", domain), nil) 398 Expect(err).ToNot(HaveOccurred()) 399 req.Header.Set("x-access-token", revaToken) 400 401 res, err := http.DefaultClient.Do(req) 402 Expect(err).ToNot(HaveOccurred()) 403 defer res.Body.Close() 404 405 var users []*remoteUser 406 _ = json.NewDecoder(res.Body).Decode(&users) 407 return users, res.StatusCode 408 } 409 410 generateToken := func(revaToken, domain string) (*generateInviteResponse, int) { 411 req, err := http.NewRequestWithContext(context.TODO(), http.MethodPost, fmt.Sprintf("http://%s/sciencemesh/generate-invite", domain), nil) 412 Expect(err).ToNot(HaveOccurred()) 413 req.Header.Set("x-access-token", revaToken) 414 415 res, err := http.DefaultClient.Do(req) 416 Expect(err).ToNot(HaveOccurred()) 417 defer res.Body.Close() 418 419 var inviteRes generateInviteResponse 420 Expect(json.NewDecoder(res.Body).Decode(&inviteRes)).To(Succeed()) 421 return &inviteRes, res.StatusCode 422 } 423 424 Context("einstein and marie do not know each other", func() { 425 426 Context("marie is not logged-in", func() { 427 It("fails with permission denied", func() { 428 code := acceptInvite("", cesnetURL, "cernbox.cern.ch", token) 429 Expect(code).To(Equal(http.StatusUnauthorized)) 430 }) 431 }) 432 It("complete the invitation workflow", func() { 433 users, code := findAccepted(tknEinstein, cernboxURL) 434 Expect(code).To(Equal(http.StatusOK)) 435 Expect(ocmUsersEqual(list.Map(users, remoteToCs3User), []*userpb.User{})).To(BeTrue()) 436 437 code = acceptInvite(tknMarie, cesnetURL, "cernbox.cern.ch", token) 438 Expect(code).To(Equal(http.StatusOK)) 439 440 users, code = findAccepted(tknEinstein, cernboxURL) 441 Expect(code).To(Equal(http.StatusOK)) 442 Expect(ocmUsersEqual(list.Map(users, remoteToCs3User), []*userpb.User{federatedMarie})).To(BeTrue()) 443 }) 444 }) 445 446 Context("marie already accepted an invitation before", func() { 447 var cleanup func() 448 BeforeEach(func() { 449 variables, cleanup, err = initData(driver, nil, map[string][]*userpb.User{ 450 einstein.Id.OpaqueId: {federatedMarie}, 451 marie.Id.OpaqueId: {federatedEinstein}, 452 }) 453 Expect(err).ToNot(HaveOccurred()) 454 }) 455 456 AfterEach(func() { 457 cleanup() 458 }) 459 460 It("fails the invitation workflow", func() { 461 users, code := findAccepted(tknEinstein, cernboxURL) 462 Expect(code).To(Equal(http.StatusOK)) 463 Expect(ocmUsersEqual(list.Map(users, remoteToCs3User), []*userpb.User{federatedMarie})).To(BeTrue()) 464 465 code = acceptInvite(tknMarie, cesnetURL, "cernbox.cern.ch", token) 466 Expect(code).To(Equal(http.StatusConflict)) 467 468 users, code = findAccepted(tknEinstein, cernboxURL) 469 Expect(code).To(Equal(http.StatusOK)) 470 Expect(ocmUsersEqual(list.Map(users, remoteToCs3User), []*userpb.User{federatedMarie})).To(BeTrue()) 471 }) 472 }) 473 474 Context("marie uses an expired token", func() { 475 expiredToken := &invitepb.InviteToken{ 476 Token: "token", 477 UserId: einstein.Id, 478 Expiration: &typesv1beta1.Timestamp{ 479 Seconds: 0, 480 }, 481 Description: "expired token", 482 } 483 484 var cleanup func() 485 BeforeEach(func() { 486 variables, cleanup, err = initData(driver, []*invitepb.InviteToken{expiredToken}, nil) 487 Expect(err).ToNot(HaveOccurred()) 488 }) 489 490 AfterEach(func() { 491 cleanup() 492 }) 493 494 It("will not complete the invitation workflow", func() { 495 users, code := findAccepted(tknEinstein, cernboxURL) 496 Expect(code).To(Equal(http.StatusOK)) 497 Expect(ocmUsersEqual(list.Map(users, remoteToCs3User), []*userpb.User{})).To(BeTrue()) 498 499 code = acceptInvite(tknMarie, cesnetURL, "cernbox.cern.ch", expiredToken.Token) 500 Expect(code).To(Equal(http.StatusBadRequest)) 501 502 users, code = findAccepted(tknEinstein, cernboxURL) 503 Expect(code).To(Equal(http.StatusOK)) 504 Expect(ocmUsersEqual(list.Map(users, remoteToCs3User), []*userpb.User{})).To(BeTrue()) 505 }) 506 }) 507 508 Context("generate the token from http apis", func() { 509 var cleanup func() 510 BeforeEach(func() { 511 variables, cleanup, err = initData(driver, nil, nil) 512 Expect(err).ToNot(HaveOccurred()) 513 }) 514 515 AfterEach(func() { 516 cleanup() 517 }) 518 519 It("succeeds", func() { 520 users, code := findAccepted(tknEinstein, cernboxURL) 521 Expect(code).To(Equal(http.StatusOK)) 522 Expect(ocmUsersEqual(list.Map(users, remoteToCs3User), []*userpb.User{})).To(BeTrue()) 523 524 ocmToken, code := generateToken(tknEinstein, cernboxURL) 525 Expect(code).To(Equal(http.StatusOK)) 526 527 code = acceptInvite(tknMarie, cesnetURL, "cernbox.cern.ch", ocmToken.Token) 528 Expect(code).To(Equal(http.StatusOK)) 529 530 users, code = findAccepted(tknEinstein, cernboxURL) 531 Expect(code).To(Equal(http.StatusOK)) 532 Expect(ocmUsersEqual(list.Map(users, remoteToCs3User), []*userpb.User{federatedMarie})).To(BeTrue()) 533 }) 534 }) 535 536 }) 537 538 } 539 540 }) 541 542 func ocmUsersEqual(u1, u2 []*userpb.User) bool { 543 if len(u1) != len(u2) { 544 return false 545 } 546 for i := range u1 { 547 if !ocmUserEqual(u1[i], u2[i]) { 548 return false 549 } 550 } 551 return true 552 }