87be8c4c4b
This PR fixes the Nomad Object Namespace <-> Consul ACL Token relationship check when using Consul OSS (or Consul ENT without namespace support). Nomad v1.1.0 introduced a regression where Nomad would fail the validation when submitting Connect jobs and allow_unauthenticated set to true, with Consul OSS - because it would do the namespace check against the Consul ACL token assuming the "default" namespace, which does not work because Consul OSS does not have namespaces. Instead of making the bad assumption, expand the namespace check to handle each special case explicitly. Fixes #10718
459 lines
12 KiB
Go
459 lines
12 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"
|
|
)
|
|
|
|
// 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"
|
|
)
|
|
|
|
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: "",
|
|
}
|
|
|
|
// 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",
|
|
}
|
|
)
|
|
|
|
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
|
|
|
|
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
|
|
}
|