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