open-nomad/command/agent/consul/acl_testing.go
Seth Hoenig 074b76e3bf
consul: check for acceptable service identity on consul tokens (#15928)
When registering a job with a service and 'consul.allow_unauthenticated=false',
we scan the given Consul token for an acceptable policy or role with an
acceptable policy, but did not scan for an acceptable service identity (which
is backed by an acceptable virtual policy). This PR updates our consul token
validation to also accept a matching service identity when registering a service
into Consul.

Fixes #15902
2023-01-27 18:15:51 -06:00

528 lines
14 KiB
Go

package consul
import (
"errors"
"sync"
"time"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/helper/uuid"
)
var _ ACLsAPI = (*MockACLsAPI)(nil)
// MockACLsAPI is a mock of consul.ACLsAPI
type MockACLsAPI struct {
logger hclog.Logger
lock sync.Mutex
state struct {
index uint64
error error
tokens map[string]*api.ACLToken
}
}
func NewMockACLsAPI(l hclog.Logger) *MockACLsAPI {
return &MockACLsAPI{
logger: l.Named("mock_consul"),
state: struct {
index uint64
error error
tokens map[string]*api.ACLToken
}{tokens: make(map[string]*api.ACLToken)},
}
}
// Example Consul policies for use in tests.
const (
ExamplePolicyID1 = "a7c86856-0af5-4ab5-8834-03f4517e5564"
ExamplePolicyID2 = "ffa1b66c-967d-4468-8775-c687b5cfc16e"
ExamplePolicyID3 = "f68f0c36-51f8-4343-97dd-f0d4816c915f"
ExamplePolicyID4 = "1087ff34-b8a0-9bb3-9430-d2f758f52bd3"
)
func (m *MockACLsAPI) PolicyRead(policyID string, _ *api.QueryOptions) (*api.ACLPolicy, *api.QueryMeta, error) {
switch policyID {
case ExamplePolicyID1:
return &api.ACLPolicy{
ID: ExamplePolicyID1,
Name: "example-policy-1",
Rules: `service "service1" { policy = "write" }`,
}, nil, nil
case ExamplePolicyID2:
return &api.ACLPolicy{
ID: ExamplePolicyID2,
Rules: `service_prefix "foo-" { policy = "write" }`,
}, nil, nil
case ExamplePolicyID3:
return &api.ACLPolicy{
ID: ExamplePolicyID3,
Rules: `
service "service1" { policy = "read" }
service "service2" { policy = "write" }`,
}, nil, nil
case ExamplePolicyID4:
return &api.ACLPolicy{
ID: ExamplePolicyID4,
Rules: `key_prefix "" { policy = "read" }`,
}, nil, nil
default:
return nil, nil, errors.New("no such policy")
}
}
// Example Consul roles for use in tests.
const (
ExampleRoleID1 = "e569a3a8-7dfb-b024-e492-e790fe3c4183"
ExampleRoleID2 = "88c825f4-d0da-1c2b-0c1c-cc9fe84c4468"
ExampleRoleID3 = "b19b2058-6205-6dff-d2b0-470f29b8e627"
)
func (m *MockACLsAPI) RoleRead(roleID string, _ *api.QueryOptions) (*api.ACLRole, *api.QueryMeta, error) {
switch roleID {
case ExampleRoleID1:
return &api.ACLRole{
ID: ExampleRoleID1,
Name: "example-role-1",
Policies: []*api.ACLRolePolicyLink{{
ID: ExamplePolicyID1,
Name: "example-policy-1",
}},
ServiceIdentities: nil,
}, nil, nil
case ExampleRoleID2:
return &api.ACLRole{
ID: ExampleRoleID2,
Name: "example-role-2",
Policies: []*api.ACLRolePolicyLink{{
ID: ExamplePolicyID2,
Name: "example-policy-2",
}},
ServiceIdentities: nil,
}, nil, nil
case ExampleRoleID3:
return &api.ACLRole{
ID: ExampleRoleID3,
Name: "example-role-3",
Policies: nil, // todo add more if needed
ServiceIdentities: nil, // todo add more if needed
}, nil, nil
default:
return nil, nil, nil
}
}
// Example Consul ACL tokens for use in tests. These tokens belong to the
// default Consul namespace.
const (
ExampleOperatorTokenID0 = "de591604-86eb-1e6f-8b44-d4db752921ae"
ExampleOperatorTokenID1 = "59c219c2-47e4-43f3-bb45-258fd13f59d5"
ExampleOperatorTokenID2 = "868cc216-e123-4c2b-b362-f4d4c087de8e"
ExampleOperatorTokenID3 = "6177d1b9-c0f6-4118-b891-d818a3cb80b1"
ExampleOperatorTokenID4 = "754ae26c-f3cc-e088-d486-9c0d20f5eaea"
ExampleOperatorTokenID5 = "097cbb45-506b-c79c-ec38-82eb0dc0794a"
ExampleOperatorTokenID6 = "6268bd42-6f72-4c90-9c83-90ed6336dcf9"
)
// Example Consul ACL tokens for use in tests that match the policies as the
// tokens above, but these belong to the "banana" Consul namespace.
const (
ExampleOperatorTokenID10 = "ddfe688f-655f-e8dd-1db5-5650eed00aeb"
ExampleOperatorTokenID11 = "46d09394-598c-1e55-b7fd-64cd2f409707"
ExampleOperatorTokenID12 = "a041cb88-0f4b-0314-89f6-10e1e093d2e5"
ExampleOperatorTokenID13 = "cc22a583-243f-3258-14ad-db0e56749657"
ExampleOperatorTokenID14 = "5b6d0508-13a6-4bc3-33a1-ba1941e1175b"
ExampleOperatorTokenID15 = "e9db1754-c075-d0fc-0a7e-de1e9e7bff98"
)
// Example Consul ACL tokens for use in tests that match the policies as the
// tokens above, but these belong to the "default" Consul namespace.
const (
ExampleOperatorTokenID20 = "937b3287-557c-5af8-beb0-d62191988719"
ExampleOperatorTokenID21 = "067fd927-abfb-d98f-b693-bb05dccea565"
ExampleOperatorTokenID22 = "71f8030f-f6bd-6157-6614-ba6a0bbfba9f"
ExampleOperatorTokenID23 = "1dfd2982-b7a1-89ec-09b4-74712983d13c"
ExampleOperatorTokenID24 = "d26dbc2a-d5d8-e3d9-8a38-e05dec499124"
ExampleOperatorTokenID25 = "dd5a8eef-554c-a1f9-fdb8-f25eb77258bc"
)
var (
// In no Consul namespace (OSS, ENT w/o Namespaces)
ExampleOperatorToken0 = &api.ACLToken{
SecretID: ExampleOperatorTokenID0,
AccessorID: "228865c6-3bf6-6683-df03-06dea2779088 ",
Description: "Operator Token 0",
Namespace: "",
}
ExampleOperatorToken1 = &api.ACLToken{
SecretID: ExampleOperatorTokenID1,
AccessorID: "e341bacd-535e-417c-8f45-f88d7faffcaf",
Description: "Operator Token 1",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID1,
}},
Namespace: "",
}
ExampleOperatorToken2 = &api.ACLToken{
SecretID: ExampleOperatorTokenID2,
AccessorID: "615b4d77-5164-4ec6-b616-24c0b24ac9cb",
Description: "Operator Token 2",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID2,
}},
Namespace: "",
}
ExampleOperatorToken3 = &api.ACLToken{
SecretID: ExampleOperatorTokenID3,
AccessorID: "6b7de0d7-15f7-45b4-95eb-fb775bfe3fdc",
Description: "Operator Token 3",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID3,
}},
Namespace: "",
}
ExampleOperatorToken4 = &api.ACLToken{
SecretID: ExampleOperatorTokenID4,
AccessorID: "7b5fdb1a-71e5-f3d8-2cfe-448d973f327d",
Description: "Operator Token 4",
Policies: nil, // no direct policy, only roles
Roles: []*api.ACLTokenRoleLink{{
ID: ExampleRoleID1,
Name: "example-role-1",
}},
Namespace: "",
}
ExampleOperatorToken5 = &api.ACLToken{
SecretID: ExampleOperatorTokenID5,
AccessorID: "cf39aad5-00c3-af23-cf0b-75d41e12f28d",
Description: "Operator Token 5",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID4,
}},
Namespace: "",
}
ExampleOperatorToken6 = &api.ACLToken{
SecretID: ExampleOperatorTokenID6,
AccessorID: "93786935-8856-6e17-0488-c5370a1f044e",
Description: "Operator Token 6",
ServiceIdentities: []*api.ACLServiceIdentity{
{ServiceName: "service1"},
},
Namespace: "",
}
// In Consul namespace "banana"
ExampleOperatorToken10 = &api.ACLToken{
SecretID: ExampleOperatorTokenID10,
AccessorID: "76a2c3b5-5d64-9089-f701-660eec2d3554",
Description: "Operator Token 0",
Namespace: "banana",
}
ExampleOperatorToken11 = &api.ACLToken{
SecretID: ExampleOperatorTokenID11,
AccessorID: "40f2a36a-0a65-1972-106c-b2e5dd46d6e8",
Description: "Operator Token 1",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID1,
}},
Namespace: "banana",
}
ExampleOperatorToken12 = &api.ACLToken{
SecretID: ExampleOperatorTokenID12,
AccessorID: "894f2c5c-b285-71bf-4acb-6344cecf71f3",
Description: "Operator Token 2",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID2,
}},
Namespace: "banana",
}
ExampleOperatorToken13 = &api.ACLToken{
SecretID: ExampleOperatorTokenID13,
AccessorID: "2a81ec0b-692e-845e-f5b8-c33c05e5af22",
Description: "Operator Token 3",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID3,
}},
Namespace: "banana",
}
ExampleOperatorToken14 = &api.ACLToken{
SecretID: ExampleOperatorTokenID14,
AccessorID: "4273f1cc-5626-7a77-dc65-1f24af035ed5d",
Description: "Operator Token 4",
Policies: nil, // no direct policy, only roles
Roles: []*api.ACLTokenRoleLink{{
ID: ExampleRoleID1,
Name: "example-role-1",
}},
Namespace: "banana",
}
ExampleOperatorToken15 = &api.ACLToken{
SecretID: ExampleOperatorTokenID15,
AccessorID: "5b78e186-87d8-c1ad-966f-f5fa87b05c9a",
Description: "Operator Token 5",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID4,
}},
Namespace: "banana",
}
// In Consul namespace "default"
ExampleOperatorToken20 = &api.ACLToken{
SecretID: ExampleOperatorTokenID20,
AccessorID: "228865c6-3bf6-6683-df03-06dea2779088",
Description: "Operator Token 0",
// Should still be able to register jobs where no namespace was set
Namespace: "default",
}
ExampleOperatorToken21 = &api.ACLToken{
SecretID: ExampleOperatorTokenID21,
AccessorID: "54d01af9-5036-31d3-296b-b15b941d7aa2",
Description: "Operator Token 1",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID1,
}},
// Should still be able to register jobs where no namespace was set
Namespace: "default",
}
ExampleOperatorToken22 = &api.ACLToken{
SecretID: ExampleOperatorTokenID22,
AccessorID: "894f2c5c-b285-71bf-4acb-6344cecf71f3",
Description: "Operator Token 2",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID2,
}},
Namespace: "default",
}
ExampleOperatorToken23 = &api.ACLToken{
SecretID: ExampleOperatorTokenID23,
AccessorID: "2a81ec0b-692e-845e-f5b8-c33c05e5af22",
Description: "Operator Token 3",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID3,
}},
Namespace: "default",
}
ExampleOperatorToken24 = &api.ACLToken{
SecretID: ExampleOperatorTokenID24,
AccessorID: "4273f1cc-5626-7a77-dc65-1f24af035ed5d",
Description: "Operator Token 4",
Policies: nil, // no direct policy, only roles
Roles: []*api.ACLTokenRoleLink{{
ID: ExampleRoleID1,
Name: "example-role-1",
}},
Namespace: "default",
}
ExampleOperatorToken25 = &api.ACLToken{
SecretID: ExampleOperatorTokenID25,
AccessorID: "5b78e186-87d8-c1ad-966f-f5fa87b05c9a",
Description: "Operator Token 5",
Policies: []*api.ACLTokenPolicyLink{{
ID: ExamplePolicyID4,
}},
Namespace: "default",
}
)
func (m *MockACLsAPI) TokenReadSelf(q *api.QueryOptions) (*api.ACLToken, *api.QueryMeta, error) {
switch q.Token {
case ExampleOperatorTokenID1:
return ExampleOperatorToken1, nil, nil
case ExampleOperatorTokenID2:
return ExampleOperatorToken2, nil, nil
case ExampleOperatorTokenID3:
return ExampleOperatorToken3, nil, nil
case ExampleOperatorTokenID4:
return ExampleOperatorToken4, nil, nil
case ExampleOperatorTokenID5:
return ExampleOperatorToken5, nil, nil
case ExampleOperatorTokenID10:
return ExampleOperatorToken10, nil, nil
case ExampleOperatorTokenID11:
return ExampleOperatorToken11, nil, nil
case ExampleOperatorTokenID12:
return ExampleOperatorToken12, nil, nil
case ExampleOperatorTokenID13:
return ExampleOperatorToken13, nil, nil
case ExampleOperatorTokenID14:
return ExampleOperatorToken14, nil, nil
case ExampleOperatorTokenID15:
return ExampleOperatorToken15, nil, nil
case ExampleOperatorTokenID20:
return ExampleOperatorToken20, nil, nil
case ExampleOperatorTokenID21:
return ExampleOperatorToken21, nil, nil
case ExampleOperatorTokenID22:
return ExampleOperatorToken22, nil, nil
case ExampleOperatorTokenID23:
return ExampleOperatorToken23, nil, nil
case ExampleOperatorTokenID24:
return ExampleOperatorToken24, nil, nil
case ExampleOperatorTokenID25:
return ExampleOperatorToken25, nil, nil
default:
return nil, nil, errors.New("no such token")
}
}
// SetError is a helper method for configuring an error that will be returned
// on future calls to mocked methods.
func (m *MockACLsAPI) SetError(err error) {
m.lock.Lock()
defer m.lock.Unlock()
m.state.error = err
}
// TokenCreate is a mock of ACLsAPI.TokenCreate
func (m *MockACLsAPI) TokenCreate(token *api.ACLToken, opts *api.WriteOptions) (*api.ACLToken, *api.WriteMeta, error) {
index, created, meta, err := m.tokenCreate(token, opts)
services := func(token *api.ACLToken) []string {
if token == nil {
return nil
}
var names []string
for _, id := range token.ServiceIdentities {
names = append(names, id.ServiceName)
}
return names
}(created)
description := func(token *api.ACLToken) string {
if token == nil {
return "<nil>"
}
return token.Description
}(created)
accessor := func(token *api.ACLToken) string {
if token == nil {
return "<nil>"
}
return token.AccessorID
}(created)
secret := func(token *api.ACLToken) string {
if token == nil {
return "<nil>"
}
return token.SecretID
}(created)
m.logger.Trace("TokenCreate()", "description", description, "service_identities", services, "accessor", accessor, "secret", secret, "index", index, "error", err)
return created, meta, err
}
func (m *MockACLsAPI) tokenCreate(token *api.ACLToken, _ *api.WriteOptions) (uint64, *api.ACLToken, *api.WriteMeta, error) {
m.lock.Lock()
defer m.lock.Unlock()
m.state.index++
if m.state.error != nil {
return m.state.index, nil, nil, m.state.error
}
secret := &api.ACLToken{
CreateIndex: m.state.index,
ModifyIndex: m.state.index,
AccessorID: uuid.Generate(),
SecretID: uuid.Generate(),
Description: token.Description,
ServiceIdentities: token.ServiceIdentities,
Namespace: token.Namespace,
CreateTime: time.Now(),
}
m.state.tokens[secret.AccessorID] = secret
w := &api.WriteMeta{
RequestTime: 1 * time.Millisecond,
}
return m.state.index, secret, w, nil
}
// TokenDelete is a mock of ACLsAPI.TokenDelete
func (m *MockACLsAPI) TokenDelete(accessorID string, opts *api.WriteOptions) (*api.WriteMeta, error) {
meta, err := m.tokenDelete(accessorID, opts)
m.logger.Trace("TokenDelete()", "accessor", accessorID, "error", err)
return meta, err
}
func (m *MockACLsAPI) tokenDelete(tokenID string, _ *api.WriteOptions) (*api.WriteMeta, error) {
m.lock.Lock()
defer m.lock.Unlock()
m.state.index++
if m.state.error != nil {
return nil, m.state.error
}
if _, exists := m.state.tokens[tokenID]; !exists {
return nil, nil // consul no-ops delete of non-existent token
}
delete(m.state.tokens, tokenID)
m.logger.Trace("TokenDelete()")
return nil, nil
}
// TokenList is a mock of ACLsAPI.TokenList
func (m *MockACLsAPI) TokenList(_ *api.QueryOptions) ([]*api.ACLTokenListEntry, *api.QueryMeta, error) {
m.lock.Lock()
defer m.lock.Unlock()
//todo(shoenig): will need this for background token reconciliation
// coming in another issue
return nil, nil, nil
}