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  }