From 3a4eb87693524653de56bb5b575797c3acf22b96 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Wed, 27 Sep 2017 16:30:13 +0000 Subject: [PATCH 1/4] add acl for dispatch job --- nomad/job_endpoint.go | 7 +++ nomad/job_endpoint_test.go | 79 +++++++++++++++++++++++++++++++++ website/source/api/jobs.html.md | 6 +-- 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index a788f57aa..c4bbd9812 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -1173,6 +1173,13 @@ func (j *Job) Dispatch(args *structs.JobDispatchRequest, reply *structs.JobDispa } defer metrics.MeasureSince([]string{"nomad", "job", "dispatch"}, time.Now()) + // Check for submit-job permissions + if aclObj, err := j.srv.resolveToken(args.SecretID); err != nil { + return err + } else if aclObj != nil && !aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilitySubmitJob) { + return structs.ErrPermissionDenied + } + // Lookup the parameterized job if args.JobID == "" { return fmt.Errorf("missing parameterized job ID") diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go index 5cac4b670..7061a6c76 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -3219,6 +3219,85 @@ func TestJobEndpoint_ValidateJobUpdate_ACL(t *testing.T) { assert.Equal("", validResp.Warnings) } +func TestJobEndpoint_Dispatch_ACL(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + s1, root := testACLServer(t, func(c *Config) { + c.NumSchedulers = 0 // Prevent automatic dequeue + }) + + defer s1.Shutdown() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + state := s1.fsm.State() + + // Create a parameterized job + job := mock.Job() + job.Type = structs.JobTypeBatch + job.ParameterizedJob = &structs.ParameterizedJobConfig{} + err := state.UpsertJob(400, job) + assert.Nil(err) + + req := &structs.JobDispatchRequest{ + JobID: job.ID, + WriteRequest: structs.WriteRequest{ + Region: "global", + Namespace: job.Namespace, + }, + } + + // Attempt to fetch the response without a token should fail + var resp structs.JobDispatchResponse + err = msgpackrpc.CallWithCodec(codec, "Job.Dispatch", req, &resp) + assert.NotNil(err) + assert.Contains(err.Error(), "Permission denied") + + // Attempt to fetch the response with an invalid token should fail + invalidToken := CreatePolicyAndToken(t, state, 1001, "test-invalid", + NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs})) + req.SecretID = invalidToken.SecretID + + var invalidResp structs.JobDispatchResponse + err = msgpackrpc.CallWithCodec(codec, "Job.Dispatch", req, &invalidResp) + assert.NotNil(err) + assert.Contains(err.Error(), "Permission denied") + + // Dispatch with a valid management token should succeed + req.SecretID = root.SecretID + + var validResp structs.JobDispatchResponse + err = msgpackrpc.CallWithCodec(codec, "Job.Dispatch", req, &validResp) + assert.Nil(err) + assert.NotNil(validResp.EvalID) + assert.NotNil(validResp.DispatchedJobID) + assert.NotEqual(validResp.DispatchedJobID, "") + + // Dispatch with a valid token should succeed + validToken := CreatePolicyAndToken(t, state, 1003, "test-valid", + NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilitySubmitJob})) + req.SecretID = validToken.SecretID + + var validResp2 structs.JobDispatchResponse + err = msgpackrpc.CallWithCodec(codec, "Job.Dispatch", req, &validResp2) + assert.Nil(err) + assert.NotNil(validResp2.EvalID) + assert.NotNil(validResp2.DispatchedJobID) + assert.NotEqual(validResp2.DispatchedJobID, "") + + ws := memdb.NewWatchSet() + out, err := state.JobByID(ws, job.Namespace, validResp2.DispatchedJobID) + assert.Nil(err) + assert.NotNil(out) + assert.Equal(out.ParentID, job.ID) + + // Look up the evaluation + eval, err := state.EvalByID(ws, validResp2.EvalID) + assert.Nil(err) + assert.NotNil(eval) + assert.Equal(eval.CreateIndex, validResp2.EvalCreateIndex) +} + func TestJobEndpoint_Dispatch(t *testing.T) { t.Parallel() diff --git a/website/source/api/jobs.html.md b/website/source/api/jobs.html.md index 89261fa97..755d415f5 100644 --- a/website/source/api/jobs.html.md +++ b/website/source/api/jobs.html.md @@ -1103,9 +1103,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 From 5a738d28af9ee9684e85b47b9be4fbd6222c2014 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 28 Sep 2017 14:27:51 +0000 Subject: [PATCH 2/4] job dispatch should have dispatch policy --- acl/policy.go | 5 ++++- nomad/job_endpoint.go | 2 +- nomad/job_endpoint_test.go | 2 +- website/source/api/jobs.html.md | 6 +++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/acl/policy.go b/acl/policy.go index 757fe2fde..d1a13e55e 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -25,6 +25,7 @@ const ( NamespaceCapabilityListJobs = "list-jobs" NamespaceCapabilityReadJob = "read-job" NamespaceCapabilitySubmitJob = "submit-job" + NamespaceCapabilityDispatchJob = "dispatch-job" NamespaceCapabilityReadLogs = "read-logs" NamespaceCapabilityReadFS = "read-fs" NamespaceCapabilitySentinelOverride = "sentinel-override" @@ -76,7 +77,8 @@ func isPolicyValid(policy string) bool { func isNamespaceCapabilityValid(cap string) bool { switch cap { case NamespaceCapabilityDeny, NamespaceCapabilityListJobs, NamespaceCapabilityReadJob, - NamespaceCapabilitySubmitJob, NamespaceCapabilityReadLogs, NamespaceCapabilityReadFS: + NamespaceCapabilitySubmitJob, NamespaceCapabilityDispatchJob, NamespaceCapabilityReadLogs, + NamespaceCapabilityReadFS: return true // Seperate the enterprise-only capabilities case NamespaceCapabilitySentinelOverride: @@ -102,6 +104,7 @@ func expandNamespacePolicy(policy string) []string { NamespaceCapabilityListJobs, NamespaceCapabilityReadJob, NamespaceCapabilitySubmitJob, + NamespaceCapabilityDispatchJob, NamespaceCapabilityReadLogs, NamespaceCapabilityReadFS, } diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index c4bbd9812..7ebbf2eba 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -1176,7 +1176,7 @@ func (j *Job) Dispatch(args *structs.JobDispatchRequest, reply *structs.JobDispa // Check for submit-job permissions if aclObj, err := j.srv.resolveToken(args.SecretID); err != nil { return err - } else if aclObj != nil && !aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilitySubmitJob) { + } else if aclObj != nil && !aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityDispatchJob) { return structs.ErrPermissionDenied } diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go index 7061a6c76..df1427ec2 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -3275,7 +3275,7 @@ func TestJobEndpoint_Dispatch_ACL(t *testing.T) { // Dispatch with a valid token should succeed validToken := CreatePolicyAndToken(t, state, 1003, "test-valid", - NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilitySubmitJob})) + NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityDispatchJob})) req.SecretID = validToken.SecretID var validResp2 structs.JobDispatchResponse diff --git a/website/source/api/jobs.html.md b/website/source/api/jobs.html.md index 755d415f5..16d6ed272 100644 --- a/website/source/api/jobs.html.md +++ b/website/source/api/jobs.html.md @@ -1103,9 +1103,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` | `namespace:submit-job` | +| Blocking Queries | ACL Required | +| ---------------- | ------------------------------ | +| `NO` | `namespace:dispatch-job` | ### Parameters From bc11fcc0125f2d29693e5d88660b666efb23d020 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Fri, 29 Sep 2017 21:22:36 +0000 Subject: [PATCH 3/4] fix up policy test --- acl/policy_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/acl/policy_test.go b/acl/policy_test.go index 9c553cee7..4ee291d33 100644 --- a/acl/policy_test.go +++ b/acl/policy_test.go @@ -74,6 +74,7 @@ func TestParse(t *testing.T) { NamespaceCapabilityListJobs, NamespaceCapabilityReadJob, NamespaceCapabilitySubmitJob, + NamespaceCapabilityDispatchJob, NamespaceCapabilityReadLogs, NamespaceCapabilityReadFS, }, From 9424ee54ef1389a45472085df4eae7a9751c47ce Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Tue, 3 Oct 2017 13:14:55 -0400 Subject: [PATCH 4/4] add documentation for dispatch-job --- website/source/guides/acl.html.markdown | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/source/guides/acl.html.markdown b/website/source/guides/acl.html.markdown index 7f5f94597..7688cb61a 100644 --- a/website/source/guides/acl.html.markdown +++ b/website/source/guides/acl.html.markdown @@ -223,6 +223,7 @@ Namespace rules are keyed by the namespace name they apply to. When no namespace * `list-jobs` - Allows listing the jobs and seeing coarse grain status. * `read-job` - Allows inspecting a job and seeing fine grain status. * `submit-job` - Allows jobs to be submitted or modified. +* `dispatch-job` - Allows jobs to be dispatched * `read-logs` - Allows the logs associated with a job to be viewed. * `read-fs` - Allows the filesystem of allocations associated to be viewed. * `sentinel-override` - Allows soft mandatory policies to be overriden. @@ -231,7 +232,7 @@ The coarse grained policy dispositions are shorthand for the fine grained capabi * `deny` policy - ["deny"] * `read` policy - ["list-jobs", "read-job"] -* `write` policy - ["list-jobs", "read-job", "submit-job", "read-logs", "read-fs"] +* `write` policy - ["list-jobs", "read-job", "submit-job", "read-logs", "read-fs", "dispatch-job"] When both the policy short hand and a capabilities list are provided, the capabilities are merged: