1package nomad
2
3import (
4	"context"
5	"errors"
6	"sync"
7	"testing"
8	"time"
9
10	"github.com/hashicorp/nomad/command/agent/consul"
11	"github.com/hashicorp/nomad/helper"
12	"github.com/hashicorp/nomad/helper/testlog"
13	"github.com/hashicorp/nomad/helper/uuid"
14	"github.com/hashicorp/nomad/nomad/structs"
15	"github.com/stretchr/testify/require"
16	"golang.org/x/time/rate"
17)
18
19var _ ConsulACLsAPI = (*consulACLsAPI)(nil)
20var _ ConsulACLsAPI = (*mockConsulACLsAPI)(nil)
21var _ ConsulConfigsAPI = (*consulConfigsAPI)(nil)
22
23func TestConsulConfigsAPI_SetCE(t *testing.T) {
24	t.Parallel()
25
26	try := func(t *testing.T, expect error, f func(ConsulConfigsAPI) error) {
27		logger := testlog.HCLogger(t)
28		configsAPI := consul.NewMockConfigsAPI(logger)
29		configsAPI.SetError(expect)
30
31		c := NewConsulConfigsAPI(configsAPI, logger)
32		err := f(c) // set the config entry
33
34		switch expect {
35		case nil:
36			require.NoError(t, err)
37		default:
38			require.Equal(t, expect, err)
39		}
40	}
41
42	ctx := context.Background()
43
44	// existing behavior is no set namespace
45	consulNamespace := ""
46
47	ingressCE := new(structs.ConsulIngressConfigEntry)
48	t.Run("ingress ok", func(t *testing.T) {
49		try(t, nil, func(c ConsulConfigsAPI) error {
50			return c.SetIngressCE(ctx, consulNamespace, "ig", ingressCE)
51		})
52	})
53
54	t.Run("ingress fail", func(t *testing.T) {
55		try(t, errors.New("consul broke"), func(c ConsulConfigsAPI) error {
56			return c.SetIngressCE(ctx, consulNamespace, "ig", ingressCE)
57		})
58	})
59
60	terminatingCE := new(structs.ConsulTerminatingConfigEntry)
61	t.Run("terminating ok", func(t *testing.T) {
62		try(t, nil, func(c ConsulConfigsAPI) error {
63			return c.SetTerminatingCE(ctx, consulNamespace, "tg", terminatingCE)
64		})
65	})
66
67	t.Run("terminating fail", func(t *testing.T) {
68		try(t, errors.New("consul broke"), func(c ConsulConfigsAPI) error {
69			return c.SetTerminatingCE(ctx, consulNamespace, "tg", terminatingCE)
70		})
71	})
72
73	// also mesh
74}
75
76type revokeRequest struct {
77	accessorID string
78	committed  bool
79}
80
81type mockConsulACLsAPI struct {
82	lock           sync.Mutex
83	revokeRequests []revokeRequest
84	stopped        bool
85}
86
87func (m *mockConsulACLsAPI) CheckPermissions(context.Context, string, *structs.ConsulUsage, string) error {
88	panic("not implemented yet")
89}
90
91func (m *mockConsulACLsAPI) CreateToken(context.Context, ServiceIdentityRequest) (*structs.SIToken, error) {
92	panic("not implemented yet")
93}
94
95func (m *mockConsulACLsAPI) ListTokens() ([]string, error) {
96	panic("not implemented yet")
97}
98
99func (m *mockConsulACLsAPI) Stop() {
100	m.lock.Lock()
101	defer m.lock.Unlock()
102	m.stopped = true
103}
104
105type mockPurgingServer struct {
106	purgedAccessorIDs []string
107	failure           error
108}
109
110func (mps *mockPurgingServer) purgeFunc(accessors []*structs.SITokenAccessor) error {
111	if mps.failure != nil {
112		return mps.failure
113	}
114
115	for _, accessor := range accessors {
116		mps.purgedAccessorIDs = append(mps.purgedAccessorIDs, accessor.AccessorID)
117	}
118	return nil
119}
120
121func (m *mockConsulACLsAPI) RevokeTokens(_ context.Context, accessors []*structs.SITokenAccessor, committed bool) bool {
122	return m.storeForRevocation(accessors, committed)
123}
124
125func (m *mockConsulACLsAPI) MarkForRevocation(accessors []*structs.SITokenAccessor) {
126	m.storeForRevocation(accessors, true)
127}
128
129func (m *mockConsulACLsAPI) storeForRevocation(accessors []*structs.SITokenAccessor, committed bool) bool {
130	m.lock.Lock()
131	defer m.lock.Unlock()
132
133	for _, accessor := range accessors {
134		m.revokeRequests = append(m.revokeRequests, revokeRequest{
135			accessorID: accessor.AccessorID,
136			committed:  committed,
137		})
138	}
139	return false
140}
141
142func TestConsulACLsAPI_CreateToken(t *testing.T) {
143	t.Parallel()
144
145	try := func(t *testing.T, expErr error) {
146		logger := testlog.HCLogger(t)
147		aclAPI := consul.NewMockACLsAPI(logger)
148		aclAPI.SetError(expErr)
149
150		c := NewConsulACLsAPI(aclAPI, logger, nil)
151
152		ctx := context.Background()
153		sii := ServiceIdentityRequest{
154			ConsulNamespace: "foo-namespace",
155			AllocID:         uuid.Generate(),
156			ClusterID:       uuid.Generate(),
157			TaskName:        "my-task1-sidecar-proxy",
158			TaskKind:        structs.NewTaskKind(structs.ConnectProxyPrefix, "my-service"),
159		}
160
161		token, err := c.CreateToken(ctx, sii)
162
163		if expErr != nil {
164			require.Equal(t, expErr, err)
165			require.Nil(t, token)
166		} else {
167			require.NoError(t, err)
168			require.Equal(t, "foo-namespace", token.ConsulNamespace)
169			require.Equal(t, "my-task1-sidecar-proxy", token.TaskName)
170			require.True(t, helper.IsUUID(token.AccessorID))
171			require.True(t, helper.IsUUID(token.SecretID))
172		}
173	}
174
175	t.Run("create token success", func(t *testing.T) {
176		try(t, nil)
177	})
178
179	t.Run("create token error", func(t *testing.T) {
180		try(t, errors.New("consul broke"))
181	})
182}
183
184func TestConsulACLsAPI_RevokeTokens(t *testing.T) {
185	t.Parallel()
186
187	setup := func(t *testing.T, exp error) (context.Context, ConsulACLsAPI, *structs.SIToken) {
188		logger := testlog.HCLogger(t)
189		aclAPI := consul.NewMockACLsAPI(logger)
190
191		c := NewConsulACLsAPI(aclAPI, logger, nil)
192
193		ctx := context.Background()
194		generated, err := c.CreateToken(ctx, ServiceIdentityRequest{
195			ConsulNamespace: "foo-namespace",
196			ClusterID:       uuid.Generate(),
197			AllocID:         uuid.Generate(),
198			TaskName:        "task1-sidecar-proxy",
199			TaskKind:        structs.NewTaskKind(structs.ConnectProxyPrefix, "service1"),
200		})
201		require.NoError(t, err)
202
203		// set the mock error after calling CreateToken for setting up
204		aclAPI.SetError(exp)
205
206		return context.Background(), c, generated
207	}
208
209	accessors := func(ids ...string) (result []*structs.SITokenAccessor) {
210		for _, id := range ids {
211			result = append(result, &structs.SITokenAccessor{
212				AccessorID:      id,
213				ConsulNamespace: "foo-namespace",
214			})
215		}
216		return
217	}
218
219	t.Run("revoke token success", func(t *testing.T) {
220		ctx, c, token := setup(t, nil)
221		retryLater := c.RevokeTokens(ctx, accessors(token.AccessorID), false)
222		require.False(t, retryLater)
223	})
224
225	t.Run("revoke token non-existent", func(t *testing.T) {
226		ctx, c, _ := setup(t, nil)
227		retryLater := c.RevokeTokens(ctx, accessors(uuid.Generate()), false)
228		require.False(t, retryLater)
229	})
230
231	t.Run("revoke token error", func(t *testing.T) {
232		exp := errors.New("consul broke")
233		ctx, c, token := setup(t, exp)
234		retryLater := c.RevokeTokens(ctx, accessors(token.AccessorID), false)
235		require.True(t, retryLater)
236	})
237}
238
239func TestConsulACLsAPI_MarkForRevocation(t *testing.T) {
240	t.Parallel()
241
242	logger := testlog.HCLogger(t)
243	aclAPI := consul.NewMockACLsAPI(logger)
244
245	c := NewConsulACLsAPI(aclAPI, logger, nil)
246
247	generated, err := c.CreateToken(context.Background(), ServiceIdentityRequest{
248		ConsulNamespace: "foo-namespace",
249		ClusterID:       uuid.Generate(),
250		AllocID:         uuid.Generate(),
251		TaskName:        "task1-sidecar-proxy",
252		TaskKind:        structs.NewTaskKind(structs.ConnectProxyPrefix, "service1"),
253	})
254	require.NoError(t, err)
255
256	// set the mock error after calling CreateToken for setting up
257	aclAPI.SetError(nil)
258
259	accessors := []*structs.SITokenAccessor{{
260		ConsulNamespace: "foo-namespace",
261		AccessorID:      generated.AccessorID,
262	}}
263	c.MarkForRevocation(accessors)
264	require.Len(t, c.bgRetryRevocation, 1)
265	require.Contains(t, c.bgRetryRevocation, accessors[0])
266}
267
268func TestConsulACLsAPI_bgRetryRevoke(t *testing.T) {
269	t.Parallel()
270
271	// manually create so the bg daemon does not run, letting us explicitly
272	// call and test bgRetryRevoke
273	setup := func(t *testing.T) (*consulACLsAPI, *mockPurgingServer) {
274		logger := testlog.HCLogger(t)
275		aclAPI := consul.NewMockACLsAPI(logger)
276		server := new(mockPurgingServer)
277		shortWait := rate.Limit(1 * time.Millisecond)
278
279		return &consulACLsAPI{
280			aclClient: aclAPI,
281			purgeFunc: server.purgeFunc,
282			limiter:   rate.NewLimiter(shortWait, int(shortWait)),
283			stopC:     make(chan struct{}),
284			logger:    logger,
285		}, server
286	}
287
288	t.Run("retry revoke no items", func(t *testing.T) {
289		c, server := setup(t)
290		c.bgRetryRevoke()
291		require.Empty(t, server)
292	})
293
294	t.Run("retry revoke success", func(t *testing.T) {
295		c, server := setup(t)
296		accessorID := uuid.Generate()
297		c.bgRetryRevocation = append(c.bgRetryRevocation, &structs.SITokenAccessor{
298			ConsulNamespace: "foo-namespace",
299			NodeID:          uuid.Generate(),
300			AllocID:         uuid.Generate(),
301			AccessorID:      accessorID,
302			TaskName:        "task1",
303		})
304		require.Empty(t, server.purgedAccessorIDs)
305		c.bgRetryRevoke()
306		require.Equal(t, 1, len(server.purgedAccessorIDs))
307		require.Equal(t, accessorID, server.purgedAccessorIDs[0])
308		require.Empty(t, c.bgRetryRevocation) // should be empty now
309	})
310
311	t.Run("retry revoke failure", func(t *testing.T) {
312		c, server := setup(t)
313		server.failure = errors.New("revocation fail")
314		accessorID := uuid.Generate()
315		c.bgRetryRevocation = append(c.bgRetryRevocation, &structs.SITokenAccessor{
316			ConsulNamespace: "foo-namespace",
317			NodeID:          uuid.Generate(),
318			AllocID:         uuid.Generate(),
319			AccessorID:      accessorID,
320			TaskName:        "task1",
321		})
322		require.Empty(t, server.purgedAccessorIDs)
323		c.bgRetryRevoke()
324		require.Equal(t, 1, len(c.bgRetryRevocation)) // non-empty because purge failed
325		require.Equal(t, accessorID, c.bgRetryRevocation[0].AccessorID)
326	})
327}
328
329func TestConsulACLsAPI_Stop(t *testing.T) {
330	t.Parallel()
331
332	setup := func(t *testing.T) *consulACLsAPI {
333		logger := testlog.HCLogger(t)
334		return NewConsulACLsAPI(nil, logger, nil)
335	}
336
337	c := setup(t)
338	c.Stop()
339	_, err := c.CreateToken(context.Background(), ServiceIdentityRequest{
340		ClusterID: "",
341		AllocID:   "",
342		TaskName:  "",
343	})
344	require.Error(t, err)
345}
346
347func TestConsulACLsAPI_CheckPermissions(t *testing.T) {
348	t.Parallel()
349
350	try := func(t *testing.T, namespace string, usage *structs.ConsulUsage, secretID string, exp error) {
351		logger := testlog.HCLogger(t)
352		aclAPI := consul.NewMockACLsAPI(logger)
353		cAPI := NewConsulACLsAPI(aclAPI, logger, nil)
354
355		err := cAPI.CheckPermissions(context.Background(), namespace, usage, secretID)
356		if exp == nil {
357			require.NoError(t, err)
358		} else {
359			require.Equal(t, exp.Error(), err.Error())
360		}
361	}
362
363	t.Run("check-permissions kv read", func(t *testing.T) {
364		t.Run("uses kv has permission", func(t *testing.T) {
365			u := &structs.ConsulUsage{KV: true}
366			try(t, "default", u, consul.ExampleOperatorTokenID5, nil)
367		})
368
369		t.Run("uses kv without permission", func(t *testing.T) {
370			u := &structs.ConsulUsage{KV: true}
371			try(t, "default", u, consul.ExampleOperatorTokenID1, errors.New("insufficient Consul ACL permissions to use template"))
372		})
373
374		t.Run("uses kv no token", func(t *testing.T) {
375			u := &structs.ConsulUsage{KV: true}
376			try(t, "default", u, "", errors.New("missing consul token"))
377		})
378
379		t.Run("uses kv nonsense token", func(t *testing.T) {
380			u := &structs.ConsulUsage{KV: true}
381			try(t, "default", u, "47d33e22-720a-7fe6-7d7f-418bf844a0be", errors.New("unable to read consul token: no such token"))
382		})
383
384		t.Run("no kv no token", func(t *testing.T) {
385			u := &structs.ConsulUsage{KV: false}
386			try(t, "default", u, "", nil)
387		})
388
389		t.Run("uses kv default token missing permissions", func(t *testing.T) {
390			u := &structs.ConsulUsage{KV: true}
391			try(t, "other", u, consul.ExampleOperatorTokenID5, errors.New(`insufficient Consul ACL permissions to use template`))
392		})
393
394		t.Run("uses kv token in wrong namespace", func(t *testing.T) {
395			u := &structs.ConsulUsage{KV: true}
396			try(t, "other", u, consul.ExampleOperatorTokenID15, errors.New(`consul ACL token cannot use namespace "other"`))
397		})
398	})
399
400	t.Run("check-permissions service write", func(t *testing.T) {
401		usage := &structs.ConsulUsage{Services: []string{"service1"}}
402
403		t.Run("operator has service write", func(t *testing.T) {
404			try(t, "default", usage, consul.ExampleOperatorTokenID1, nil)
405		})
406
407		t.Run("operator has service write but no policy", func(t *testing.T) {
408			try(t, "other", usage, consul.ExampleOperatorTokenID1, errors.New(`insufficient Consul ACL permissions to write service "service1"`))
409		})
410
411		t.Run("operator has token in wrong namespace", func(t *testing.T) {
412			try(t, "other", usage, consul.ExampleOperatorTokenID11, errors.New(`consul ACL token cannot use namespace "other"`))
413		})
414
415		t.Run("operator has service_prefix write", func(t *testing.T) {
416			u := &structs.ConsulUsage{Services: []string{"foo-service1"}}
417			try(t, "default", u, consul.ExampleOperatorTokenID2, nil)
418		})
419
420		t.Run("operator has service_prefix write wrong prefix", func(t *testing.T) {
421			u := &structs.ConsulUsage{Services: []string{"bar-service1"}}
422			try(t, "default", u, consul.ExampleOperatorTokenID2, errors.New(`insufficient Consul ACL permissions to write service "bar-service1"`))
423		})
424
425		t.Run("operator permissions insufficient", func(t *testing.T) {
426			try(t, "default", usage, consul.ExampleOperatorTokenID3, errors.New(`insufficient Consul ACL permissions to write service "service1"`))
427		})
428
429		t.Run("operator provided no token", func(t *testing.T) {
430			try(t, "default", usage, "", errors.New("missing consul token"))
431		})
432
433		t.Run("operator provided nonsense token", func(t *testing.T) {
434			try(t, "default", usage, "f1682bde-1e71-90b1-9204-85d35467ba61", errors.New("unable to read consul token: no such token"))
435		})
436	})
437
438	t.Run("check-permissions connect service identity write", func(t *testing.T) {
439		usage := &structs.ConsulUsage{Kinds: []structs.TaskKind{structs.NewTaskKind(structs.ConnectProxyPrefix, "service1")}}
440
441		t.Run("operator has service write", func(t *testing.T) {
442			try(t, "default", usage, consul.ExampleOperatorTokenID1, nil)
443		})
444
445		t.Run("operator has service write wrong ns", func(t *testing.T) {
446			try(t, "other", usage, consul.ExampleOperatorTokenID1, errors.New(`insufficient Consul ACL permissions to write Connect service "service1"`))
447		})
448
449		t.Run("operator has token in wrong namespace", func(t *testing.T) {
450			try(t, "other", usage, consul.ExampleOperatorTokenID11, errors.New(`consul ACL token cannot use namespace "other"`))
451		})
452
453		t.Run("operator has service_prefix write", func(t *testing.T) {
454			u := &structs.ConsulUsage{Kinds: []structs.TaskKind{structs.NewTaskKind(structs.ConnectProxyPrefix, "foo-service1")}}
455			try(t, "default", u, consul.ExampleOperatorTokenID2, nil)
456		})
457
458		t.Run("operator has service_prefix write wrong prefix", func(t *testing.T) {
459			u := &structs.ConsulUsage{Kinds: []structs.TaskKind{structs.NewTaskKind(structs.ConnectProxyPrefix, "bar-service1")}}
460			try(t, "default", u, consul.ExampleOperatorTokenID2, errors.New(`insufficient Consul ACL permissions to write Connect service "bar-service1"`))
461		})
462
463		t.Run("operator permissions insufficient", func(t *testing.T) {
464			try(t, "default", usage, consul.ExampleOperatorTokenID3, errors.New(`insufficient Consul ACL permissions to write Connect service "service1"`))
465		})
466
467		t.Run("operator provided no token", func(t *testing.T) {
468			try(t, "default", usage, "", errors.New("missing consul token"))
469		})
470
471		t.Run("operator provided nonsense token", func(t *testing.T) {
472			try(t, "default", usage, "f1682bde-1e71-90b1-9204-85d35467ba61", errors.New("unable to read consul token: no such token"))
473		})
474	})
475}
476