From c9d2c62d0b402a75ef0f526d0d95fc1262d29b54 Mon Sep 17 00:00:00 2001 From: Michael Schurter Date: Mon, 2 Oct 2017 14:31:58 -0700 Subject: [PATCH] Deployment.Promote ACL enforcement --- nomad/deployment_endpoint.go | 7 ++ nomad/deployment_endpoint_test.go | 90 ++++++++++++++++++++++++++ website/source/api/deployments.html.md | 6 +- 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/nomad/deployment_endpoint.go b/nomad/deployment_endpoint.go index 6e52b1f91..855c09627 100644 --- a/nomad/deployment_endpoint.go +++ b/nomad/deployment_endpoint.go @@ -162,6 +162,13 @@ func (d *Deployment) Promote(args *structs.DeploymentPromoteRequest, reply *stru } defer metrics.MeasureSince([]string{"nomad", "deployment", "promote"}, time.Now()) + // Check namespace submit-job permissions + if aclObj, err := d.srv.resolveToken(args.SecretID); err != nil { + return err + } else if aclObj != nil && !aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilitySubmitJob) { + return structs.ErrPermissionDenied + } + // Validate the arguments if args.DeploymentID == "" { return fmt.Errorf("missing deployment ID") diff --git a/nomad/deployment_endpoint_test.go b/nomad/deployment_endpoint_test.go index 855f53b0a..d83ce2cdf 100644 --- a/nomad/deployment_endpoint_test.go +++ b/nomad/deployment_endpoint_test.go @@ -520,6 +520,96 @@ func TestDeploymentEndpoint_Promote(t *testing.T) { assert.True(dout.TaskGroups["web"].Promoted, "web group should be promoted") } +func TestDeploymentEndpoint_Promote_ACL(t *testing.T) { + t.Parallel() + s1, _ := testACLServer(t, func(c *Config) { + c.NumSchedulers = 0 // Prevent automatic dequeue + }) + defer s1.Shutdown() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + assert := assert.New(t) + + // Create the deployment, job and canary + j := mock.Job() + j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() + j.TaskGroups[0].Update.MaxParallel = 2 + j.TaskGroups[0].Update.Canary = 2 + d := mock.Deployment() + d.TaskGroups["web"].DesiredCanaries = 2 + d.JobID = j.ID + a := mock.Alloc() + d.TaskGroups[a.TaskGroup].PlacedCanaries = []string{a.ID} + a.DeploymentID = d.ID + a.DeploymentStatus = &structs.AllocDeploymentStatus{ + Healthy: helper.BoolToPtr(true), + } + + state := s1.fsm.State() + assert.Nil(state.UpsertJob(999, j), "UpsertJob") + assert.Nil(state.UpsertDeployment(1000, d), "UpsertDeployment") + assert.Nil(state.UpsertAllocs(1001, []*structs.Allocation{a}), "UpsertAllocs") + + // Create the namespace policy and tokens + validToken := CreatePolicyAndToken(t, state, 1001, "test-valid", + NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilitySubmitJob})) + invalidToken := CreatePolicyAndToken(t, state, 1003, "test-invalid", + NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob})) + + // Promote the deployment + req := &structs.DeploymentPromoteRequest{ + DeploymentID: d.ID, + All: true, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + + // Try with no token and expect permission denied + { + var resp structs.DeploymentUpdateResponse + err := msgpackrpc.CallWithCodec(codec, "Deployment.Promote", req, &resp) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Try with an invalid token + { + req.SecretID = invalidToken.SecretID + var resp structs.DeploymentUpdateResponse + err := msgpackrpc.CallWithCodec(codec, "Deployment.Promote", req, &resp) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Fetch the response with a valid token + { + req.SecretID = validToken.SecretID + var resp structs.DeploymentUpdateResponse + assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.Promote", req, &resp), "RPC") + assert.NotEqual(resp.Index, uint64(0), "bad response index") + + // Lookup the evaluation + ws := memdb.NewWatchSet() + eval, err := state.EvalByID(ws, resp.EvalID) + assert.Nil(err, "EvalByID failed") + assert.NotNil(eval, "Expect eval") + assert.Equal(eval.CreateIndex, resp.EvalCreateIndex, "eval index mismatch") + assert.Equal(eval.TriggeredBy, structs.EvalTriggerDeploymentWatcher, "eval trigger") + assert.Equal(eval.JobID, d.JobID, "eval job id") + assert.Equal(eval.DeploymentID, d.ID, "eval deployment id") + assert.Equal(eval.Status, structs.EvalStatusPending, "eval status") + + // Lookup the deployment + dout, err := state.DeploymentByID(ws, d.ID) + assert.Nil(err, "DeploymentByID failed") + assert.Equal(dout.Status, structs.DeploymentStatusRunning, "wrong status") + assert.Equal(dout.StatusDescription, structs.DeploymentStatusDescriptionRunning, "wrong status description") + assert.Equal(dout.ModifyIndex, resp.DeploymentModifyIndex, "wrong modify index") + assert.Len(dout.TaskGroups, 1, "should have one group") + assert.Contains(dout.TaskGroups, "web", "should have web group") + assert.True(dout.TaskGroups["web"].Promoted, "web group should be promoted") + } +} + func TestDeploymentEndpoint_SetAllocHealth(t *testing.T) { t.Parallel() s1 := testServer(t, func(c *Config) { diff --git a/website/source/api/deployments.html.md b/website/source/api/deployments.html.md index 0b3238a60..de1ef84a6 100644 --- a/website/source/api/deployments.html.md +++ b/website/source/api/deployments.html.md @@ -369,9 +369,9 @@ The table below shows this endpoint's support for [blocking queries](/api/index.html#blocking-queries) and [required ACLs](/api/index.html#acls). -| Blocking Queries | ACL Required | -| ---------------- | ------------ | -| `NO` | `none` | +| Blocking Queries | ACL Required | +| ---------------- | ---------------------- | +| `NO` | `namespace:submit-job` | ### Parameters