diff --git a/nomad/acl_endpoint.go b/nomad/acl_endpoint.go index 044aeb375..9b1180ab3 100644 --- a/nomad/acl_endpoint.go +++ b/nomad/acl_endpoint.go @@ -192,6 +192,62 @@ func (a *ACL) GetPolicies(args *structs.ACLPolicySetRequest, reply *structs.ACLP return a.srv.blockingRPC(&opts) } +// Bootstrap is used to bootstrap the initial token +func (a *ACL) Bootstrap(args *structs.ACLTokenBootstrapRequest, reply *structs.ACLTokenUpsertResponse) error { + if done, err := a.srv.forward("ACL.Bootstrap", args, args, reply); done { + return err + } + defer metrics.MeasureSince([]string{"nomad", "acl", "bootstrap"}, time.Now()) + + // Snapshot the state + state, err := a.srv.State().Snapshot() + if err != nil { + return err + } + + // Verify bootstrap is possible. The state store method re-verifies this, + // but we do an early check to avoid raft transactions when possible. + ok, err := state.CanBootstrapACLToken() + if err != nil { + return err + } + if !ok { + return fmt.Errorf("ACL bootstrap already done") + } + + // Create a new global management token, override any parameter + args.Token = &structs.ACLToken{ + AccessorID: structs.GenerateUUID(), + SecretID: structs.GenerateUUID(), + Name: "Bootstrap Token", + Type: structs.ACLManagementToken, + Global: true, + CreateTime: time.Now().UTC(), + } + + // Update via Raft + _, index, err := a.srv.raftApply(structs.ACLTokenBootstrapRequestType, args) + if err != nil { + return err + } + + // Populate the response. We do a lookup against the state to + // pickup the proper create / modify times. + state, err = a.srv.State().Snapshot() + if err != nil { + return err + } + out, err := state.ACLTokenByAccessorID(nil, args.Token.AccessorID) + if err != nil { + return fmt.Errorf("token lookup failed: %v", err) + } + reply.Tokens = append(reply.Tokens, out) + + // Update the index + reply.Index = index + return nil +} + // UpsertTokens is used to create or update a set of tokens func (a *ACL) UpsertTokens(args *structs.ACLTokenUpsertRequest, reply *structs.ACLTokenUpsertResponse) error { if done, err := a.srv.forward("ACL.UpsertTokens", args, args, reply); done { diff --git a/nomad/acl_endpoint_test.go b/nomad/acl_endpoint_test.go index 18fe1e5bd..077d95de5 100644 --- a/nomad/acl_endpoint_test.go +++ b/nomad/acl_endpoint_test.go @@ -771,6 +771,39 @@ func TestACLEndpoint_DeleteTokens(t *testing.T) { assert.NotEqual(t, uint64(0), resp.Index) } +func TestACLEndpoint_Bootstrap(t *testing.T) { + t.Parallel() + s1 := testServer(t, nil) + defer s1.Shutdown() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + + // Lookup the tokens + req := &structs.ACLTokenBootstrapRequest{ + WriteRequest: structs.WriteRequest{Region: "global"}, + } + var resp structs.ACLTokenUpsertResponse + if err := msgpackrpc.CallWithCodec(codec, "ACL.Bootstrap", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + assert.NotEqual(t, uint64(0), resp.Index) + assert.NotNil(t, resp.Tokens[0]) + + // Get the token out from the response + created := resp.Tokens[0] + assert.NotEqual(t, "", created.AccessorID) + assert.NotEqual(t, "", created.SecretID) + assert.NotEqual(t, time.Time{}, created.CreateTime) + assert.Equal(t, structs.ACLManagementToken, created.Type) + assert.Equal(t, "Bootstrap Token", created.Name) + assert.Equal(t, true, created.Global) + + // Check we created the token + out, err := s1.fsm.State().ACLTokenByAccessorID(nil, created.AccessorID) + assert.Nil(t, err) + assert.Equal(t, created, out) +} + func TestACLEndpoint_UpsertTokens(t *testing.T) { t.Parallel() s1 := testServer(t, nil) diff --git a/nomad/fsm.go b/nomad/fsm.go index baa93b0fb..dddb90ff1 100644 --- a/nomad/fsm.go +++ b/nomad/fsm.go @@ -177,6 +177,8 @@ func (n *nomadFSM) Apply(log *raft.Log) interface{} { return n.applyACLTokenUpsert(buf[1:], log.Index) case structs.ACLTokenDeleteRequestType: return n.applyACLTokenDelete(buf[1:], log.Index) + case structs.ACLTokenBootstrapRequestType: + return n.applyACLTokenBootstrap(buf[1:], log.Index) default: if ignoreUnknown { n.logger.Printf("[WARN] nomad.fsm: ignoring unknown message type (%d), upgrade to newer version", msgType) @@ -739,6 +741,21 @@ func (n *nomadFSM) applyACLTokenDelete(buf []byte, index uint64) interface{} { return nil } +// applyACLTokenBootstrap is used to bootstrap an ACL token +func (n *nomadFSM) applyACLTokenBootstrap(buf []byte, index uint64) interface{} { + defer metrics.MeasureSince([]string{"nomad", "fsm", "apply_acl_token_bootstrap"}, time.Now()) + var req structs.ACLTokenBootstrapRequest + if err := structs.Decode(buf, &req); err != nil { + panic(fmt.Errorf("failed to decode request: %v", err)) + } + + if err := n.state.BootstrapACLTokens(index, req.Token); err != nil { + n.logger.Printf("[ERR] nomad.fsm: BootstrapACLToken failed: %v", err) + return err + } + return nil +} + func (n *nomadFSM) Snapshot() (raft.FSMSnapshot, error) { // Create a new snapshot snap, err := n.state.Snapshot() diff --git a/nomad/fsm_test.go b/nomad/fsm_test.go index cc79af36e..e872ce2df 100644 --- a/nomad/fsm_test.go +++ b/nomad/fsm_test.go @@ -1571,6 +1571,30 @@ func TestFSM_DeleteACLPolicies(t *testing.T) { assert.Nil(t, out) } +func TestFSM_BootstrapACLTokens(t *testing.T) { + t.Parallel() + fsm := testFSM(t) + + token := mock.ACLToken() + req := structs.ACLTokenBootstrapRequest{ + Token: token, + } + buf, err := structs.Encode(structs.ACLTokenBootstrapRequestType, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := fsm.Apply(makeLog(buf)) + if resp != nil { + t.Fatalf("resp: %v", resp) + } + + // Verify we are registered + out, err := fsm.State().ACLTokenByAccessorID(nil, token.AccessorID) + assert.Nil(t, err) + assert.NotNil(t, out) +} + func TestFSM_UpsertACLTokens(t *testing.T) { t.Parallel() fsm := testFSM(t) diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index dce7c9e93..8442c6745 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -67,6 +67,7 @@ const ( ACLPolicyDeleteRequestType ACLTokenUpsertRequestType ACLTokenDeleteRequestType + ACLTokenBootstrapRequestType ) const ( @@ -5560,6 +5561,12 @@ type ACLTokenDeleteRequest struct { WriteRequest } +// ACLTokenBootstrapRequest is used to bootstrap ACLs +type ACLTokenBootstrapRequest struct { + Token *ACLToken // Not client specifiable + WriteRequest +} + // ACLTokenUpsertRequest is used to upsert a set of tokens type ACLTokenUpsertRequest struct { Tokens []*ACLToken