Merge pull request #3205 from hashicorp/f-deployment-acl
Deployment.GetDeployment ACL enforcement
This commit is contained in:
commit
fa2dd57071
57
nomad/acl_testutil_test.go
Normal file
57
nomad/acl_testutil_test.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package nomad
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/nomad/nomad/mock"
|
||||||
|
"github.com/hashicorp/nomad/nomad/state"
|
||||||
|
"github.com/hashicorp/nomad/nomad/structs"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NamespacePolicy is a helper for generating the policy hcl for a given
|
||||||
|
// namepsace. Either policy or capabilites may be nil but not both.
|
||||||
|
func NamespacePolicy(namespace string, policy string, capabilities []string) string {
|
||||||
|
policyHCL := fmt.Sprintf("namespace %q {", namespace)
|
||||||
|
if policy != "" {
|
||||||
|
policyHCL += fmt.Sprintf("\n\tpolicy = %q", policy)
|
||||||
|
}
|
||||||
|
if len(capabilities) != 0 {
|
||||||
|
policyHCL += fmt.Sprintf("\n\tcapabilities = %q", capabilities)
|
||||||
|
}
|
||||||
|
policyHCL += "\n}"
|
||||||
|
return policyHCL
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePolicy creates a policy with the given name and rule.
|
||||||
|
func CreatePolicy(t *testing.T, state *state.StateStore, index uint64, name, rule string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Create the ACLPolicy
|
||||||
|
policy := &structs.ACLPolicy{
|
||||||
|
Name: name,
|
||||||
|
Rules: rule,
|
||||||
|
}
|
||||||
|
policy.SetHash()
|
||||||
|
assert.Nil(t, state.UpsertACLPolicies(index, []*structs.ACLPolicy{policy}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateToken creates a local, client token for the given policies
|
||||||
|
func CreateToken(t *testing.T, state *state.StateStore, index uint64, policies []string) *structs.ACLToken {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Create the ACLToken
|
||||||
|
token := mock.ACLToken()
|
||||||
|
token.Policies = policies
|
||||||
|
token.SetHash()
|
||||||
|
assert.Nil(t, state.UpsertACLTokens(index, []*structs.ACLToken{token}))
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePolicyAndToken creates a policy and then returns a token configured for
|
||||||
|
// just that policy. CreatePolicyAndToken uses the given index and index+1.
|
||||||
|
func CreatePolicyAndToken(t *testing.T, state *state.StateStore, index uint64, name, rule string) *structs.ACLToken {
|
||||||
|
CreatePolicy(t, state, index, name, rule)
|
||||||
|
return CreateToken(t, state, index+1, []string{name})
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
metrics "github.com/armon/go-metrics"
|
metrics "github.com/armon/go-metrics"
|
||||||
memdb "github.com/hashicorp/go-memdb"
|
memdb "github.com/hashicorp/go-memdb"
|
||||||
|
"github.com/hashicorp/nomad/acl"
|
||||||
"github.com/hashicorp/nomad/nomad/state"
|
"github.com/hashicorp/nomad/nomad/state"
|
||||||
"github.com/hashicorp/nomad/nomad/structs"
|
"github.com/hashicorp/nomad/nomad/structs"
|
||||||
)
|
)
|
||||||
|
@ -23,6 +24,13 @@ func (d *Deployment) GetDeployment(args *structs.DeploymentSpecificRequest,
|
||||||
}
|
}
|
||||||
defer metrics.MeasureSince([]string{"nomad", "deployment", "get_deployment"}, time.Now())
|
defer metrics.MeasureSince([]string{"nomad", "deployment", "get_deployment"}, time.Now())
|
||||||
|
|
||||||
|
// Check namespace read-job permissions
|
||||||
|
if aclObj, err := d.srv.resolveToken(args.SecretID); err != nil {
|
||||||
|
return err
|
||||||
|
} else if aclObj != nil && !aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityReadJob) {
|
||||||
|
return structs.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
// Setup the blocking query
|
// Setup the blocking query
|
||||||
opts := blockingOptions{
|
opts := blockingOptions{
|
||||||
queryOpts: &args.QueryOptions,
|
queryOpts: &args.QueryOptions,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
memdb "github.com/hashicorp/go-memdb"
|
memdb "github.com/hashicorp/go-memdb"
|
||||||
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
||||||
|
"github.com/hashicorp/nomad/acl"
|
||||||
"github.com/hashicorp/nomad/helper"
|
"github.com/hashicorp/nomad/helper"
|
||||||
"github.com/hashicorp/nomad/nomad/mock"
|
"github.com/hashicorp/nomad/nomad/mock"
|
||||||
"github.com/hashicorp/nomad/nomad/structs"
|
"github.com/hashicorp/nomad/nomad/structs"
|
||||||
|
@ -44,6 +45,59 @@ func TestDeploymentEndpoint_GetDeployment(t *testing.T) {
|
||||||
assert.Equal(d, resp.Deployment, "Returned deployment not equal")
|
assert.Equal(d, resp.Deployment, "Returned deployment not equal")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeploymentEndpoint_GetDeployment_ACL(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
s1, root := testACLServer(t, nil)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
codec := rpcClient(t, s1)
|
||||||
|
testutil.WaitForLeader(t, s1.RPC)
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Create the deployment
|
||||||
|
j := mock.Job()
|
||||||
|
d := mock.Deployment()
|
||||||
|
d.JobID = j.ID
|
||||||
|
state := s1.fsm.State()
|
||||||
|
|
||||||
|
assert.Nil(state.UpsertJob(999, j), "UpsertJob")
|
||||||
|
assert.Nil(state.UpsertDeployment(1000, d), "UpsertDeployment")
|
||||||
|
|
||||||
|
// Create the namespace policy and tokens
|
||||||
|
validToken := CreatePolicyAndToken(t, state, 1001, "test-valid",
|
||||||
|
NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}))
|
||||||
|
invalidToken := CreatePolicyAndToken(t, state, 1003, "test-invalid",
|
||||||
|
NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs}))
|
||||||
|
|
||||||
|
// Lookup the deployments without a token and expect failure
|
||||||
|
get := &structs.DeploymentSpecificRequest{
|
||||||
|
DeploymentID: d.ID,
|
||||||
|
QueryOptions: structs.QueryOptions{
|
||||||
|
Region: "global",
|
||||||
|
Namespace: structs.DefaultNamespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var resp structs.SingleDeploymentResponse
|
||||||
|
assert.NotNil(msgpackrpc.CallWithCodec(codec, "Deployment.GetDeployment", get, &resp), "RPC")
|
||||||
|
|
||||||
|
// Try with a good token
|
||||||
|
get.SecretID = validToken.SecretID
|
||||||
|
assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.GetDeployment", get, &resp), "RPC")
|
||||||
|
assert.EqualValues(resp.Index, 1000, "resp.Index")
|
||||||
|
assert.Equal(d, resp.Deployment, "Returned deployment not equal")
|
||||||
|
|
||||||
|
// Try with a bad token
|
||||||
|
get.SecretID = invalidToken.SecretID
|
||||||
|
err := msgpackrpc.CallWithCodec(codec, "Deployment.GetDeployment", get, &resp)
|
||||||
|
assert.NotNil(err, "RPC")
|
||||||
|
assert.Equal(err.Error(), structs.ErrPermissionDenied.Error())
|
||||||
|
|
||||||
|
// Try with a root token
|
||||||
|
get.SecretID = root.SecretID
|
||||||
|
assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.GetDeployment", get, &resp), "RPC")
|
||||||
|
assert.EqualValues(resp.Index, 1000, "resp.Index")
|
||||||
|
assert.Equal(d, resp.Deployment, "Returned deployment not equal")
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeploymentEndpoint_GetDeployment_Blocking(t *testing.T) {
|
func TestDeploymentEndpoint_GetDeployment_Blocking(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
s1 := testServer(t, nil)
|
s1 := testServer(t, nil)
|
||||||
|
|
|
@ -83,9 +83,9 @@ The table below shows this endpoint's support for
|
||||||
[blocking queries](/api/index.html#blocking-queries) and
|
[blocking queries](/api/index.html#blocking-queries) and
|
||||||
[required ACLs](/api/index.html#acls).
|
[required ACLs](/api/index.html#acls).
|
||||||
|
|
||||||
| Blocking Queries | ACL Required |
|
| Blocking Queries | ACL Required |
|
||||||
| ---------------- | ------------ |
|
| ---------------- | -------------------- |
|
||||||
| `YES` | `none` |
|
| `YES` | `namespace:read-job` |
|
||||||
|
|
||||||
### Parameters
|
### Parameters
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue