1package systests
2
3import (
4	"fmt"
5	"sort"
6	"strings"
7	"testing"
8	"time"
9
10	"golang.org/x/net/context"
11
12	"github.com/davecgh/go-spew/spew"
13	"github.com/keybase/client/go/emails"
14	"github.com/keybase/client/go/kbtest"
15	"github.com/keybase/client/go/libkb"
16	"github.com/keybase/client/go/protocol/keybase1"
17	"github.com/keybase/client/go/teams"
18	"github.com/stretchr/testify/require"
19)
20
21func TestImplicitTeamRotateOnRevokePrivate(t *testing.T) {
22	testImplicitTeamRotateOnRevoke(t, false)
23}
24
25func TestImplicitTeamRotateOnRevokePublic(t *testing.T) {
26	testImplicitTeamRotateOnRevoke(t, true)
27}
28
29func testImplicitTeamRotateOnRevoke(t *testing.T, public bool) {
30	t.Logf("public: %v", public)
31	tt := newTeamTester(t)
32	defer tt.cleanup()
33
34	alice := tt.addUser("alice")
35	bob := tt.addUserWithPaper("bob")
36
37	iTeamName := strings.Join([]string{alice.username, bob.username}, ",")
38
39	t.Logf("make an implicit team")
40	team, err := alice.lookupImplicitTeam(true /*create*/, iTeamName, public)
41	require.NoError(t, err)
42
43	// get the before state of the team
44	before, err := GetTeamForTestByID(context.TODO(), alice.tc.G, team, public)
45	require.NoError(t, err)
46	require.Equal(t, keybase1.PerTeamKeyGeneration(1), before.Generation())
47	secretBefore := before.Data.PerTeamKeySeedsUnverified[before.Generation()].Seed.ToBytes()
48
49	bob.revokePaperKey()
50
51	// We wait for different chain arrangements based on whether this was a public or private rotation
52	var visible, hidden keybase1.Seqno
53	if public {
54		visible = keybase1.Seqno(2)
55		hidden = keybase1.Seqno(0)
56	} else {
57		visible = keybase1.Seqno(1)
58		hidden = keybase1.Seqno(1)
59	}
60
61	alice.waitForAnyRotateByID(team, visible, hidden)
62
63	// check that key was rotated for team
64	after, err := GetTeamForTestByID(context.TODO(), alice.tc.G, team, public)
65	require.NoError(t, err)
66	require.Equal(t, keybase1.PerTeamKeyGeneration(2), after.Generation(), "generation after rotate")
67
68	secretAfter := after.Data.PerTeamKeySeedsUnverified[after.Generation()].Seed.ToBytes()
69	if libkb.SecureByteArrayEq(secretAfter, secretBefore) {
70		t.Fatal("team secret did not change when rotated")
71	}
72}
73
74// Invites should be visible to everyone for implicit teams.
75// Even readers.
76func TestImplicitTeamInviteVisibilityPrivate(t *testing.T) {
77	testImplicitTeamInviteVisibility(t, false)
78}
79
80func TestImplicitTeamInviteVisibilityPublic(t *testing.T) {
81	testImplicitTeamInviteVisibility(t, true)
82}
83
84func testImplicitTeamInviteVisibility(t *testing.T, public bool) {
85	t.Logf("public: %v", public)
86
87	tt := newTeamTester(t)
88	defer tt.cleanup()
89
90	// Alice is a writer
91	alice := tt.addUser("alice")
92	// Bob is a writer by social assertion (proved partway through test)
93	bob := tt.addUser("bob")
94	// Char is a pukless writer
95	char := tt.addPuklessUser("char")
96	// test-private: Drake is a reader
97	// test-public: Drake is not a member
98	drake := tt.addUser("drake")
99
100	bobSocial := fmt.Sprintf("%v@rooter", bob.username)
101
102	impteamName := fmt.Sprintf("%v,%v,%v#%v", alice.username, bobSocial, char.username, drake.username)
103	if public {
104		impteamName = fmt.Sprintf("%v,%v,%v", alice.username, bobSocial, char.username)
105	}
106
107	t.Logf("impteamName: %v", impteamName)
108	teamID, err := alice.lookupImplicitTeam(true /*create*/, impteamName, public)
109	require.NoError(t, err)
110	_ = teamID
111
112	assertions := func(rooterDone bool) {
113		lookupRes, err := drake.lookupImplicitTeam2(false /*create*/, impteamName, public)
114		require.NoError(t, err)
115		require.Equal(t, teamID, lookupRes.TeamID)
116		require.Equal(t, public, lookupRes.DisplayName.IsPublic)
117
118		team, err := teams.Load(context.TODO(), drake.tc.G, keybase1.LoadTeamArg{
119			ID:          lookupRes.TeamID,
120			Public:      public,
121			ForceRepoll: true,
122		})
123		require.NoError(t, err)
124		require.True(t, team.IsImplicit())
125
126		// Assert that `list` and `users` are the same set.
127		// Ignores EldestSeqno, just uses UID.
128		// Sorts list in place
129		assertUvSet := func(actual []keybase1.UserVersion, expected ...*userPlusDevice) {
130			require.Len(t, actual, len(expected))
131			// Sort both by uid and compare
132			sort.Slice(actual, func(i, j int) bool {
133				return actual[i].Uid < actual[j].Uid
134			})
135			sort.Slice(expected, func(i, j int) bool {
136				return expected[i].uid < expected[j].uid
137			})
138			for i, expected1 := range expected {
139				actual1 := actual[i]
140				require.Equal(t, expected1.uid, actual1.Uid, "%v", expected1.username)
141			}
142		}
143
144		t.Logf("check the Team object")
145		members, err := team.Members()
146		require.NoError(t, err)
147		t.Logf("members: %v", spew.Sdump(members))
148		if !rooterDone {
149			assertUvSet(members.Owners, alice)
150			require.Equal(t, 2, team.NumActiveInvites(), "bob (social) and char (pukless)")
151		} else {
152			assertUvSet(members.Owners, alice, bob)
153			require.Equal(t, 1, team.NumActiveInvites(), "char (pukless)")
154		}
155		assertUvSet(members.Admins)
156		assertUvSet(members.Writers)
157		if public {
158			assertUvSet(members.Readers)
159		} else {
160			assertUvSet(members.Readers, drake)
161		}
162
163		t.Logf("check the ImplicitTeamDisplayName from LookupImplicitTeam: %v", spew.Sdump(lookupRes.DisplayName))
164		if !rooterDone {
165			require.Len(t, lookupRes.DisplayName.Writers.KeybaseUsers, 2, "alice, char (pukless)")
166			require.Len(t, lookupRes.DisplayName.Writers.UnresolvedUsers, 1, "bob (rooter)")
167		} else {
168			require.Len(t, lookupRes.DisplayName.Writers.KeybaseUsers, 3, "alice, bob (resolved), char (pukless)")
169			require.Len(t, lookupRes.DisplayName.Writers.UnresolvedUsers, 0)
170		}
171		require.Len(t, lookupRes.DisplayName.Readers.UnresolvedUsers, 0)
172		if public {
173			require.Len(t, lookupRes.DisplayName.Readers.KeybaseUsers, 0)
174		} else {
175			require.Len(t, lookupRes.DisplayName.Readers.KeybaseUsers, 1)
176		}
177	}
178
179	assertions(false)
180
181	bob.proveRooter()
182
183	t.Logf("wait for someone to add bob")
184	pollForConditionWithTimeout(t, 10*time.Second, "bob to be added to the team after rooter proof", func(ctx context.Context) bool {
185		team, err := teams.Load(ctx, drake.tc.G, keybase1.LoadTeamArg{
186			ID:          teamID,
187			Public:      public,
188			ForceRepoll: true,
189		})
190		require.NoError(t, err)
191		role, err := team.MemberRole(ctx, bob.userVersion())
192		require.NoError(t, err)
193		return role != keybase1.TeamRole_NONE
194	})
195
196	assertions(true)
197}
198
199// Poll until the condition is satisfied.
200// Fails the test and returns after the timeout.
201func pollForConditionWithTimeout(t *testing.T, timeout time.Duration, description string, condition func(context.Context) bool) {
202	pollCtx, pollCancel := context.WithCancel(context.Background())
203	defer pollCancel()
204	successCh := make(chan struct{})
205
206	// Start polling
207	go func(ctx context.Context) {
208		for {
209			if condition(ctx) {
210				successCh <- struct{}{}
211				return
212			}
213			time.Sleep(300 * time.Millisecond)
214		}
215	}(pollCtx)
216
217	// Wait for success or timeout
218	select {
219	case <-successCh:
220	case <-time.After(30 * time.Second):
221		pollCancel()
222		t.Fatalf("timed out waiting for condition: %v", description)
223	}
224}
225
226func trySBSConsolidation(t *testing.T, impteamExpr string, public bool) {
227	t.Logf("trySBSConsolidation(expr=%q, public=%t)", impteamExpr, public)
228
229	tt := newTeamTester(t)
230	defer tt.cleanup()
231
232	ann := tt.addUser("ann")
233	bob := tt.addUser("bob")
234	tt.logUserNames()
235
236	impteamName := fmt.Sprintf(impteamExpr, ann.username, bob.username, bob.username)
237	teamID, err := ann.lookupImplicitTeam(true /* create */, impteamName, public)
238	require.NoError(t, err)
239
240	t.Logf("Created team %s -> %s", impteamName, teamID)
241
242	bob.kickTeamRekeyd()
243	bob.proveRooter()
244	t.Logf("Bob (%s) proved rooter", bob.username)
245
246	expectedTeamName := fmt.Sprintf("%v,%v", ann.username, bob.username)
247	pollForConditionWithTimeout(t, 10*time.Second, "team consolidated to ann,bob", func(ctx context.Context) bool {
248		team, err := teams.Load(ctx, ann.tc.G, keybase1.LoadTeamArg{
249			ID:          teamID,
250			ForceRepoll: true,
251			Public:      public,
252		})
253		require.NoError(t, err)
254		displayName, err := team.ImplicitTeamDisplayName(context.Background())
255		require.NoError(t, err)
256		t.Logf("Got team back: %q (waiting for %q)", displayName.String(), expectedTeamName)
257		return displayName.String() == expectedTeamName
258	})
259
260	teamID2, err := ann.lookupImplicitTeam(false /* create */, expectedTeamName, public)
261	require.NoError(t, err)
262	require.Equal(t, teamID, teamID2)
263
264	_, err = teams.ResolveIDToName(context.Background(), ann.tc.G, teamID2)
265	require.NoError(t, err)
266
267	if public {
268		pam := tt.addUser("pam")
269		t.Logf("Signed up %s (%s) for public team check", pam.username, pam.uid)
270		teamID3, err := pam.lookupImplicitTeam(false /* create */, impteamName, true /* public */)
271		require.NoError(t, err)
272		require.Equal(t, teamID2, teamID3)
273
274		_, err = teams.Load(context.Background(), pam.tc.G, keybase1.LoadTeamArg{
275			ID:          teamID3,
276			ForceRepoll: true,
277			Public:      true,
278		})
279		require.NoError(t, err)
280
281		_, err = teams.ResolveIDToName(context.Background(), pam.tc.G, teamID3)
282		require.NoError(t, err)
283	}
284}
285
286func trySBSConsolidationPubAndPriv(t *testing.T, impteamExpr string) {
287	trySBSConsolidation(t, impteamExpr, true /* public */)
288	trySBSConsolidation(t, impteamExpr, false /* public */)
289}
290
291func TestImplicitSBSConsolidation(t *testing.T) {
292	trySBSConsolidationPubAndPriv(t, "%v,%v,%v@rooter")
293}
294
295func TestImplicitSBSPromotion(t *testing.T) {
296	trySBSConsolidationPubAndPriv(t, "%v,%v@rooter#%v")
297}
298
299func TestImplicitSBSConsolidation2(t *testing.T) {
300	// Test "downgrade" case, where it should not downgrade if social
301	// assertion is a reader. Result should still be "ann,bob", not
302	// "ann#bob".
303
304	trySBSConsolidationPubAndPriv(t, "%v,%v#%v@rooter")
305}
306
307func TestImplicitSBSPukless(t *testing.T) {
308	tt := newTeamTester(t)
309	defer tt.cleanup()
310
311	ann := tt.addUser("ann")
312	bob := tt.addPuklessUser("bob")
313	t.Logf("Signed ann (%s) and pukless bob (%s)", ann.username, bob.username)
314
315	impteamName := fmt.Sprintf("%s,%s@rooter", ann.username, bob.username)
316	teamID, err := ann.lookupImplicitTeam(true /* create */, impteamName, false)
317	require.NoError(t, err)
318
319	t.Logf("Created team %s -> %s", impteamName, teamID)
320
321	bob.proveRooter()
322
323	// Because of a bug in team provisional status checking combined
324	// with how lookupImplicitTeam works, this load is busted right
325	// now:
326
327	// Loading "alice,bob@rooter" resolves "alice" to "alice", and
328	// "bob@rooter" to "bob", and it "redirects" the load to
329	// "alice,bob". But "alice,bob" cannot be loaded because
330	// "alice,bob" implicit team will not exist until alice completes
331	// "bob@rooter" invite. Team server blocks team load until that to
332	// prevent races.
333
334	t.Logf(":: Trying to load %q", impteamName)
335	_, err = ann.lookupImplicitTeam(false /* create */, impteamName, false)
336	require.Error(t, err)
337	//require.Equal(t, teamID, teamID2)
338	t.Logf("Loading %s failed with: %v", impteamName, err)
339
340	// The following load call will not work as well. So this team is
341	// essentially locked until bob gets PUK and alice keys him in.
342
343	expectedTeamName := fmt.Sprintf("%v,%v", ann.username, bob.username)
344	t.Logf(":: Trying to load %q", expectedTeamName)
345	_, err = ann.lookupImplicitTeam(false /* create */, expectedTeamName, false)
346	require.Error(t, err)
347	t.Logf("Loading %s failed with: %v", expectedTeamName, err)
348
349	bob.kickTeamRekeyd()
350	bob.perUserKeyUpgrade()
351
352	pollForConditionWithTimeout(t, 10*time.Second, "team resolved to ann,bob", func(ctx context.Context) bool {
353		team, err := teams.Load(ctx, ann.tc.G, keybase1.LoadTeamArg{
354			ID:          teamID,
355			ForceRepoll: true,
356		})
357		require.NoError(t, err)
358		displayName, err := team.ImplicitTeamDisplayName(context.Background())
359		require.NoError(t, err)
360		t.Logf("Got team back: %s", displayName.String())
361		return displayName.String() == expectedTeamName
362	})
363
364	teamID3, err := ann.lookupImplicitTeam(false /* create */, expectedTeamName, false)
365	require.NoError(t, err)
366	require.Equal(t, teamID, teamID3)
367}
368
369func TestResolveSBSTeamWithConflict(t *testing.T) {
370	tt := newTeamTester(t)
371	defer tt.cleanup()
372
373	ann := tt.addUser("ann")
374	bob := tt.addUser("bob")
375
376	// Create two implicit teams that will become conflicted later:
377	// - alice,bob
378	// - alice,bob@rooter
379	impteamName1 := fmt.Sprintf("%s,%s", ann.username, bob.username)
380	_, err := ann.lookupImplicitTeam(true /* create */, impteamName1, false /* public */)
381	require.NoError(t, err)
382
383	impteamName2 := fmt.Sprintf("%s,%s@rooter", ann.username, bob.username)
384	_, err = ann.lookupImplicitTeam(true /* create */, impteamName2, false /* public */)
385	require.NoError(t, err)
386
387	// Make sure we can resolve them right now and get two different team IDs.
388	teamid1, err := ann.lookupImplicitTeam(false /* create */, impteamName1, false /* public */)
389	require.NoError(t, err)
390
391	teamid2, err := ann.lookupImplicitTeam(false /* create */, impteamName2, false /* public */)
392	require.NoError(t, err)
393
394	require.NotEqual(t, teamid1, teamid2)
395
396	// Make sure we can load these teams.
397	teamObj1 := ann.loadTeamByID(teamid1, true /* admin */)
398	teamObj2 := ann.loadTeamByID(teamid2, true /* admin */)
399
400	// Check display names with conflicts, teams are not in conflict right now
401	// (ImplicitTeamDisplayNameString returns display name with suffix).
402	name, err := teamObj1.ImplicitTeamDisplayNameString(context.Background())
403	require.NoError(t, err)
404	require.Equal(t, impteamName1, name)
405	t.Logf("Team 1 display name is: %s", name)
406	name, err = teamObj2.ImplicitTeamDisplayNameString(context.Background())
407	require.NoError(t, err)
408	require.Equal(t, impteamName2, name)
409	t.Logf("Team 2 (w/ rooter) display name is: %s", name)
410
411	// Bob proves rooter.
412	bob.kickTeamRekeyd()
413	bob.proveRooter()
414
415	// Wait till team2 resolves.
416	ann.pollForTeamSeqnoLinkWithLoadArgs(keybase1.LoadTeamArg{
417		ID:          teamid2,
418		ForceRepoll: true,
419	}, keybase1.Seqno(2))
420
421	// Make sure teams are still loadable by ID.
422	teamObj1 = ann.loadTeamByID(teamid1, true /* admin */)
423	teamObj2 = ann.loadTeamByID(teamid2, true /* admin */)
424
425	// Team1 display name with suffix should stay unchanged.
426	name, err = teamObj1.ImplicitTeamDisplayNameString(context.Background())
427	require.NoError(t, err)
428	t.Logf("After resolution, team1 display name is: %s", name)
429	require.Equal(t, impteamName1, name)
430
431	// See if we can resolve implicit team by name without suffix and get the
432	// first team.
433	lookupTeamID, err := ann.lookupImplicitTeam(false /* create */, name, false /* public */)
434	require.NoError(t, err)
435	require.Equal(t, teamid1, lookupTeamID)
436
437	// Team 2 should be the one that gets conflict suffix.
438	name, err = teamObj2.ImplicitTeamDisplayNameString(context.Background())
439	require.NoError(t, err)
440	t.Logf("After resolution, team2 display name is: %s", name)
441	require.Contains(t, name, "(conflicted copy")
442	require.Contains(t, name, "#1)")
443
444	// We should be able to resolve team2 by name with suffix. This is where
445	// the CORE-9732 cache bug was.
446	lookupTeamID, err = ann.lookupImplicitTeam(false /* create */, name, false /* public */)
447	require.NoError(t, err)
448	require.Equal(t, teamid2, lookupTeamID)
449}
450
451func TestResolveSBSConsolidatedTeamWithConflict(t *testing.T) {
452	tt := newTeamTester(t)
453	defer tt.cleanup()
454
455	ann := tt.addUser("ann")
456	bob := tt.addUser("bob")
457
458	// Create two implicit teams that will become conflicted later
459	// - alice,bob
460	// - alice,bob#bob@rooter
461	// The second team will consolidate to "alice,bob", but since there
462	// already is "alice,bob", it will become "conflicted copy #1".
463
464	impteamName1 := fmt.Sprintf("%s,%s", ann.username, bob.username)
465	_, err := ann.lookupImplicitTeam(true /* create */, impteamName1, false /* public */)
466	require.NoError(t, err)
467
468	impteamName2 := fmt.Sprintf("%s,%s#%s@rooter", ann.username, bob.username, bob.username)
469	_, err = ann.lookupImplicitTeam(true /* create */, impteamName2, false /* public */)
470	require.NoError(t, err)
471
472	// Make sure we can resolve them right now.
473	teamid1, err := ann.lookupImplicitTeam(false /* create */, impteamName1, false /* public */)
474	require.NoError(t, err)
475
476	teamid2, err := ann.lookupImplicitTeam(false /* create */, impteamName2, false /* public */)
477	require.NoError(t, err)
478
479	// Make sure we can load these teams.
480	teamObj1 := ann.loadTeamByID(teamid1, true /* admin */)
481	teamObj2 := ann.loadTeamByID(teamid2, true /* admin */)
482
483	name, err := teamObj1.ImplicitTeamDisplayNameString(context.Background())
484	require.NoError(t, err)
485	require.Equal(t, impteamName1, name)
486	t.Logf("Team 1 display name is: %s", name)
487	name, err = teamObj2.ImplicitTeamDisplayNameString(context.Background())
488	require.NoError(t, err)
489	require.Equal(t, impteamName2, name)
490	t.Logf("Team 2 (w/ rooter) display name is: %s", name)
491
492	// Bob proves rooter.
493	bob.kickTeamRekeyd()
494	bob.proveRooter()
495
496	// Wait till team2 resolves.
497	ann.pollForTeamSeqnoLinkWithLoadArgs(keybase1.LoadTeamArg{
498		ID:          teamid2,
499		ForceRepoll: true,
500	}, keybase1.Seqno(2))
501
502	// Make sure teams are still loadable by ID.
503	_ = ann.loadTeamByID(teamid1, true /* admin */)
504	teamObj2 = ann.loadTeamByID(teamid2, true /* admin */)
505
506	name, err = teamObj2.ImplicitTeamDisplayNameString(context.Background())
507	require.NoError(t, err)
508	t.Logf("Second team became: %s", name)
509	require.Contains(t, name, "(conflicted copy")
510	require.Contains(t, name, "#1)")
511
512	// See if we can lookup this team.
513	lookupTeamID, err := ann.lookupImplicitTeam(false /* create */, name, false /* public */)
514	require.NoError(t, err)
515	require.Equal(t, teamid2, lookupTeamID)
516}
517
518func TestCreateAndResolveEmailImpTeam(t *testing.T) {
519	tt := newTeamTester(t)
520	defer tt.cleanup()
521
522	ann := tt.addUser("ann")
523	bob := tt.addUser("bob")
524
525	email2 := keybase1.EmailAddress("BOB+" + bob.userInfo.email)
526	err := emails.AddEmail(bob.MetaContext(), email2, keybase1.IdentityVisibility_PRIVATE)
527	require.NoError(t, err)
528	err = kbtest.VerifyEmailAuto(bob.MetaContext(), email2)
529	require.NoError(t, err)
530
531	t.Logf("Bob's email is: %q", email2)
532
533	// Display names have to be lowercase
534	impteamName := fmt.Sprintf("%s,[%s]@email", ann.username, strings.ToLower(string(email2)))
535	t.Logf("Display name is: %q", impteamName)
536
537	teamID, err := ann.lookupImplicitTeam(true /* create */, impteamName, false /* public */)
538	require.NoError(t, err)
539
540	// Bob sets "BOB+..." email to public
541	bob.kickTeamRekeyd()
542	err = emails.SetVisibilityEmail(bob.MetaContext(), email2, keybase1.IdentityVisibility_PUBLIC)
543	require.NoError(t, err)
544
545	ann.pollForTeamSeqnoLinkWithLoadArgs(keybase1.LoadTeamArg{
546		ID:          teamID,
547		ForceRepoll: true,
548	}, keybase1.Seqno(2))
549
550	teamID2, err := bob.lookupImplicitTeam(false /* create */, impteamName, false /* public */)
551	require.NoError(t, err)
552	require.Equal(t, teamID, teamID2)
553}
554
555func TestCreateImpteamWithoutTOFUResolver(t *testing.T) {
556	tt := newTeamTester(t)
557	defer tt.cleanup()
558
559	ann := tt.addUser("ann")
560	ann.disableTOFUSearch()
561
562	phone := kbtest.GenerateTestPhoneNumber()
563	email := "aa" + ann.userInfo.email
564
565	for _, impteamName := range []string{
566		fmt.Sprintf("%s,%s@phone", ann.username, phone),
567		fmt.Sprintf("%s,[%s]@email", ann.username, email),
568	} {
569		_, err := ann.lookupImplicitTeam(true /* create */, impteamName, false /* public */)
570		require.Error(t, err)
571		require.Contains(t, err.Error(), "error 602") // user cannot search for assertions
572	}
573
574	// Make sure no teams got created.
575	res, err := ann.teamsClient.TeamListVerified(context.TODO(), keybase1.TeamListVerifiedArg{
576		IncludeImplicitTeams: true,
577	})
578	require.NoError(t, err)
579	require.Len(t, res.Teams, 0)
580}
581