Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Operator Generated bootstrap token #12520

Merged
merged 71 commits into from
Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
e9f4394
Working MVP - Admin Provided Bootstrap Token
lhaig Apr 8, 2022
cd62726
Update acl.go
lhaig Apr 8, 2022
0722f72
Update acl_bootstrap.go
lhaig Apr 8, 2022
cc0b3a4
Update acl_bootstrap.go
lhaig Apr 10, 2022
eb0699a
Update the flag name and some comments
lhaig Apr 11, 2022
67ca953
Updated the header name in the agent
lhaig Apr 11, 2022
e4a6924
Update Header Name Typo
lhaig Apr 11, 2022
8b046de
Validate ProvidedToken is a UUID
lhaig May 13, 2022
c435389
Update acl_endpoint.go
lhaig May 13, 2022
7143fa2
Refactor the ACL Bootstrap token code
lhaig May 13, 2022
2198fdf
Add Additional field to allow current test to pass
lhaig May 13, 2022
57ea2fe
Working MVP - Admin Provided Bootstrap Token
lhaig Apr 8, 2022
e85c104
Update the flag name and some comments
lhaig Apr 11, 2022
7a35037
Updated the header name in the agent
lhaig Apr 11, 2022
e4242ae
Update Header Name Typo
lhaig Apr 11, 2022
111181d
Validate ProvidedToken is a UUID
lhaig May 13, 2022
ec6115d
Refactor the ACL Bootstrap token code
lhaig May 13, 2022
908544b
Update acl_bootstrap_test.go
lhaig May 15, 2022
8587603
WIP: ACL Test for Operator token.
lhaig May 15, 2022
865892b
Working MVP - Admin Provided Bootstrap Token
lhaig Apr 8, 2022
7f5737b
Update acl_bootstrap.go
lhaig Apr 8, 2022
4af3111
Update the flag name and some comments
lhaig Apr 11, 2022
7fec7f8
Updated the header name in the agent
lhaig Apr 11, 2022
17cabf2
Update Header Name Typo
lhaig Apr 11, 2022
bade1e9
Refactor the ACL Bootstrap token code
lhaig May 13, 2022
cb2ae1f
Working MVP - Admin Provided Bootstrap Token
lhaig Apr 8, 2022
d25cee1
Updated the header name in the agent
lhaig Apr 11, 2022
3c7a0dc
Update Header Name Typo
lhaig Apr 11, 2022
24117d3
Refactor the ACL Bootstrap token code
lhaig May 13, 2022
46b515e
Update acl_test.go
lhaig May 16, 2022
363eabb
Update bootstrap.mdx
lhaig May 16, 2022
59c425b
Update api/acl_test.go
lhaig May 17, 2022
8bdbb52
Update command/acl_bootstrap.go
lhaig May 17, 2022
358a8a8
Update nomad/acl_endpoint.go
lhaig May 17, 2022
408940f
Update api/acl.go
lhaig May 17, 2022
7af15b4
Update command/acl_bootstrap_test.go
lhaig May 17, 2022
15f03e9
Update command/acl_bootstrap_test.go
lhaig May 17, 2022
e46f431
Update acl_bootstrap_test.go
lhaig May 17, 2022
b3fbae9
Separate out the new Bootstrap code into a separate method
lhaig May 17, 2022
867984f
Create 12520.txt
lhaig May 17, 2022
160a81d
Update acl-tokens.mdx
lhaig May 17, 2022
d48a705
Update website/content/api-docs/acl-tokens.mdx
lhaig May 17, 2022
1b40dec
Add the UUID requirement to the documentation
lhaig May 17, 2022
585ee2b
Update acl_bootstrap_test.go
lhaig May 17, 2022
e294174
Change Struct Field Name to be more descriptive
lhaig May 18, 2022
8f360a5
Remove the need for a header
lhaig May 22, 2022
9c798fa
Remove .vscode/launch.json
lhaig May 24, 2022
f790c36
Update api/acl.go
lhaig May 24, 2022
fc207de
Moved as per request
lhaig May 24, 2022
d739fbb
Update acl_bootstrap.go
lhaig May 25, 2022
2d16aaa
Update .gitignore
lhaig May 26, 2022
e93fc35
Update acl_bootstrap.go
lhaig May 26, 2022
b7aa487
Update acl_bootstrap_test.go
lhaig May 26, 2022
7bacf6f
Update acl_bootstrap_test.go
lhaig May 26, 2022
317ec5e
Update acl_endpoint_test.go
lhaig May 26, 2022
89b34ec
Update acl-tokens.mdx
lhaig May 26, 2022
ae2691c
Update 12520.txt
lhaig May 26, 2022
10289f1
Update bootstrap.mdx
lhaig May 26, 2022
e99c9c0
Update hashistack.tf
lhaig May 26, 2022
2de8f49
Update acl_endpoint_test.go
lhaig May 26, 2022
b138bbd
Update acl_endpoint.go
lhaig May 26, 2022
633cf82
Update acl_endpoint.go
lhaig May 26, 2022
c973e2d
Update bootstrap.mdx
lhaig May 26, 2022
8a0d393
Update api/acl.go
lhaig May 28, 2022
731936c
Update acl_bootstrap.go
lhaig May 28, 2022
5bccf03
Merge branch 'f-bootstrap-token' of github.com:hashicorp/nomad into f…
lhaig May 28, 2022
16ec53b
Update acl-tokens.mdx
lhaig May 28, 2022
2d7b315
Update api/acl.go
lhaig May 28, 2022
d716f3f
Revert Back to the ContentLength check
lhaig Jun 2, 2022
9ca6c63
test: manually set contentlength in tests
schmichael Jun 2, 2022
772d8ee
appease hclfmt
schmichael Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/12520.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
bootstrap: Added option to allow for an operator generated bootstrap token to be passed to the `acl bootstrap` command
```
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ GNUMakefile.local

rkt-*

# Common editor config
./idea
*.iml
.vscode

# UI rules

Expand Down
23 changes: 23 additions & 0 deletions api/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func (c *Client) ACLTokens() *ACLTokens {
return &ACLTokens{client: c}
}

// DEPRECATED: will be removed in Nomad 1.5.0
// Bootstrap is used to get the initial bootstrap token
func (a *ACLTokens) Bootstrap(q *WriteOptions) (*ACLToken, *WriteMeta, error) {
var resp ACLToken
Expand All @@ -82,6 +83,23 @@ func (a *ACLTokens) Bootstrap(q *WriteOptions) (*ACLToken, *WriteMeta, error) {
return &resp, wm, nil
}

// BootstrapOpts is used to get the initial bootstrap token or pass in the one that was provided in the API
func (a *ACLTokens) BootstrapOpts(btoken string, q *WriteOptions) (*ACLToken, *WriteMeta, error) {
if q == nil {
q = &WriteOptions{}
}
req := &BootstrapRequest{
BootstrapSecret: btoken,
}

var resp ACLToken
wm, err := a.client.write("/v1/acl/bootstrap", req, &resp, q)
if err != nil {
return nil, nil, err
}
return &resp, wm, nil
}

// List is used to dump all of the tokens.
func (a *ACLTokens) List(q *QueryOptions) ([]*ACLTokenListStub, *QueryMeta, error) {
var resp []*ACLTokenListStub
Expand Down Expand Up @@ -244,3 +262,8 @@ type OneTimeTokenExchangeRequest struct {
type OneTimeTokenExchangeResponse struct {
Token *ACLToken
}

// BootstrapRequest is used for when operators provide an ACL Bootstrap Token
type BootstrapRequest struct {
BootstrapSecret string
}
30 changes: 30 additions & 0 deletions api/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,33 @@ func TestACL_OneTimeToken(t *testing.T) {
assert.NotNil(t, out3)
assert.Equal(t, out3.AccessorID, out.AccessorID)
}

func TestACLTokens_BootstrapInvalidToken(t *testing.T) {
testutil.Parallel(t)
c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) {
c.ACL.Enabled = true
})
defer s.Stop()
at := c.ACLTokens()

bootkn := "badtoken"
// Bootstrap with invalid token
_, _, err := at.BootstrapOpts(bootkn, nil)
assert.EqualError(t, err, "Unexpected response code: 400 (invalid acl token)")
}

func TestACLTokens_BootstrapValidToken(t *testing.T) {
testutil.Parallel(t)
c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) {
c.ACL.Enabled = true
})
defer s.Stop()
at := c.ACLTokens()

bootkn := "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a"
// Bootstrap with Valid token
out, wm, err := at.BootstrapOpts(bootkn, nil)
assert.NoError(t, err)
assertWriteMeta(t, wm)
assert.Equal(t, bootkn, out.SecretID)
}
31 changes: 28 additions & 3 deletions command/acl_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package command

import (
"fmt"
"io/ioutil"
"os"
"strings"

"github.com/hashicorp/nomad/api"
Expand Down Expand Up @@ -57,6 +59,7 @@ func (c *ACLBootstrapCommand) Run(args []string) int {
var (
json bool
tmpl string
file string
)

flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
Expand All @@ -69,12 +72,34 @@ func (c *ACLBootstrapCommand) Run(args []string) int {

// Check that we got no arguments
args = flags.Args()
if l := len(args); l != 0 {
c.Ui.Error("This command takes no arguments")
if l := len(args); l < 0 || l > 1 {
c.Ui.Error("This command takes up to one argument")
c.Ui.Error(commandErrorText(c))
return 1
}

var terminalToken []byte
var err error

if len(args) == 1 {
switch args[0] {
case "":
terminalToken = []byte{}
case "-":
terminalToken, err = ioutil.ReadAll(os.Stdin)
default:
file = args[0]
terminalToken, err = ioutil.ReadFile(file)
}
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading provided token: %v", err))
return 1
}
}

// Remove newline from the token if it was passed by stdin
boottoken := strings.TrimSuffix(string(terminalToken), "\n")

// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
Expand All @@ -83,7 +108,7 @@ func (c *ACLBootstrapCommand) Run(args []string) int {
}

// Get the bootstrap token
token, _, err := client.ACLTokens().Bootstrap(nil)
token, _, err := client.ACLTokens().BootstrapOpts(boottoken, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error bootstrapping: %s", err))
return 1
Expand Down
79 changes: 79 additions & 0 deletions command/acl_bootstrap_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package command

import (
"io/ioutil"
"os"
"testing"

"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestACLBootstrapCommand(t *testing.T) {
Expand Down Expand Up @@ -76,3 +80,78 @@ func TestACLBootstrapCommand_NonACLServer(t *testing.T) {
out := ui.OutputWriter.String()
assert.NotContains(out, "Secret ID")
}

// Attempting to bootstrap the server with an operator provided token in a file should
// return the same token in the result.
func TestACLBootstrapCommand_WithOperatorFileBootstrapToken(t *testing.T) {
ci.Parallel(t)
// create a acl-enabled server without bootstrapping the token
config := func(c *agent.Config) {
c.ACL.Enabled = true
c.ACL.PolicyTTL = 0
}

// create a valid token
mockToken := mock.ACLToken()

// Create temp file
f, err := ioutil.TempFile("", "nomad-token.token")
assert.Nil(t, err)
defer os.Remove(f.Name())

// Write the token to the file
err = ioutil.WriteFile(f.Name(), []byte(mockToken.SecretID), 0700)
assert.Nil(t, err)

srv, _, url := testServer(t, true, config)
defer srv.Shutdown()

require.Nil(t, srv.RootToken)

ui := cli.NewMockUi()
cmd := &ACLBootstrapCommand{Meta: Meta{Ui: ui, flagAddress: url}}

code := cmd.Run([]string{"-address=" + url, f.Name()})
assert.Equal(t, 0, code)

out := ui.OutputWriter.String()
assert.Contains(t, out, mockToken.SecretID)
}

// Attempting to bootstrap the server with an invalid operator provided token in a file should
// fail.
func TestACLBootstrapCommand_WithBadOperatorFileBootstrapToken(t *testing.T) {
ci.Parallel(t)

// create a acl-enabled server without bootstrapping the token
config := func(c *agent.Config) {
c.ACL.Enabled = true
c.ACL.PolicyTTL = 0
}

// create a invalid token
invalidToken := "invalid-token"

// Create temp file
f, err := ioutil.TempFile("", "nomad-token.token")
assert.Nil(t, err)
defer os.Remove(f.Name())

// Write the token to the file
err = ioutil.WriteFile(f.Name(), []byte(invalidToken), 0700)
assert.Nil(t, err)

srv, _, url := testServer(t, true, config)
defer srv.Shutdown()

assert.Nil(t, srv.RootToken)

ui := cli.NewMockUi()
cmd := &ACLBootstrapCommand{Meta: Meta{Ui: ui, flagAddress: url}}

code := cmd.Run([]string{"-address=" + url, f.Name()})
assert.Equal(t, 1, code)

out := ui.OutputWriter.String()
assert.NotContains(t, out, invalidToken)
}
10 changes: 8 additions & 2 deletions command/agent/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,14 @@ func (s *HTTPServer) ACLTokenBootstrap(resp http.ResponseWriter, req *http.Reque
return nil, CodedError(405, ErrInvalidMethod)
}

// Format the request
args := structs.ACLTokenBootstrapRequest{}
var args structs.ACLTokenBootstrapRequest

if req.ContentLength != 0 {
if err := decodeBody(req, &args); err != nil {
return nil, CodedError(400, err.Error())
}
}

s.parseWriteRequest(req, &args.WriteRequest)

var out structs.ACLTokenUpsertResponse
Expand Down
43 changes: 43 additions & 0 deletions command/agent/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,49 @@ func TestHTTP_ACLTokenBootstrap(t *testing.T) {
})
}

func TestHTTP_ACLTokenBootstrapOperator(t *testing.T) {
ci.Parallel(t)
conf := func(c *Config) {
c.ACL.Enabled = true
c.ACL.PolicyTTL = 0 // Special flag to disable auto-bootstrap
}
httpTest(t, conf, func(s *TestAgent) {
// Provide token
args := structs.ACLTokenBootstrapRequest{
BootstrapSecret: "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a",
}

buf := encodeReq(args)

// Make the HTTP request
req, err := http.NewRequest("PUT", "/v1/acl/bootstrap", buf)
if err != nil {
t.Fatalf("err: %v", err)
}

// Since we're not actually writing this HTTP request, we have
// to manually set ContentLength
req.ContentLength = -1
Comment on lines +244 to +246
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow this took me a while to figure out: since we're never actually writing this HTTP request, the stdlib code that sets ContentLength is never set!

Therefore in tests we need to set ContentLength to some nonzero value since tests do not use a real HTTP server.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am glad it is finally solved it kept me up a few nights :-)


respW := httptest.NewRecorder()
// Make the request
obj, err := s.Server.ACLTokenBootstrap(respW, req)
if err != nil {
t.Fatalf("err: %v", err)
}

// Check for the index
if respW.Result().Header.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}

// Check the output
n := obj.(*structs.ACLToken)
assert.NotNil(t, n)
assert.Equal(t, args.BootstrapSecret, n.SecretID)
})
}

func TestHTTP_ACLTokenList(t *testing.T) {
ci.Parallel(t)
httpACLTest(t, nil, func(s *TestAgent) {
Expand Down
12 changes: 12 additions & 0 deletions nomad/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
log "github.com/hashicorp/go-hclog"
memdb "github.com/hashicorp/go-memdb"
policy "github.com/hashicorp/nomad/acl"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/state"
"github.com/hashicorp/nomad/nomad/state/paginator"
Expand Down Expand Up @@ -353,6 +354,7 @@ func (a *ACL) Bootstrap(args *structs.ACLTokenBootstrapRequest, reply *structs.A
return aclDisabled
}
args.Region = a.srv.config.AuthoritativeRegion
providedTokenID := args.BootstrapSecret

if done, err := a.srv.forward("ACL.Bootstrap", args, args, reply); done {
return err
Expand Down Expand Up @@ -396,6 +398,16 @@ func (a *ACL) Bootstrap(args *structs.ACLTokenBootstrapRequest, reply *structs.A
Global: true,
CreateTime: time.Now().UTC(),
}

// if a token has been passed in from the API overwrite the generated one.
if providedTokenID != "" {
if helper.IsUUID(providedTokenID) {
args.Token.SecretID = providedTokenID
} else {
return structs.NewErrRPCCodedf(400, "invalid acl token")
}
}

args.Token.SetHash()

// Update via Raft
Expand Down
40 changes: 40 additions & 0 deletions nomad/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,46 @@ func TestACLEndpoint_Bootstrap(t *testing.T) {
assert.Equal(t, created, out)
}

func TestACLEndpoint_BootstrapOperator(t *testing.T) {
ci.Parallel(t)
s1, cleanupS1 := TestServer(t, func(c *Config) {
c.ACLEnabled = true
})
defer cleanupS1()
codec := rpcClient(t, s1)
testutil.WaitForLeader(t, s1.RPC)

// Lookup the tokens
req := &structs.ACLTokenBootstrapRequest{
WriteRequest: structs.WriteRequest{Region: "global"},
BootstrapSecret: "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a",
}
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)
// Check we have the correct operator token
tokenout, err := s1.fsm.State().ACLTokenBySecretID(nil, created.SecretID)
assert.Nil(t, err)
assert.Equal(t, created, tokenout)
}

func TestACLEndpoint_Bootstrap_Reset(t *testing.T) {
ci.Parallel(t)
dir := t.TempDir()
Expand Down
Loading