1package login
2
3import (
4	"io/ioutil"
5	"os"
6	"path/filepath"
7	"strings"
8	"testing"
9
10	"github.com/hashicorp/consul/agent"
11	"github.com/hashicorp/consul/agent/consul/authmethod/kubeauth"
12	"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
13	"github.com/hashicorp/consul/api"
14	"github.com/hashicorp/consul/command/acl"
15	"github.com/hashicorp/consul/sdk/testutil"
16	"github.com/hashicorp/consul/testrpc"
17	"github.com/mitchellh/cli"
18	"github.com/stretchr/testify/require"
19)
20
21func TestLoginCommand_noTabs(t *testing.T) {
22	t.Parallel()
23
24	if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
25		t.Fatal("help has tabs")
26	}
27}
28
29func TestLoginCommand(t *testing.T) {
30	t.Parallel()
31
32	testDir := testutil.TempDir(t, "acl")
33	defer os.RemoveAll(testDir)
34
35	a := agent.NewTestAgent(t, t.Name(), `
36	primary_datacenter = "dc1"
37	acl {
38		enabled = true
39		tokens {
40			master = "root"
41		}
42	}`)
43
44	defer a.Shutdown()
45	testrpc.WaitForLeader(t, a.RPC, "dc1")
46
47	client := a.Client()
48
49	t.Run("method is required", func(t *testing.T) {
50		ui := cli.NewMockUi()
51		cmd := New(ui)
52
53		args := []string{
54			"-http-addr=" + a.HTTPAddr(),
55			"-token=root",
56		}
57
58		code := cmd.Run(args)
59		require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
60		require.Contains(t, ui.ErrorWriter.String(), "Missing required '-method' flag")
61	})
62
63	tokenSinkFile := filepath.Join(testDir, "test.token")
64
65	t.Run("token-sink-file is required", func(t *testing.T) {
66		ui := cli.NewMockUi()
67		cmd := New(ui)
68
69		args := []string{
70			"-http-addr=" + a.HTTPAddr(),
71			"-token=root",
72			"-method=test",
73		}
74
75		code := cmd.Run(args)
76		require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
77		require.Contains(t, ui.ErrorWriter.String(), "Missing required '-token-sink-file' flag")
78	})
79
80	t.Run("bearer-token-file is required", func(t *testing.T) {
81		defer os.Remove(tokenSinkFile)
82
83		ui := cli.NewMockUi()
84		cmd := New(ui)
85
86		args := []string{
87			"-http-addr=" + a.HTTPAddr(),
88			"-token=root",
89			"-method=test",
90			"-token-sink-file", tokenSinkFile,
91		}
92
93		code := cmd.Run(args)
94		require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
95		require.Contains(t, ui.ErrorWriter.String(), "Missing required '-bearer-token-file' flag")
96	})
97
98	bearerTokenFile := filepath.Join(testDir, "bearer.token")
99
100	t.Run("bearer-token-file is empty", func(t *testing.T) {
101		defer os.Remove(tokenSinkFile)
102
103		require.NoError(t, ioutil.WriteFile(bearerTokenFile, []byte(""), 0600))
104
105		ui := cli.NewMockUi()
106		cmd := New(ui)
107
108		args := []string{
109			"-http-addr=" + a.HTTPAddr(),
110			"-token=root",
111			"-method=test",
112			"-token-sink-file", tokenSinkFile,
113			"-bearer-token-file", bearerTokenFile,
114		}
115
116		code := cmd.Run(args)
117		require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
118		require.Contains(t, ui.ErrorWriter.String(), "No bearer token found in")
119	})
120
121	require.NoError(t, ioutil.WriteFile(bearerTokenFile, []byte("demo-token"), 0600))
122
123	t.Run("try login with no method configured", func(t *testing.T) {
124		defer os.Remove(tokenSinkFile)
125
126		ui := cli.NewMockUi()
127		cmd := New(ui)
128
129		args := []string{
130			"-http-addr=" + a.HTTPAddr(),
131			"-token=root",
132			"-method=test",
133			"-token-sink-file", tokenSinkFile,
134			"-bearer-token-file", bearerTokenFile,
135		}
136
137		code := cmd.Run(args)
138		require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
139		require.Contains(t, ui.ErrorWriter.String(), "403 (ACL not found)")
140	})
141
142	testSessionID := testauth.StartSession()
143	defer testauth.ResetSession(testSessionID)
144
145	testauth.InstallSessionToken(
146		testSessionID,
147		"demo-token",
148		"default", "demo", "76091af4-4b56-11e9-ac4b-708b11801cbe",
149	)
150
151	{
152		_, _, err := client.ACL().AuthMethodCreate(
153			&api.ACLAuthMethod{
154				Name: "test",
155				Type: "testing",
156				Config: map[string]interface{}{
157					"SessionID": testSessionID,
158				},
159			},
160			&api.WriteOptions{Token: "root"},
161		)
162		require.NoError(t, err)
163	}
164
165	t.Run("try login with method configured but no binding rules", func(t *testing.T) {
166		defer os.Remove(tokenSinkFile)
167
168		ui := cli.NewMockUi()
169		cmd := New(ui)
170
171		args := []string{
172			"-http-addr=" + a.HTTPAddr(),
173			"-token=root",
174			"-method=test",
175			"-token-sink-file", tokenSinkFile,
176			"-bearer-token-file", bearerTokenFile,
177		}
178
179		code := cmd.Run(args)
180		require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String())
181		require.Contains(t, ui.ErrorWriter.String(), "403 (Permission denied)")
182	})
183
184	{
185		_, _, err := client.ACL().BindingRuleCreate(&api.ACLBindingRule{
186			AuthMethod: "test",
187			BindType:   api.BindingRuleBindTypeService,
188			BindName:   "${serviceaccount.name}",
189			Selector:   "serviceaccount.namespace==default",
190		},
191			&api.WriteOptions{Token: "root"},
192		)
193		require.NoError(t, err)
194	}
195
196	t.Run("try login with method configured and binding rules", func(t *testing.T) {
197		defer os.Remove(tokenSinkFile)
198
199		ui := cli.NewMockUi()
200		cmd := New(ui)
201
202		args := []string{
203			"-http-addr=" + a.HTTPAddr(),
204			"-token=root",
205			"-method=test",
206			"-token-sink-file", tokenSinkFile,
207			"-bearer-token-file", bearerTokenFile,
208		}
209
210		code := cmd.Run(args)
211		require.Equal(t, 0, code, "err: %s", ui.ErrorWriter.String())
212		require.Empty(t, ui.ErrorWriter.String())
213		require.Empty(t, ui.OutputWriter.String())
214
215		raw, err := ioutil.ReadFile(tokenSinkFile)
216		require.NoError(t, err)
217
218		token := strings.TrimSpace(string(raw))
219		require.Len(t, token, 36, "must be a valid uid: %s", token)
220	})
221}
222
223func TestLoginCommand_k8s(t *testing.T) {
224	t.Parallel()
225
226	testDir := testutil.TempDir(t, "acl")
227	defer os.RemoveAll(testDir)
228
229	a := agent.NewTestAgent(t, t.Name(), `
230	primary_datacenter = "dc1"
231	acl {
232		enabled = true
233		tokens {
234			master = "root"
235		}
236	}`)
237
238	defer a.Shutdown()
239	testrpc.WaitForLeader(t, a.RPC, "dc1")
240
241	client := a.Client()
242
243	tokenSinkFile := filepath.Join(testDir, "test.token")
244	bearerTokenFile := filepath.Join(testDir, "bearer.token")
245
246	// the "B" jwt will be the one being reviewed
247	require.NoError(t, ioutil.WriteFile(bearerTokenFile, []byte(acl.TestKubernetesJWT_B), 0600))
248
249	// spin up a fake api server
250	testSrv := kubeauth.StartTestAPIServer(t)
251	defer testSrv.Stop()
252
253	testSrv.AuthorizeJWT(acl.TestKubernetesJWT_A)
254	testSrv.SetAllowedServiceAccount(
255		"default",
256		"demo",
257		"76091af4-4b56-11e9-ac4b-708b11801cbe",
258		"",
259		acl.TestKubernetesJWT_B,
260	)
261
262	{
263		_, _, err := client.ACL().AuthMethodCreate(
264			&api.ACLAuthMethod{
265				Name: "k8s",
266				Type: "kubernetes",
267				Config: map[string]interface{}{
268					"Host":   testSrv.Addr(),
269					"CACert": testSrv.CACert(),
270					// the "A" jwt will be the one with token review privs
271					"ServiceAccountJWT": acl.TestKubernetesJWT_A,
272				},
273			},
274			&api.WriteOptions{Token: "root"},
275		)
276		require.NoError(t, err)
277	}
278
279	{
280		_, _, err := client.ACL().BindingRuleCreate(&api.ACLBindingRule{
281			AuthMethod: "k8s",
282			BindType:   api.BindingRuleBindTypeService,
283			BindName:   "${serviceaccount.name}",
284			Selector:   "serviceaccount.namespace==default",
285		},
286			&api.WriteOptions{Token: "root"},
287		)
288		require.NoError(t, err)
289	}
290
291	t.Run("try login with method configured and binding rules", func(t *testing.T) {
292		defer os.Remove(tokenSinkFile)
293
294		ui := cli.NewMockUi()
295		cmd := New(ui)
296
297		args := []string{
298			"-http-addr=" + a.HTTPAddr(),
299			"-token=root",
300			"-method=k8s",
301			"-token-sink-file", tokenSinkFile,
302			"-bearer-token-file", bearerTokenFile,
303		}
304
305		code := cmd.Run(args)
306		require.Equal(t, 0, code, "err: %s", ui.ErrorWriter.String())
307		require.Empty(t, ui.ErrorWriter.String())
308		require.Empty(t, ui.OutputWriter.String())
309
310		raw, err := ioutil.ReadFile(tokenSinkFile)
311		require.NoError(t, err)
312
313		token := strings.TrimSpace(string(raw))
314		require.Len(t, token, 36, "must be a valid uid: %s", token)
315	})
316}
317