diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index b37ea308d..281d1f516 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/consul/lib" "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-multierror" + "github.com/hashicorp/nomad/acl" "github.com/hashicorp/nomad/client/driver" "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/nomad/state" @@ -71,6 +72,13 @@ func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegis // Set the warning message reply.Warnings = structs.MergeMultierrorWarnings(warnings, canonicalizeWarnings) + // Check job submission permissions + if aclObj, err := j.srv.resolveToken(args.SecretID); err != nil { + return err + } else if aclObj != nil && !aclObj.AllowNamespaceOperation(structs.DefaultNamespace, acl.NamespaceCapabilitySubmitJob) { + return structs.ErrPermissionDenied + } + // Lookup the job snap, err := j.srv.fsm.State().Snapshot() if err != nil { diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go index d92d8629e..add802001 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -93,6 +93,49 @@ func TestJobEndpoint_Register(t *testing.T) { } } +func TestJobEndpoint_Register_ACL(t *testing.T) { + t.Parallel() + 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) + + // Create the register request + job := mock.Job() + req := &structs.JobRegisterRequest{ + Job: job, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + + // Try without a token, expect failure + var resp structs.JobRegisterResponse + if err := msgpackrpc.CallWithCodec(codec, "Job.Register", req, &resp); err == nil { + t.Fatalf("expected error") + } + + // Try with a token + req.SecretID = root.SecretID + if err := msgpackrpc.CallWithCodec(codec, "Job.Register", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + if resp.Index == 0 { + t.Fatalf("bad index: %d", resp.Index) + } + + // Check for the node in the FSM + state := s1.fsm.State() + ws := memdb.NewWatchSet() + out, err := state.JobByID(ws, job.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if out == nil { + t.Fatalf("expected job") + } +} + func TestJobEndpoint_Register_InvalidDriverConfig(t *testing.T) { t.Parallel() s1 := testServer(t, func(c *Config) { diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index a88fd1e33..c2dda1fba 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -108,6 +108,9 @@ const ( // ACLClientToken and ACLManagementToken are the only types of tokens ACLClientToken = "client" ACLManagementToken = "management" + + // DefaultNamespace is the default namespace. + DefaultNamespace = "default" ) // Context defines the scope in which a search for Nomad object operates, and