-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
consul: correctly check consul acl token namespace when using consul oss #10720
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
//+build !ent | ||
|
||
package nomad | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"testing" | ||
|
||
"github.com/hashicorp/nomad/command/agent/consul" | ||
"github.com/hashicorp/nomad/helper/testlog" | ||
"github.com/hashicorp/nomad/nomad/structs" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestConsulACLsAPI_CheckPermissions_oss(t *testing.T) { | ||
|
||
// In Nomad OSS, CheckPermissions will only receive "" as input for the | ||
// namespace parameter - as the ConsulUsage map from namespace to usages will | ||
// always contain one key - the empty string. | ||
|
||
t.Parallel() | ||
|
||
try := func(t *testing.T, namespace string, usage *structs.ConsulUsage, secretID string, exp error) { | ||
logger := testlog.HCLogger(t) | ||
aclAPI := consul.NewMockACLsAPI(logger) | ||
cAPI := NewConsulACLsAPI(aclAPI, logger, nil) | ||
|
||
err := cAPI.CheckPermissions(context.Background(), namespace, usage, secretID) | ||
if exp == nil { | ||
require.NoError(t, err) | ||
} else { | ||
require.Equal(t, exp.Error(), err.Error()) | ||
} | ||
} | ||
|
||
t.Run("check-permissions kv read", func(t *testing.T) { | ||
t.Run("uses kv has permission", func(t *testing.T) { | ||
u := &structs.ConsulUsage{KV: true} | ||
try(t, "", u, consul.ExampleOperatorTokenID5, nil) | ||
}) | ||
|
||
t.Run("uses kv without permission", func(t *testing.T) { | ||
u := &structs.ConsulUsage{KV: true} | ||
try(t, "", u, consul.ExampleOperatorTokenID1, errors.New("insufficient Consul ACL permissions to use template")) | ||
}) | ||
|
||
t.Run("uses kv no token", func(t *testing.T) { | ||
u := &structs.ConsulUsage{KV: true} | ||
try(t, "", u, "", errors.New("missing consul token")) | ||
}) | ||
|
||
t.Run("uses kv nonsense token", func(t *testing.T) { | ||
u := &structs.ConsulUsage{KV: true} | ||
try(t, "", u, "47d33e22-720a-7fe6-7d7f-418bf844a0be", errors.New("unable to read consul token: no such token")) | ||
}) | ||
|
||
t.Run("no kv no token", func(t *testing.T) { | ||
u := &structs.ConsulUsage{KV: false} | ||
try(t, "", u, "", nil) | ||
}) | ||
}) | ||
|
||
t.Run("check-permissions service write", func(t *testing.T) { | ||
usage := &structs.ConsulUsage{Services: []string{"service1"}} | ||
|
||
t.Run("operator has service write", func(t *testing.T) { | ||
try(t, "", usage, consul.ExampleOperatorTokenID1, nil) | ||
}) | ||
|
||
t.Run("operator has service_prefix write", func(t *testing.T) { | ||
u := &structs.ConsulUsage{Services: []string{"foo-service1"}} | ||
try(t, "", u, consul.ExampleOperatorTokenID2, nil) | ||
}) | ||
|
||
t.Run("operator has service_prefix write wrong prefix", func(t *testing.T) { | ||
u := &structs.ConsulUsage{Services: []string{"bar-service1"}} | ||
try(t, "", u, consul.ExampleOperatorTokenID2, errors.New(`insufficient Consul ACL permissions to write service "bar-service1"`)) | ||
}) | ||
|
||
t.Run("operator permissions insufficient", func(t *testing.T) { | ||
try(t, "", usage, consul.ExampleOperatorTokenID3, errors.New(`insufficient Consul ACL permissions to write service "service1"`)) | ||
}) | ||
|
||
t.Run("operator provided no token", func(t *testing.T) { | ||
try(t, "", usage, "", errors.New("missing consul token")) | ||
}) | ||
|
||
t.Run("operator provided nonsense token", func(t *testing.T) { | ||
try(t, "", usage, "f1682bde-1e71-90b1-9204-85d35467ba61", errors.New("unable to read consul token: no such token")) | ||
}) | ||
}) | ||
|
||
t.Run("check-permissions connect service identity write", func(t *testing.T) { | ||
usage := &structs.ConsulUsage{Kinds: []structs.TaskKind{structs.NewTaskKind(structs.ConnectProxyPrefix, "service1")}} | ||
|
||
t.Run("operator has service write", func(t *testing.T) { | ||
try(t, "", usage, consul.ExampleOperatorTokenID1, nil) | ||
}) | ||
|
||
t.Run("operator has service_prefix write", func(t *testing.T) { | ||
u := &structs.ConsulUsage{Kinds: []structs.TaskKind{structs.NewTaskKind(structs.ConnectProxyPrefix, "foo-service1")}} | ||
try(t, "", u, consul.ExampleOperatorTokenID2, nil) | ||
}) | ||
|
||
t.Run("operator has service_prefix write wrong prefix", func(t *testing.T) { | ||
u := &structs.ConsulUsage{Kinds: []structs.TaskKind{structs.NewTaskKind(structs.ConnectProxyPrefix, "bar-service1")}} | ||
try(t, "", u, consul.ExampleOperatorTokenID2, errors.New(`insufficient Consul ACL permissions to write Connect service "bar-service1"`)) | ||
}) | ||
|
||
t.Run("operator permissions insufficient", func(t *testing.T) { | ||
try(t, "", usage, consul.ExampleOperatorTokenID3, errors.New(`insufficient Consul ACL permissions to write Connect service "service1"`)) | ||
}) | ||
|
||
t.Run("operator provided no token", func(t *testing.T) { | ||
try(t, "", usage, "", errors.New("missing consul token")) | ||
}) | ||
|
||
t.Run("operator provided nonsense token", func(t *testing.T) { | ||
try(t, "", usage, "f1682bde-1e71-90b1-9204-85d35467ba61", errors.New("unable to read consul token: no such token")) | ||
}) | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,17 +67,42 @@ func (c *consulACLsAPI) isManagementToken(token *api.ACLToken) bool { | |
return false | ||
} | ||
|
||
// namespaceCheck is used to verify the namespace of the object matches the | ||
// namespace of the ACL token provided. | ||
// namespaceCheck is used to fail the request if the namespace of the object does | ||
// not match the namespace of the ACL token provided. | ||
// | ||
// exception: iff token is in the default namespace, it may contain policies | ||
// *exception*: if token is in the default namespace, it may contain policies | ||
// that extend into other namespaces using namespace_prefix, which must bypass | ||
// this early check and validate in the service/keystore helpers | ||
// | ||
// *exception*: if token is not in a namespace, consul namespaces are not enabled | ||
// and there is nothing to validate | ||
// | ||
// If the namespaces match, whether the token is allowed to perform an operation | ||
// is checked later. | ||
func namespaceCheck(namespace string, token *api.ACLToken) error { | ||
if token.Namespace != "default" && token.Namespace != namespace { | ||
|
||
switch { | ||
case namespace == token.Namespace: | ||
// ACLs enabled, namespaces are the same | ||
return nil | ||
|
||
case token.Namespace == "default": | ||
// ACLs enabled, must defer to per-object checking, since the token could | ||
// have namespace or namespace_prefix blocks with extended policies that | ||
// allow an operation. Using namespace or namespace_prefix blocks is only | ||
// applicable to tokens in the "default" namespace. | ||
// | ||
// https://www.consul.io/docs/security/acl/acl-rules#namespace-rules | ||
return nil | ||
|
||
case namespace == "" && token.Namespace != "default": | ||
// ACLs enabled with non-default token, but namespace on job not set, so | ||
// provide a more informative error message. | ||
return errors.Errorf("consul ACL token requires using namespace %q", token.Namespace) | ||
|
||
default: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found it a tad harder to reason through conditions, maybe because of ordering. I would probably find it easier if the conditions were order the following way:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for this suggestion, this is easier to read 🙂 |
||
return errors.Errorf("consul ACL token cannot use namespace %q", namespace) | ||
} | ||
return nil | ||
} | ||
|
||
func (c *consulACLsAPI) canReadKeystore(namespace string, token *api.ACLToken) (bool, error) { | ||
|
@@ -87,7 +112,10 @@ func (c *consulACLsAPI) canReadKeystore(namespace string, token *api.ACLToken) ( | |
} | ||
|
||
// determines whether a top-level ACL policy will be applicable | ||
matches := namespace == token.Namespace | ||
// | ||
// if the namespace is not set in the job and the token is in the default namespace, | ||
// treat that like an exact match to preserve backwards compatibility | ||
matches := (namespace == token.Namespace) || (namespace == "" && token.Namespace == "default") | ||
|
||
// check each policy directly attached to the token | ||
for _, policyRef := range token.Policies { | ||
|
@@ -127,7 +155,10 @@ func (c *consulACLsAPI) canWriteService(namespace, service string, token *api.AC | |
} | ||
|
||
// determines whether a top-level ACL policy will be applicable | ||
matches := namespace == token.Namespace | ||
// | ||
// if the namespace is not set in the job and the token is in the default namespace, | ||
// treat that like an exact match to preserve backwards compatibility | ||
matches := (namespace == token.Namespace) || (namespace == "" && token.Namespace == "default") | ||
|
||
// check each policy directly attached to the token | ||
for _, policyRef := range token.Policies { | ||
|
@@ -179,6 +210,7 @@ func (c *consulACLsAPI) policyAllowsServiceWrite(matches bool, namespace, servic | |
if cp.allowsServiceWrite(matches, namespace, service) { | ||
return true, nil | ||
} | ||
|
||
return false, nil | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how I personally feel about this style of nested
t.Run
: When the assertions are simply delegation totry
, it feels a simple table tests might read better and is more concise.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, for tests like these it's hard to find the right balance between readability of the code and readability of the description of the test. By composing
t.Run
we get a free way of composing the description, making it obvious what the difference is between one particular test from the tests surrounding it. OTOH it's a lot of lines of code that aren't actually doing anything. I'll take a look at refactoring these tests some more after the bugfix release.