1package systests
2
3import (
4	"strings"
5	"testing"
6
7	"golang.org/x/net/context"
8
9	libkb "github.com/keybase/client/go/libkb"
10	keybase1 "github.com/keybase/client/go/protocol/keybase1"
11	teams "github.com/keybase/client/go/teams"
12	"github.com/stretchr/testify/require"
13)
14
15// bob resets, implicit team lookup should still work for ann
16func TestImplicitTeamReset(t *testing.T) {
17	tt := newTeamTester(t)
18	defer tt.cleanup()
19
20	ann := tt.addUser("ann")
21	t.Logf("Signed up ann (%s)", ann.username)
22
23	bob := tt.addUser("bob")
24	t.Logf("Signed up bob (%s)", bob.username)
25
26	displayName := strings.Join([]string{ann.username, bob.username}, ",")
27	iteam, err := ann.lookupImplicitTeam(true /*create*/, displayName, false /*isPublic*/)
28	require.NoError(t, err)
29	t.Logf("team created (%s)", iteam)
30
31	iteam2, err := ann.lookupImplicitTeam(false /*create*/, displayName, false /*isPublic*/)
32	require.NoError(t, err)
33	require.Equal(t, iteam, iteam2, "second lookup should return same team")
34	t.Logf("team looked up before reset")
35
36	bob.reset()
37	t.Logf("Reset bob (%s)", bob.username)
38
39	iteam3, err := ann.lookupImplicitTeam(false /*create*/, displayName, false /*isPublic*/)
40	require.NoError(t, err)
41	require.Equal(t, iteam, iteam3, "lookup after reset should return same team")
42	t.Logf("team looked up before reset")
43}
44
45func TestImplicitTeamUserReset(t *testing.T) {
46	ctx := newSMUContext(t)
47	defer ctx.cleanup()
48
49	// Sign up two users, bob and alice.
50	alice := ctx.installKeybaseForUser("alice", 10)
51	alice.signup()
52	divDebug(ctx, "Signed up alice (%s)", alice.username)
53	bob := ctx.installKeybaseForUser("bob", 10)
54	bob.signup()
55	divDebug(ctx, "Signed up bob (%s)", bob.username)
56
57	displayName := strings.Join([]string{alice.username, bob.username}, ",")
58	team := alice.lookupImplicitTeam(true /*create*/, displayName, false /*isPublic*/)
59
60	divDebug(ctx, "Created implicit team %s\n", team.ID)
61
62	// Reset bob and reprovision.
63	bob.reset()
64	divDebug(ctx, "Reset bob (%s)", bob.username)
65
66	bob.loginAfterReset(10)
67	divDebug(ctx, "Bob logged in after reset")
68
69	// Setup team loader on alice
70	G := alice.getPrimaryGlobalContext()
71	teams.NewTeamLoaderAndInstall(G)
72
73	tryLoad := func(teamID keybase1.TeamID) (res *teams.Team) {
74		res, err := teams.Load(context.TODO(), G, keybase1.LoadTeamArg{
75			ID:          teamID,
76			Public:      teamID.IsPublic(),
77			ForceRepoll: true,
78		})
79		require.NoError(t, err)
80		return res
81	}
82
83	tryLoad(team.ID)
84
85	getRole := func(username string) keybase1.TeamRole {
86		g := G
87		loadUserArg := libkb.NewLoadUserArg(g).
88			WithNetContext(context.TODO()).
89			WithName(username).
90			WithPublicKeyOptional().
91			WithForcePoll(true)
92		upak, _, err := g.GetUPAKLoader().LoadV2(loadUserArg)
93		require.NoError(t, err)
94
95		team, err := teams.GetForTeamManagementByTeamID(context.TODO(), g, team.ID, false)
96		require.NoError(t, err)
97		role, err := team.MemberRole(context.TODO(), upak.Current.ToUserVersion())
98		require.NoError(t, err)
99		return role
100	}
101
102	// Bob's role should be NONE since he's still reset.
103	role := getRole(bob.username)
104	require.Equal(t, role, keybase1.TeamRole_NONE)
105
106	// Alice re-adds bob.
107	alice.reAddUserAfterReset(team, bob)
108	divDebug(ctx, "Re-Added bob as an owner")
109
110	// Check if sigchain still plays back correctly
111	tryLoad(team.ID)
112
113	// Check if bob is back as OWNER.
114	role = getRole(bob.username)
115	require.Equal(t, role, keybase1.TeamRole_OWNER)
116
117	// Reset and re-provision bob again.
118	bob.reset()
119	divDebug(ctx, "Reset bob again (%s) (poor bob)", bob.username)
120
121	bob.loginAfterReset(10)
122	divDebug(ctx, "Bob logged in after reset")
123
124	// Check if sigchain plays correctly, check if role is NONE.
125	tryLoad(team.ID)
126
127	role = getRole(bob.username)
128	require.Equal(t, role, keybase1.TeamRole_NONE)
129
130	// Alice re-adds bob, again.
131	alice.reAddUserAfterReset(team, bob)
132	divDebug(ctx, "Re-Added bob as an owner again")
133
134	// Check if sigchain plays correctly, at this point there are two
135	// sigs similar to:
136	//   "change_membership: { owner: ['xxxx%6'], none: ['xxxx%3'] }"
137	// with uids and eldest from before and after reset.
138	tryLoad(team.ID)
139
140	role = getRole(bob.username)
141	require.Equal(t, role, keybase1.TeamRole_OWNER)
142}
143
144// ann and bob both reset
145func TestImplicitTeamResetAll(t *testing.T) {
146	ctx := newSMUContext(t)
147	defer ctx.cleanup()
148
149	ann := ctx.installKeybaseForUser("ann", 10)
150	ann.signup()
151	ann.registerForNotifications()
152	divDebug(ctx, "Signed up ann (%s)", ann.username)
153
154	bob := ctx.installKeybaseForUser("bob", 10)
155	bob.signup()
156	bob.registerForNotifications()
157	divDebug(ctx, "Signed up bob (%s)", bob.username)
158
159	displayName := strings.Join([]string{ann.username, bob.username}, ",")
160	iteam := ann.lookupImplicitTeam(true /*create*/, displayName, false /*isPublic*/)
161	divDebug(ctx, "team created (%s)", iteam.ID)
162
163	iteam2 := ann.lookupImplicitTeam(false /*create*/, displayName, false /*isPublic*/)
164	require.Equal(t, iteam.ID, iteam2.ID, "second lookup should return same team")
165	divDebug(ctx, "team looked up before reset")
166
167	bob.reset()
168	divDebug(ctx, "Reset bob (%s)", bob.username)
169
170	ann.reset()
171	divDebug(ctx, "Reset ann (%s)", ann.username)
172
173	ann.loginAfterReset(10)
174	divDebug(ctx, "Ann logged in after reset")
175
176	ann.waitForTeamAbandoned(iteam.ID)
177
178	iteam3 := ann.lookupImplicitTeam(true /*create*/, displayName, false /*isPublic*/)
179	require.NotEqual(t, iteam.ID, iteam3.ID, "lookup after resets should return different team")
180	divDebug(ctx, "team looked up after resets")
181}
182
183func TestImplicitTeamResetAndSBSBringback(t *testing.T) {
184	// 1. ann and bob (both PUKful) make imp team
185	// 2. bob resets
186	// 3. bob doesn't get a PUK
187	// 4. ann re-adds bob (this just adds invite link, doesn't remove old PUKful bob)
188	// 5. bob gets a PUK
189	// 6. he should be automatically brought back as crypto member by alice
190	tt := newTeamTester(t)
191	defer tt.cleanup()
192
193	ann := tt.addUser("ann")
194	t.Logf("Signed up ann (%s)", ann.username)
195
196	bob := tt.addUser("bob")
197	t.Logf("Signed up bob (%s)", bob.username)
198
199	// (1)
200	displayName := strings.Join([]string{ann.username, bob.username}, ",")
201	iteam, err := ann.lookupImplicitTeam(true /* create */, displayName, false /* isPublic */)
202	require.NoError(t, err)
203	t.Logf("impteam created for %q (id: %s)", displayName, iteam)
204
205	bob.kickTeamRekeyd()
206	bob.reset()                  // (2)
207	bob.loginAfterResetPukless() // (3)
208
209	ann.reAddUserAfterReset(iteam, bob) // (4)
210
211	teamObj := ann.loadTeamByID(iteam, true)
212	nextSeqno := teamObj.NextSeqno()
213
214	bob.perUserKeyUpgrade() // (5)
215
216	t.Logf("Bob upgraded puk, polling for seqno %d", nextSeqno)
217	ann.pollForTeamSeqnoLinkWithLoadArgs(keybase1.LoadTeamArg{ID: iteam}, nextSeqno) // (6)
218
219	pollForTrue(t, ann.tc.G, func(i int) bool {
220		teamObj = ann.loadTeamByID(iteam, true)
221		role, err := teamObj.MemberRole(context.Background(), bob.userVersion())
222		require.NoError(t, err)
223		return role == keybase1.TeamRole_OWNER
224	})
225
226	invites := teamObj.GetActiveAndObsoleteInvites()
227	require.Equal(t, 0, len(invites), "leftover invite")
228}
229
230func testImplicitResetParameterized(t *testing.T, startPUK, getPUKAfter bool) {
231	tt := newTeamTester(t)
232	defer tt.cleanup()
233
234	ann := tt.addUser("ann")
235	t.Logf("Signed up ann (%s)", ann.username)
236
237	var bob *userPlusDevice
238	if startPUK {
239		bob = tt.addUser("bob")
240		t.Logf("Signed up bob (%s)", bob.username)
241	} else {
242		bob = tt.addPuklessUser("bob")
243		t.Logf("Signed up PUKless bob (%s)", bob.username)
244	}
245
246	displayName := strings.Join([]string{ann.username, bob.username}, ",")
247	iteam, err := ann.lookupImplicitTeam(true /* create */, displayName, false /* isPublic */)
248	require.NoError(t, err)
249	t.Logf("impteam created for %q (id: %s)", displayName, iteam)
250
251	ann.kickTeamRekeyd()
252	bob.reset()
253	if getPUKAfter {
254		// Bob resets and gets a PUK afterwards
255		bob.loginAfterReset()
256	} else {
257		// Bob resets and does not get a PUK.
258		bob.loginAfterResetPukless()
259	}
260
261	iteam2, err := ann.lookupImplicitTeam(false /* create */, displayName, false /* isPublic */)
262	require.NoError(t, err)
263	require.Equal(t, iteam, iteam2)
264
265	if startPUK {
266		// Wait for rotation after bob resets.
267		ann.waitForAnyRotateByID(iteam2, keybase1.Seqno(1), keybase1.Seqno(1))
268	}
269	ann.reAddUserAfterReset(iteam, bob)
270
271	if !getPUKAfter {
272		teamObj := ann.loadTeamByID(iteam, true)
273
274		// Bob is not a crypto member so no "real" role
275		role, err := teamObj.MemberRole(context.Background(), bob.userVersion())
276		require.NoError(t, err)
277		require.Equal(t, keybase1.TeamRole_NONE, role)
278
279		// but should have active invite
280		invite, uv, found := teamObj.FindActiveKeybaseInvite(bob.uid)
281		require.True(t, found)
282		require.EqualValues(t, bob.userVersion(), uv)
283		require.Equal(t, keybase1.TeamRole_OWNER, invite.Role)
284
285		// bob upgrades PUK
286		bob.kickTeamRekeyd()
287		bob.perUserKeyUpgrade()
288
289		// Wait for SBS
290		expectedSeqno := keybase1.Seqno(3)
291		ann.pollForTeamSeqnoLinkWithLoadArgs(keybase1.LoadTeamArg{ID: iteam}, expectedSeqno)
292	}
293
294	teamObj := ann.loadTeamByID(iteam, true)
295
296	// Bob is now a real crypto member!
297	role, err := teamObj.MemberRole(context.Background(), bob.userVersion())
298	require.NoError(t, err)
299	require.Equal(t, keybase1.TeamRole_OWNER, role)
300
301	// Make sure we are still getting the same team.
302	iteam3, err := ann.lookupImplicitTeam(false /* create */, displayName, false /* isPublic */)
303	require.Equal(t, iteam, iteam3)
304	require.NoError(t, err)
305}
306
307func TestImplicitTeamResetNoPUKtoNoPUK(t *testing.T) {
308	testImplicitResetParameterized(t, false /* startPUK */, false /* getPUKAfter */)
309}
310
311func TestImplicitTeamResetNoPUKtoPUK(t *testing.T) {
312	testImplicitResetParameterized(t, false /* startPUK */, true /* getPUKAfter */)
313}
314
315func TestImplicitTeamResetPUKtoNoPUK(t *testing.T) {
316	// We are lucky this case even works, it breaks the rules a little
317	// bit: there is no way to post removeMember+addInvite in one
318	// link, so when PUKful bob resets and ann re-adds him as PUKless,
319	// only invite link is posted. So technically there are 3 active
320	// people in the team at the time:
321	//   ann, PUKful bob, PUKless (invited) bob.
322
323	testImplicitResetParameterized(t, true /* startPUK */, false /* getPUKAfter */)
324}
325
326func TestImplicitTeamResetNoPukEncore(t *testing.T) {
327	// 1. ann and bob (both PUKful) make imp team
328	// 2. bob resets
329	// 3. bob doesn't get a PUK
330	// 4. ann re-adds bob (this just adds invite link, doesn't remove old PUKful bob)
331	// (up to this point, this case is tested in
332	// TestImplicitResetPUKtoNoPUK and TestChatSrvUserReset)
333	// 5. now bob resets again, but this time gets a PUK
334	// 6. when they are re-added, old PUKful bob is removed to make
335	//    room for new PUK-ful bob, and old invite is also sweeped
336	//    (completed).
337	tt := newTeamTester(t)
338	defer tt.cleanup()
339
340	ann := tt.addUser("ann")
341	t.Logf("Signed up ann (%s)", ann.username)
342
343	bob := tt.addUser("bob")
344	t.Logf("Signed up bob (%s)", bob.username)
345
346	// (1)
347	displayName := strings.Join([]string{ann.username, bob.username}, ",")
348	iteam, err := ann.lookupImplicitTeam(true /* create */, displayName, false /* isPublic */)
349	require.NoError(t, err)
350	t.Logf("impteam created for %q (id: %s)", displayName, iteam)
351
352	bob.reset()                  // (2)
353	bob.loginAfterResetPukless() // (3)
354
355	ann.reAddUserAfterReset(iteam, bob) // (4)
356
357	bob.reset() // (5)
358	bob.loginAfterReset()
359
360	ann.reAddUserAfterReset(iteam, bob) // (6)
361
362	teamObj := ann.loadTeamByID(iteam, true)
363	role, err := teamObj.MemberRole(context.Background(), bob.userVersion())
364	require.NoError(t, err)
365	require.Equal(t, keybase1.TeamRole_OWNER, role)
366
367	invites := teamObj.GetActiveAndObsoleteInvites()
368	require.Equal(t, 0, len(invites), "leftover invite")
369}
370
371func TestImplicitTeamResetBadReadds(t *testing.T) {
372	// Check if we can't ruin implicit team state by bad re-adds.
373	tt := newTeamTester(t)
374	defer tt.cleanup()
375
376	ann := tt.addUser("ann")
377	bob := tt.addUser("bob")
378	pam := tt.addPuklessUser("pam")
379
380	displayName := strings.Join([]string{ann.username, bob.username, pam.username}, ",")
381	iteam, err := ann.lookupImplicitTeam(true /* create */, displayName, false /* isPublic */)
382	require.NoError(t, err)
383	t.Logf("impteam created for %q (id: %s)", displayName, iteam)
384
385	bob.reset()
386	bob.loginAfterResetPukless()
387	t.Logf("%s reset and is now PUKless", bob.username)
388
389	teamObj := ann.loadTeamByID(iteam, true /* admin */)
390	_, err = teamObj.InviteMember(context.Background(), bob.username, keybase1.TeamRole_READER, libkb.NewNormalizedUsername(bob.username), bob.userVersion())
391	require.Error(t, err)
392	t.Logf("Error of InviteMember(bob, READER) is: %v", err)
393
394	pam.reset()
395	pam.loginAfterResetPukless()
396	t.Logf("%s reset and is now PUKless again", pam.username)
397
398	_, err = teamObj.InviteMember(context.Background(), pam.username, keybase1.TeamRole_READER, libkb.NewNormalizedUsername(pam.username), pam.userVersion())
399	require.Error(t, err)
400	t.Logf("Error of InviteMember(pam, READER) is: %v", err)
401}
402