github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/teams/multiple_use_invite_test.go (about) 1 package teams 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 "time" 8 9 "github.com/davecgh/go-spew/spew" 10 "github.com/keybase/clockwork" 11 12 "github.com/keybase/client/go/kbtest" 13 "github.com/keybase/client/go/libkb" 14 "github.com/keybase/client/go/protocol/keybase1" 15 "github.com/stretchr/testify/require" 16 ) 17 18 func TestTeamInviteStubbing(t *testing.T) { 19 tc := SetupTest(t, "team", 1) 20 defer tc.Cleanup() 21 _, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 22 require.NoError(t, err) 23 24 tc2 := SetupTest(t, "team", 1) 25 defer tc2.Cleanup() 26 user2, err := kbtest.CreateAndSignupFakeUserPaper("team", tc2.G) 27 require.NoError(t, err) 28 29 teamname := createTeam(tc) 30 31 t.Logf("Created team %s", teamname) 32 33 _, err = Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 34 Name: teamname, 35 NeedAdmin: true, 36 }) 37 require.NoError(t, err) 38 39 maxUses := keybase1.TeamInviteMaxUses(10) 40 inviteLink, err := CreateInvitelink(tc.MetaContext(), teamname, keybase1.TeamRole_READER, maxUses, nil /* etime */) 41 require.NoError(t, err) 42 43 wasSeitan, err := ParseAndAcceptSeitanToken(tc2.MetaContext(), &teamsUI{}, inviteLink.Ikey.String()) 44 require.NoError(t, err) 45 require.True(t, wasSeitan) 46 47 teamObj, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 48 Name: teamname, 49 NeedAdmin: true, 50 }) 51 require.NoError(t, err) 52 53 var inviteID keybase1.TeamInviteID 54 for _, inviteMD := range teamObj.chain().ActiveInvites() { 55 inviteID = inviteMD.Invite.Id 56 break // get first invite id 57 } 58 59 changeReq := keybase1.TeamChangeReq{} 60 err = changeReq.AddUVWithRole(user2.GetUserVersion(), keybase1.TeamRole_READER, nil /* botSettings */) 61 require.NoError(t, err) 62 changeReq.UseInviteID(inviteID, user2.GetUserVersion().PercentForm()) 63 err = teamObj.ChangeMembershipWithOptions(context.TODO(), changeReq, ChangeMembershipOptions{}) 64 require.NoError(t, err) 65 66 // User 2 loads team 67 68 teamObj2, err := Load(context.TODO(), tc2.G, keybase1.LoadTeamArg{ 69 Name: teamname, 70 NeedAdmin: false, 71 }) 72 require.NoError(t, err) 73 require.Len(t, teamObj2.chain().ActiveInvites(), 0, "invites were stubbed") 74 75 // User 1 makes User 2 admin 76 77 err = SetRoleAdmin(context.TODO(), tc.G, teamname, user2.Username) 78 require.NoError(t, err) 79 80 // User 2 loads team again 81 82 teamObj, err = Load(context.TODO(), tc2.G, keybase1.LoadTeamArg{ 83 Name: teamname, 84 NeedAdmin: true, 85 }) 86 require.NoError(t, err) 87 88 inner := teamObj.chain().inner 89 require.Len(t, inner.ActiveInvites(), 1) 90 inviteMD, ok := inner.InviteMetadatas[inviteID] 91 invite := inviteMD.Invite 92 require.True(t, ok, "invite found loaded by user 2") 93 require.Len(t, inviteMD.UsedInvites, 1) 94 95 // See if User 2 can decrypt 96 pkey, err := SeitanDecodePKey(string(invite.Name)) 97 require.NoError(t, err) 98 99 keyAndLabel, err := pkey.DecryptKeyAndLabel(context.TODO(), teamObj) 100 require.NoError(t, err) 101 102 ilink := keyAndLabel.Invitelink() 103 require.Equal(t, inviteLink.Ikey, ilink.I) 104 } 105 106 func TestSeitanHandleExceededInvite(t *testing.T) { 107 // Test what happens if server sends us acceptance for an invite that's 108 // exceeded. Handler should notice that and not add the member. Even it it 109 // attempted to, there are additional belts and suspenders: 110 111 // 1) sigchain pre-check should fail, 112 // 2) server should not accept the link, 113 // 3) if none of the above checks worked: the team would have ended up 114 // broken (not loadable) for other admins. 115 116 tc := SetupTest(t, "team", 1) 117 defer tc.Cleanup() 118 119 tc.Tp.SkipSendingSystemChatMessages = true 120 121 clock := clockwork.NewFakeClockAt(time.Now()) 122 tc.G.SetClock(clock) 123 124 user2, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 125 require.NoError(t, err) 126 kbtest.Logout(tc) 127 128 admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 129 require.NoError(t, err) 130 131 teamName, teamID := createTeam2(tc) 132 t.Logf("Created team %s", teamName) 133 134 // Add team invite link with max_uses=1 135 maxUses := keybase1.TeamInviteMaxUses(1) 136 invLink, err := CreateInvitelink(tc.MetaContext(), teamName.String(), keybase1.TeamRole_READER, maxUses, nil /* etime */) 137 require.NoError(t, err) 138 139 // Accept the link as user2. 140 kbtest.LogoutAndLoginAs(tc, user2) 141 142 uv := user2.GetUserVersion() 143 unixNow := clock.Now().Unix() 144 accepted, err := generateAcceptanceSeitanInviteLink(invLink.Ikey, uv, unixNow) 145 require.NoError(t, err) 146 147 err = postSeitanInviteLink(tc.MetaContext(), accepted) 148 require.NoError(t, err) 149 150 // Login as admin, call HandleTeamSeitan with a message as it would have 151 // came from team_rekeyd. 152 kbtest.LogoutAndLoginAs(tc, admin) 153 msg := keybase1.TeamSeitanMsg{ 154 TeamID: teamID, 155 Seitans: []keybase1.TeamSeitanRequest{ 156 { 157 InviteID: keybase1.TeamInviteID(accepted.inviteID), 158 Uid: uv.Uid, 159 EldestSeqno: uv.EldestSeqno, 160 Akey: keybase1.SeitanAKey(accepted.encoded), 161 Role: keybase1.TeamRole_READER, 162 UnixCTime: unixNow, 163 }, 164 }, 165 } 166 167 API := libkb.NewAPIArgRecorder(tc.G.API) 168 tc.G.API = API 169 err = HandleTeamSeitan(context.TODO(), tc.G, msg) 170 require.NoError(t, err) 171 records := API.GetFilteredRecordsAndReset(func(rec *libkb.APIRecord) bool { 172 return rec.Arg.Endpoint == "team/reject_invite_acceptance" 173 }) 174 require.Len(t, records, 0, "no invite link acceptances were rejected") 175 176 // User2 leaves team. 177 kbtest.LogoutAndLoginAs(tc, user2) 178 err = LeaveByID(context.TODO(), tc.G, teamID, false /* permanent */) 179 require.NoError(t, err) 180 181 // Login back to admin, use same seitan gregor message 182 // to try to add the user back in. 183 kbtest.LogoutAndLoginAs(tc, admin) 184 185 // `HandleTeamSeitan` should not return an error but skip over bad 186 // `TeamSeitanRequest` and cancel it. 187 err = HandleTeamSeitan(context.TODO(), tc.G, msg) 188 require.NoError(t, err) 189 records = API.GetFilteredRecordsAndReset(func(rec *libkb.APIRecord) bool { 190 return rec.Arg.Endpoint == "team/reject_invite_acceptance" 191 }) 192 require.Len(t, records, 1, "one invite acceptance should be rejected") 193 record := records[0] 194 // since this invite acceptance had been completed already, rejecting it now 195 // fails (with a generic error) 196 assertRejectInviteArgs(t, record, accepted.inviteID, uv.Uid, uv.EldestSeqno, msg.Seitans[0].Akey, "acceptance not found") 197 198 teamObj, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 199 Name: teamName.String(), 200 NeedAdmin: true, 201 }) 202 require.NoError(t, err) 203 204 // The person shouldn't have been added 205 members, err := teamObj.Members() 206 require.NoError(t, err) 207 208 uvs := members.AllUserVersions() 209 require.Equal(t, []keybase1.UserVersion{admin.GetUserVersion()}, uvs) 210 } 211 212 func TestSeitanHandleSeitanRejectsWhenAppropriate(t *testing.T) { 213 // Test various cases where an acceptance is malformed and should be 214 // rejected. Rejections for over-used invites are tested in 215 // TestSeitanHandleExceededInvite. 216 217 tc := SetupTest(t, "team", 1) 218 defer tc.Cleanup() 219 220 tc.Tp.SkipSendingSystemChatMessages = true 221 222 user2, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 223 require.NoError(t, err) 224 kbtest.Logout(tc) 225 226 admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 227 require.NoError(t, err) 228 229 teamName, teamID := createTeam2(tc) 230 t.Logf("Created team %s", teamName) 231 232 // Add team invite link which expires in 10 minutes. 233 expTime := keybase1.ToUnixTime(tc.G.Clock().Now().Add(10 * time.Minute)) 234 invLink, err := CreateInvitelink(tc.MetaContext(), teamName.String(), keybase1.TeamRole_READER, keybase1.TeamMaxUsesInfinite, 235 &expTime) 236 require.NoError(t, err) 237 238 inviteID := inviteIDFromIkey(t, invLink.Ikey) 239 240 origAPI := tc.G.API 241 RecordAPI := libkb.NewAPIArgRecorder(origAPI) 242 243 // Accept the link as user2. 244 origMsg := acceptInvite(t, &tc, teamID, user2, invLink) 245 uv := user2.GetUserVersion() 246 247 // change the eldest seqno and ensure the invite is rejected 248 fakeMsg := origMsg.DeepCopy() 249 fakeMsg.Seitans[0].EldestSeqno = 5 250 records := adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, fakeMsg, RecordAPI) 251 require.Len(t, records, 1, "one invite acceptance should be rejected") 252 record := records[0] 253 // since this modified invite acceptance (has different eldestSeqno) was 254 // never sent to the server, rejection should fail 255 assertRejectInviteArgs(t, record, inviteID, uv.Uid, keybase1.Seqno(5), fakeMsg.Seitans[0].Akey, "acceptance not found") 256 257 // now change the akey to something that cannot be b64 decoded 258 fakeMsg = origMsg.DeepCopy() 259 fakeMsg.Seitans[0].Akey = keybase1.SeitanAKey("*") + fakeMsg.Seitans[0].Akey[1:] 260 records = adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, fakeMsg, RecordAPI) 261 require.Len(t, records, 1, "one invite acceptance should be rejected") 262 record = records[0] 263 assertRejectInviteArgs(t, record, inviteID, uv.Uid, uv.EldestSeqno, fakeMsg.Seitans[0].Akey, "bad fields: akey") 264 265 // now change the akey to something that can be decoded but is not the correct key 266 fakeMsg2 := origMsg.DeepCopy() 267 fakeMsg2.Seitans[0].Akey = keybase1.SeitanAKey("aaaaaa") + fakeMsg2.Seitans[0].Akey[6:] 268 records = adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, fakeMsg2, RecordAPI) 269 require.Len(t, records, 1, "one invite acceptance should be rejected") 270 record = records[0] 271 assertRejectInviteArgs(t, record, inviteID, uv.Uid, uv.EldestSeqno, fakeMsg2.Seitans[0].Akey, "invalid akey") 272 273 // when we try to handle the original invite, it should succeed without issues 274 records = adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, origMsg, RecordAPI) 275 require.Len(t, records, 0, "no invite link acceptances were rejected") 276 277 // User2 leaves team. 278 user2LeavesTeam := func() { 279 kbtest.LogoutAndLoginAs(tc, user2) 280 err = LeaveByID(context.TODO(), tc.G, teamID, false /* permanent */) 281 require.NoError(t, err) 282 } 283 user2LeavesTeam() 284 285 // User2 accepts again. 286 msg2 := acceptInvite(t, &tc, teamID, user2, invLink) 287 require.NoError(t, err) 288 records = adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, msg2, RecordAPI) 289 require.Len(t, records, 0, "no invite acceptance should be rejected") 290 user2LeavesTeam() 291 292 // Now, try to accept two invitations at once, ensure both fail 293 records = adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, keybase1.TeamSeitanMsg{ 294 TeamID: teamID, 295 Seitans: []keybase1.TeamSeitanRequest{fakeMsg.Seitans[0], fakeMsg2.Seitans[0]}, 296 }, RecordAPI) 297 require.Len(t, records, 2, "two invite acceptances should be rejected") 298 assertRejectInviteArgs(t, records[0], inviteID, uv.Uid, uv.EldestSeqno, fakeMsg.Seitans[0].Akey, "bad fields: akey") 299 assertRejectInviteArgs(t, records[1], inviteID, uv.Uid, uv.EldestSeqno, fakeMsg2.Seitans[0].Akey, "acceptance not found") 300 301 // Ensures different acceptances do not use the same AKey, whose only 302 // entropy is time with second granularity. 303 time.Sleep(1 * time.Second) 304 305 // Now, try to accept two invitations at once, ensure one fails and the other doesn't 306 msg3 := acceptInvite(t, &tc, teamID, user2, invLink) 307 records = adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, keybase1.TeamSeitanMsg{ 308 TeamID: teamID, 309 Seitans: []keybase1.TeamSeitanRequest{fakeMsg.Seitans[0], msg3.Seitans[0]}, 310 }, RecordAPI) 311 require.Len(t, records, 1, "only one acceptance should be rejected") 312 assertRejectInviteArgs(t, records[0], inviteID, uv.Uid, uv.EldestSeqno, fakeMsg.Seitans[0].Akey, "bad fields: akey") 313 user2LeavesTeam() 314 315 // Ensures different acceptances do not use the same AKey, whose only 316 // entropy is time with second granularity. 317 time.Sleep(1 * time.Second) 318 319 // Login back to admin, use same seitan gregor message to try to add the 320 // user back in. This time, we move the clock forward so the invite is 321 // expired. 322 msg4 := acceptInvite(t, &tc, teamID, user2, invLink) 323 kbtest.LogoutAndLoginAs(tc, admin) 324 clock := clockwork.NewFakeClockAt(time.Now()) 325 clock.Advance(24 * time.Hour) 326 tc.G.SetClock(clock) 327 328 // `HandleTeamSeitan` should not return an error but skip over bad 329 // `TeamSeitanRequest` and reject it. 330 records = adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, msg4, RecordAPI) 331 require.Len(t, records, 1, "one invite acceptance should be rejected") 332 record = records[0] 333 assertRejectInviteArgs(t, record, inviteID, uv.Uid, uv.EldestSeqno, msg4.Seitans[0].Akey, "") 334 335 ensureTeamOnlyHasAdminMember := func() { 336 teamObj, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 337 Name: teamName.String(), 338 NeedAdmin: true, 339 }) 340 require.NoError(t, err) 341 342 // The person shouldn't have been added 343 members, err := teamObj.Members() 344 require.NoError(t, err) 345 346 uvs := members.AllUserVersions() 347 require.Equal(t, []keybase1.UserVersion{admin.GetUserVersion()}, uvs) 348 } 349 ensureTeamOnlyHasAdminMember() 350 } 351 352 func TestSeitanHandleExpiredInvite(t *testing.T) { 353 // Test what happens if server sends us acceptance for an invite that's 354 // expired. Handler should notice that and not add the member. 355 356 tc := SetupTest(t, "team", 1) 357 defer tc.Cleanup() 358 359 tc.Tp.SkipSendingSystemChatMessages = true 360 361 clock := clockwork.NewFakeClockAt(time.Now()) 362 tc.G.SetClock(clock) 363 364 user2, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 365 require.NoError(t, err) 366 kbtest.Logout(tc) 367 368 user3, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 369 require.NoError(t, err) 370 kbtest.Logout(tc) 371 372 admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 373 require.NoError(t, err) 374 375 teamName, teamID := createTeam2(tc) 376 t.Logf("Created team %s", teamName) 377 378 // Add team invite link which expires in 10 minutes. 379 expTime := keybase1.ToUnixTime(clock.Now().Add(10 * time.Minute)) 380 invLink, err := CreateInvitelink(tc.MetaContext(), teamName.String(), keybase1.TeamRole_READER, keybase1.TeamMaxUsesInfinite, 381 &expTime) 382 require.NoError(t, err) 383 384 origAPI := tc.G.API 385 RecordAPI := libkb.NewAPIArgRecorder(origAPI) 386 387 msg2 := acceptInvite(t, &tc, teamID, user2, invLink) 388 msg3 := acceptInvite(t, &tc, teamID, user3, invLink) 389 390 // invite is accepted for user2 391 records := adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, msg2, RecordAPI) 392 require.Len(t, records, 0, "no invite link acceptances were rejected") 393 394 // We move the clock forward so the invite expires. 395 clock.Advance(24 * time.Hour) 396 397 // try to add user3. This should fail 398 records = adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, msg3, RecordAPI) 399 require.Len(t, records, 1, "one invite acceptance should be rejected") 400 record := records[0] 401 assertRejectInviteArgs(t, record, SCTeamInviteID(msg3.Seitans[0].InviteID), user3.GetUID(), user3.GetUserVersion().EldestSeqno, msg3.Seitans[0].Akey, "") 402 403 // ensure team has user2 and admin but not user3 404 teamObj, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 405 Name: teamName.String(), 406 NeedAdmin: true, 407 }) 408 require.NoError(t, err) 409 410 members, err := teamObj.Members() 411 require.NoError(t, err) 412 require.Len(t, members.AllUserVersions(), 2) 413 require.Equal(t, []keybase1.UserVersion{admin.GetUserVersion()}, members.Owners) 414 require.Equal(t, []keybase1.UserVersion{user2.GetUserVersion()}, members.Readers) 415 } 416 417 func TestSeitanHandleRequestAfterRoleChange(t *testing.T) { 418 tc := SetupTest(t, "team", 1) 419 defer tc.Cleanup() 420 421 tc.Tp.SkipSendingSystemChatMessages = true 422 423 user2, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 424 require.NoError(t, err) 425 kbtest.Logout(tc) 426 427 user3, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 428 require.NoError(t, err) 429 kbtest.Logout(tc) 430 431 admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 432 require.NoError(t, err) 433 434 teamName, teamID := createTeam2(tc) 435 t.Logf("Created team %s", teamName) 436 437 clock := clockwork.NewFakeClockAt(time.Now()) 438 tc.G.SetClock(clock) 439 440 // Add team invite link which expires in 10 minutes. 441 expTime := keybase1.ToUnixTime(clock.Now().Add(10 * time.Minute)) 442 invLink, err := CreateInvitelink(tc.MetaContext(), teamName.String(), keybase1.TeamRole_READER, keybase1.TeamMaxUsesInfinite, 443 &expTime) 444 require.NoError(t, err) 445 446 inviteID := inviteIDFromIkey(t, invLink.Ikey) 447 448 origAPI := tc.G.API 449 RecordAPI := libkb.NewAPIArgRecorder(origAPI) 450 451 msg2 := acceptInvite(t, &tc, teamID, user2, invLink) 452 msg3 := acceptInvite(t, &tc, teamID, user3, invLink) 453 454 clock.Advance(5 * time.Second) 455 456 // First admin adds and removes user2. Because this happens *after* invite 457 // link for user2 is accepted, it renders the acceptance obsolete. Seitan 458 // handler should check that there was a role change after acceptance ctime 459 // and reject that acceptance. 460 tc.G.API = origAPI 461 kbtest.LogoutAndLoginAs(tc, admin) 462 _, err = AddMember(context.TODO(), tc.G, teamName.String(), user2.Username, keybase1.TeamRole_WRITER, nil /* botSettings */) 463 require.NoError(t, err) 464 err = RemoveMember(context.TODO(), tc.G, teamName.String(), user2.Username) 465 require.NoError(t, err) 466 467 clock.Advance(5 * time.Second) 468 469 // Then, we try to accept all invites invites: only the one for user3 470 // should succeed (as user2's status changed after they joined). 471 require.NoError(t, err) 472 records := adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, keybase1.TeamSeitanMsg{ 473 TeamID: teamID, 474 Seitans: []keybase1.TeamSeitanRequest{msg2.Seitans[0], msg3.Seitans[0]}, 475 }, RecordAPI) 476 require.Len(t, records, 1, "one acceptance should be rejected") 477 assertRejectInviteArgs(t, records[0], inviteID, user2.GetUID(), user2.EldestSeqno, msg2.Seitans[0].Akey, "") 478 479 // Ensure team has only user3 and admin. 480 teamObj, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 481 Name: teamName.String(), 482 NeedAdmin: true, 483 }) 484 require.NoError(t, err) 485 486 members, err := teamObj.Members() 487 require.NoError(t, err) 488 require.Len(t, members.AllUserVersions(), 2) 489 require.Equal(t, []keybase1.UserVersion{admin.GetUserVersion()}, members.Owners) 490 require.Equal(t, []keybase1.UserVersion{user3.GetUserVersion()}, members.Readers) 491 } 492 493 func TestSeitanHandleFutureInvite(t *testing.T) { 494 tc := SetupTest(t, "team", 1) 495 defer tc.Cleanup() 496 497 tc.Tp.SkipSendingSystemChatMessages = true 498 499 user2, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 500 require.NoError(t, err) 501 kbtest.Logout(tc) 502 503 user3, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 504 require.NoError(t, err) 505 kbtest.Logout(tc) 506 507 admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 508 require.NoError(t, err) 509 510 teamName, teamID := createTeam2(tc) 511 t.Logf("Created team %s", teamName) 512 513 // Add team invite link which expires in 10 minutes. 514 expTime := keybase1.ToUnixTime(time.Now().Add(10 * time.Minute)) 515 invLink, err := CreateInvitelink(tc.MetaContext(), teamName.String(), keybase1.TeamRole_READER, keybase1.TeamMaxUsesInfinite, 516 &expTime) 517 require.NoError(t, err) 518 519 origAPI := tc.G.API 520 RecordAPI := libkb.NewAPIArgRecorder(origAPI) 521 522 // user 2 accepts an invite with a future timestamp 523 origClock := tc.G.GetClock() 524 clock := clockwork.NewFakeClockAt(time.Now().Add(2 * time.Hour)) 525 tc.G.SetClock(clock) 526 msg2 := acceptInvite(t, &tc, teamID, user2, invLink) 527 tc.G.SetClock(origClock) 528 529 msg3 := acceptInvite(t, &tc, teamID, user3, invLink) 530 531 // then, we try to accept all invites invites: only the one for user3 should 532 // succeed (as user2 accepted with a future timestamp). However we won't 533 // reject the invite for user2 (and instead confirm later they were not 534 // added to the team) to prevent an admin with a messed up clock from 535 // rejecting good invites. 536 require.NoError(t, err) 537 records := adminCallsHandleTeamSeitanAndReturnsRejectCalls(t, &tc, admin, keybase1.TeamSeitanMsg{ 538 TeamID: teamID, 539 Seitans: []keybase1.TeamSeitanRequest{msg2.Seitans[0], msg3.Seitans[0]}, 540 }, RecordAPI) 541 require.Len(t, records, 0, "no acceptance should be rejected") 542 543 // ensure team has only user3 and admin 544 teamObj, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 545 Name: teamName.String(), 546 NeedAdmin: true, 547 }) 548 require.NoError(t, err) 549 550 members, err := teamObj.Members() 551 require.NoError(t, err) 552 require.Len(t, members.AllUserVersions(), 2) 553 require.Equal(t, []keybase1.UserVersion{admin.GetUserVersion()}, members.Owners) 554 require.Equal(t, []keybase1.UserVersion{user3.GetUserVersion()}, members.Readers) 555 } 556 557 func acceptInvite(t *testing.T, tc *libkb.TestContext, teamID keybase1.TeamID, user *kbtest.FakeUser, inviteLink keybase1.Invitelink) keybase1.TeamSeitanMsg { 558 kbtest.LogoutAndLoginAs(*tc, user) 559 560 uv := user.GetUserVersion() 561 unixNow := tc.G.Clock().Now().Unix() 562 t.Logf("Unix is %v", unixNow) 563 accepted, err := generateAcceptanceSeitanInviteLink(inviteLink.Ikey, uv, unixNow) 564 require.NoError(t, err) 565 566 err = postSeitanInviteLink(tc.MetaContext(), accepted) 567 require.NoError(t, err) 568 569 // This simulates a seitan msg for the admin as it would have came from 570 // team_rekeyd. 571 return keybase1.TeamSeitanMsg{ 572 TeamID: teamID, 573 Seitans: []keybase1.TeamSeitanRequest{ 574 { 575 InviteID: keybase1.TeamInviteID(accepted.inviteID), 576 Uid: uv.Uid, 577 EldestSeqno: uv.EldestSeqno, 578 Akey: keybase1.SeitanAKey(accepted.encoded), 579 Role: keybase1.TeamRole_READER, 580 UnixCTime: unixNow, 581 }, 582 }, 583 } 584 } 585 586 func inviteIDFromIkey(t *testing.T, ikey keybase1.SeitanIKeyInvitelink) SCTeamInviteID { 587 sikey, err := GenerateSIKeyInvitelink(ikey) 588 require.NoError(t, err) 589 inviteID, err := sikey.GenerateTeamInviteID() 590 require.NoError(t, err) 591 return inviteID 592 } 593 594 func adminCallsHandleTeamSeitanAndReturnsRejectCalls(t *testing.T, tc *libkb.TestContext, admin *kbtest.FakeUser, msg keybase1.TeamSeitanMsg, api *libkb.APIArgRecorder) []libkb.APIRecord { 595 kbtest.LogoutAndLoginAs(*tc, admin) 596 tc.G.API = api 597 err := HandleTeamSeitan(context.TODO(), tc.G, msg) 598 require.NoError(t, err) 599 return api.GetFilteredRecordsAndReset(func(rec *libkb.APIRecord) bool { 600 return rec.Arg.Endpoint == "team/reject_invite_acceptance" 601 }) 602 } 603 604 func assertRejectInviteArgs(t *testing.T, record libkb.APIRecord, inviteID SCTeamInviteID, uid keybase1.UID, seqno keybase1.Seqno, akey keybase1.SeitanAKey, errString string) { 605 require.Equal(t, string(inviteID), record.Arg.Args["invite_id"].String()) 606 require.Equal(t, string(uid), record.Arg.Args["uid"].String()) 607 require.Equal(t, fmt.Sprintf("%v", seqno), record.Arg.Args["eldest_seqno"].String()) 608 require.Equal(t, string(akey), record.Arg.Args["akey"].String()) 609 if errString == "" { 610 require.NoError(t, record.Err) 611 } else { 612 require.NotNil(t, record.Err) 613 require.Contains(t, record.Err.Error(), errString) 614 } 615 } 616 617 func TestSeitanInviteLinkPukless(t *testing.T) { 618 // Test server sending us team invite link request with a valid acceptance 619 // key, but the user is PUK-less so they can't be added using 620 // 'team.change_membership' link. 621 622 tc := SetupTest(t, "team", 1) 623 defer tc.Cleanup() 624 625 tc.Tp.SkipSendingSystemChatMessages = true 626 627 admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 628 require.NoError(t, err) 629 t.Logf("Admin username: %s", admin.Username) 630 631 teamName, teamID := createTeam2(tc) 632 t.Logf("Created team %q", teamName.String()) 633 634 maxUses := keybase1.TeamInviteMaxUses(1) 635 invLink, err := CreateInvitelink(tc.MetaContext(), teamName.String(), keybase1.TeamRole_READER, 636 maxUses, nil /* etime */) 637 require.NoError(t, err) 638 639 t.Logf("Created invite link %q", invLink.Ikey) 640 641 kbtest.Logout(tc) 642 643 // Create a PUKless user 644 tc.Tp.DisableUpgradePerUserKey = true 645 user, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 646 require.NoError(t, err) 647 648 t.Logf("User: %s", user.Username) 649 650 timeNow := tc.G.Clock().Now().Unix() 651 seitanRet, err := generateAcceptanceSeitanInviteLink(invLink.Ikey, user.GetUserVersion(), timeNow) 652 require.NoError(t, err) 653 654 kbtest.LogoutAndLoginAs(tc, admin) 655 656 inviteID, err := seitanRet.inviteID.TeamInviteID() 657 require.NoError(t, err) 658 659 msg := keybase1.TeamSeitanMsg{ 660 TeamID: teamID, 661 Seitans: []keybase1.TeamSeitanRequest{{ 662 InviteID: inviteID, 663 Uid: user.GetUID(), 664 EldestSeqno: user.EldestSeqno, 665 Akey: keybase1.SeitanAKey(seitanRet.encoded), 666 Role: keybase1.TeamRole_WRITER, 667 UnixCTime: timeNow, 668 }}, 669 } 670 err = HandleTeamSeitan(context.Background(), tc.G, msg) 671 require.NoError(t, err) 672 673 // HandleTeamSeitan should not have added an invite for user. If it has, it 674 // also hasn't "used invite" properly (`team.invite` link does not have 675 // `use_invites` field even if it adds type=keybase invites). 676 team, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 677 Name: teamName.String(), 678 NeedAdmin: true, 679 ForceRepoll: true, 680 }) 681 require.NoError(t, err) 682 683 invite, _, found := team.FindActiveKeybaseInvite(user.GetUID()) 684 require.False(t, found, "Expected not to find invite for user: %s", spew.Sdump(invite)) 685 686 uvs := team.AllUserVersionsByUID(context.Background(), user.GetUID()) 687 require.Len(t, uvs, 0, "Expected user not to end up in a team as cryptomember (?)") 688 }