github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  }