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

Adds a new /v1/acl/bootstrap API #3349

Merged
merged 11 commits into from
Aug 3, 2017
27 changes: 27 additions & 0 deletions agent/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,33 @@ func ACLDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, erro
return nil, nil
}

// ACLBootstrap is used to perform a one-time ACL bootstrap operation on
// a cluster to get the first management token.
func (s *HTTPServer) ACLBootstrap(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if req.Method != "PUT" {
resp.WriteHeader(http.StatusMethodNotAllowed)
return nil, nil
}

args := structs.DCSpecificRequest{
Datacenter: s.agent.config.ACLDatacenter,
}

var out structs.ACL
err := s.agent.RPC("ACL.Bootstrap", &args, &out)
if err != nil {
if strings.Contains(err.Error(), structs.ACLBootstrapNotAllowedErr.Error()) {
resp.WriteHeader(http.StatusForbidden)
fmt.Fprintf(resp, "Permission denied: %v", err)
return nil, nil
} else {
return nil, err
}
}

return aclCreateResponse{out.ID}, nil
}

func (s *HTTPServer) ACLDestroy(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Mandate a PUT request
if req.Method != "PUT" {
Expand Down
46 changes: 46 additions & 0 deletions agent/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,52 @@ func makeTestACL(t *testing.T, srv *HTTPServer) string {
return aclResp.ID
}

func TestACL_Bootstrap(t *testing.T) {
t.Parallel()
cfg := TestACLConfig()
cfg.Version = "0.9.1"
cfg.ACLMasterToken = ""
a := NewTestAgent(t.Name(), cfg)
defer a.Shutdown()

tests := []struct {
name string
method string
code int
token bool
}{
{"bad method", "GET", http.StatusMethodNotAllowed, false},
{"bootstrap", "PUT", http.StatusOK, true},
{"not again", "PUT", http.StatusForbidden, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp := httptest.NewRecorder()
req, _ := http.NewRequest(tt.method, "/v1/acl/bootstrap", nil)
out, err := a.srv.ACLBootstrap(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
if got, want := resp.Code, tt.code; got != want {
t.Fatalf("got %d want %d", got, want)
}
if tt.token {
wrap, ok := out.(aclCreateResponse)
if !ok {
t.Fatalf("bad: %T", out)
}
if len(wrap.ID) != len("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") {
t.Fatalf("bad: %v", wrap)
}
} else {
if out != nil {
t.Fatalf("bad: %T", out)
}
}
})
}
}

func TestACL_Update(t *testing.T) {
t.Parallel()
a := NewTestAgent(t.Name(), TestACLConfig())
Expand Down
6 changes: 0 additions & 6 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -660,12 +660,6 @@ func (a *Agent) consulConfig() (*consul.Config, error) {
if a.config.RaftProtocol != 0 {
base.RaftConfig.ProtocolVersion = raft.ProtocolVersion(a.config.RaftProtocol)
}
if a.config.ACLToken != "" {
base.ACLToken = a.config.ACLToken
}
if a.config.ACLAgentToken != "" {
base.ACLAgentToken = a.config.ACLAgentToken
}
if a.config.ACLMasterToken != "" {
base.ACLMasterToken = a.config.ACLMasterToken
}
Expand Down
2 changes: 1 addition & 1 deletion agent/agent_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,6 @@ func (s *HTTPServer) AgentToken(resp http.ResponseWriter, req *http.Request) (in
return nil, nil
}

s.agent.logger.Printf("[INFO] Updated agent's %q", target)
s.agent.logger.Printf("[INFO] Updated agent's ACL token %q", target)
return nil, nil
}
63 changes: 63 additions & 0 deletions agent/consul/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,69 @@ type ACL struct {
srv *Server
}

// Bootstrap is used to perform a one-time ACL bootstrap operation on
// a cluster to get the first management token.
func (a *ACL) Bootstrap(args *structs.DCSpecificRequest, reply *structs.ACL) error {
if done, err := a.srv.forward("ACL.Bootstrap", args, args, reply); done {
return err
}

// Verify we are allowed to serve this request
if a.srv.config.ACLDatacenter != a.srv.config.Datacenter {
return fmt.Errorf(aclDisabled)
}

// By doing some pre-checks we can head off later bootstrap attempts
// without having to run them through Raft, which should curb abuse.
state := a.srv.fsm.State()
bs, err := state.ACLGetBootstrap()
if err != nil {
return err
}
if bs == nil {
return structs.ACLBootstrapNotInitializedErr
}
if !bs.AllowBootstrap {
return structs.ACLBootstrapNotAllowedErr
}

// Propose a new token.
token, err := uuid.GenerateUUID()
if err != nil {
return fmt.Errorf("failed to make random token: %v", err)
}

// Attempt a bootstrap.
req := structs.ACLRequest{
Datacenter: a.srv.config.ACLDatacenter,
Op: structs.ACLBootstrapNow,
ACL: structs.ACL{
ID: token,
Name: "Bootstrap Token",
Type: structs.ACLTypeManagement,
},
}
resp, err := a.srv.raftApply(structs.ACLRequestType, &req)
if err != nil {
return err
}
switch v := resp.(type) {
case error:
return v

case *structs.ACL:
*reply = *v

default:
// Just log this, since it looks like the bootstrap may have
// completed.
a.srv.logger.Printf("[ERR] consul.acl: Unexpected response during bootstrap: %T", v)
}

a.srv.logger.Printf("[INFO] consul.acl: ACL bootstrap completed")
return nil
}

// aclApplyInternal is used to apply an ACL request after it has been vetted that
// this is a valid operation. It is used when users are updating ACLs, in which
// case we check their token to make sure they have management privileges. It is
Expand Down
53 changes: 53 additions & 0 deletions agent/consul/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,59 @@ import (
"github.com/hashicorp/net-rpc-msgpackrpc"
)

func TestACLEndpoint_Bootstrap(t *testing.T) {
t.Parallel()
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.Build = "0.8.0" // Too low for auto init of bootstrap.
c.ACLDatacenter = "dc1"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()

testrpc.WaitForLeader(t, s1.RPC, "dc1")

// Expect an error initially since ACL bootstrap is not initialized.
arg := structs.DCSpecificRequest{
Datacenter: "dc1",
}
var out structs.ACL
err := msgpackrpc.CallWithCodec(codec, "ACL.Bootstrap", &arg, &out)
if err.Error() != structs.ACLBootstrapNotInitializedErr.Error() {
t.Fatalf("err: %v", err)
}

// Manually do an init.
req := structs.ACLRequest{
Datacenter: "dc1",
Op: structs.ACLBootstrapInit,
}
_, err = s1.raftApply(structs.ACLRequestType, &req)
if err != nil {
t.Fatalf("err: %v", err)
}

// Try again, this time it should go through. We can only do some high
// level checks on the ACL since we don't have control over the UUID or
// Raft indexes at this level.
if err := msgpackrpc.CallWithCodec(codec, "ACL.Bootstrap", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
if len(out.ID) != len("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") ||
out.Name != "Bootstrap Token" ||
out.Type != structs.ACLTypeManagement ||
out.CreateIndex == 0 || out.ModifyIndex == 0 {
t.Fatalf("bad: %#v", out)
}

// Finally, make sure that another attempt is rejected.
err = msgpackrpc.CallWithCodec(codec, "ACL.Bootstrap", &arg, &out)
if err.Error() != structs.ACLBootstrapNotAllowedErr.Error() {
t.Fatalf("err: %v", err)
}
}

func TestACLEndpoint_Apply(t *testing.T) {
t.Parallel()
dir1, s1 := testServerWithConfig(t, func(c *Config) {
Expand Down
10 changes: 0 additions & 10 deletions agent/consul/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,16 +191,6 @@ type Config struct {
// operators track which versions are actively deployed
Build string

// ACLToken is the default token to use when making a request.
// If not provided, the anonymous token is used. This enables
// backwards compatibility as well.
ACLToken string

// ACLAgentToken is the default token used to make requests for the agent
// itself, such as for registering itself with the catalog. If not
// configured, the ACLToken will be used.
ACLAgentToken string

// ACLMasterToken is used to bootstrap the ACL system. It should be specified
// on the servers in the ACLDatacenter. When the leader comes online, it ensures
// that the Master token is available. This provides the initial token.
Expand Down
39 changes: 39 additions & 0 deletions agent/consul/fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import (
"github.com/hashicorp/raft"
)

// TODO (slackpad) - There are two refactors we should do here:
//
// 1. Register the different types from the state store and make the FSM more
// generic, especially around snapshot/restore. Those should really just
// pass the encoder into a WriteSnapshot() kind of method.
// 2. Check all the error return values from all the Write() calls.

// msgpackHandle is a shared handle for encoding/decoding msgpack payloads
var msgpackHandle = &codec.MsgpackHandle{}

Expand Down Expand Up @@ -231,6 +238,17 @@ func (c *consulFSM) applyACLOperation(buf []byte, index uint64) interface{} {
}
defer metrics.MeasureSince([]string{"consul", "fsm", "acl", string(req.Op)}, time.Now())
switch req.Op {
case structs.ACLBootstrapInit:
enabled, err := c.state.ACLBootstrapInit(index)
if err != nil {
return err
}
return enabled
case structs.ACLBootstrapNow:
if err := c.state.ACLBootstrap(index, &req.ACL); err != nil {
return err
}
return &req.ACL
case structs.ACLForceSet, structs.ACLSet:
if err := c.state.ACLSet(index, &req.ACL); err != nil {
return err
Expand Down Expand Up @@ -423,6 +441,15 @@ func (c *consulFSM) Restore(old io.ReadCloser) error {
return err
}

case structs.ACLBootstrapRequestType:
var req structs.ACLBootstrap
if err := dec.Decode(&req); err != nil {
return err
}
if err := restore.ACLBootstrap(&req); err != nil {
return err
}

case structs.CoordinateBatchUpdateType:
var req structs.Coordinates
if err := dec.Decode(&req); err != nil {
Expand Down Expand Up @@ -623,6 +650,18 @@ func (s *consulSnapshot) persistACLs(sink raft.SnapshotSink,
return err
}
}

bs, err := s.state.ACLBootstrap()
if err != nil {
return err
}
if bs != nil {
sink.Write([]byte{byte(structs.ACLBootstrapRequestType)})
if err := encoder.Encode(bs); err != nil {
return err
}
}

return nil
}

Expand Down
Loading