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

Support ACL upsert behavior #909

Merged
merged 3 commits into from
May 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 4 additions & 9 deletions command/agent/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package agent

import (
"fmt"
"github.com/hashicorp/consul/consul/structs"
"net/http"
"strings"

"github.com/hashicorp/consul/consul/structs"
)

// aclCreateResponse is used to wrap the ACL ID
Expand Down Expand Up @@ -80,14 +81,8 @@ func (s *HTTPServer) aclSet(resp http.ResponseWriter, req *http.Request, update
}
}

// Ensure there is no ID set for create
if !update && args.ACL.ID != "" {
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf("ACL ID cannot be set")))
return nil, nil
}

// Ensure there is an ID set for update
// Ensure there is an ID set for update. ID is optional for
// create, as one will be generated if not provided.
if update && args.ACL.ID == "" {
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf("ACL ID must be set")))
Expand Down
31 changes: 30 additions & 1 deletion command/agent/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package agent
import (
"bytes"
"encoding/json"
"github.com/hashicorp/consul/consul/structs"
"net/http"
"net/http/httptest"
"testing"

"github.com/hashicorp/consul/consul/structs"
)

func makeTestACL(t *testing.T, srv *HTTPServer) string {
Expand Down Expand Up @@ -62,6 +63,34 @@ func TestACLUpdate(t *testing.T) {
})
}

func TestACLUpdate_Upsert(t *testing.T) {
httpTest(t, func(srv *HTTPServer) {
body := bytes.NewBuffer(nil)
enc := json.NewEncoder(body)
raw := map[string]interface{}{
"ID": "my-old-id",
"Name": "User Token 2",
"Type": "client",
"Rules": "",
}
enc.Encode(raw)

req, err := http.NewRequest("PUT", "/v1/acl/update?token=root", body)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
obj, err := srv.ACLUpdate(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
aclResp := obj.(aclCreateResponse)
if aclResp.ID != "my-old-id" {
t.Fatalf("bad: %v", aclResp)
}
})
}

func TestACLDestroy(t *testing.T) {
httpTest(t, func(srv *HTTPServer) {
id := makeTestACL(t, srv)
Expand Down
16 changes: 3 additions & 13 deletions consul/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,12 @@ func (a *ACL) Apply(args *structs.ACLRequest, reply *string) error {
return fmt.Errorf("ACL rule compilation failed: %v", err)
}

// Check if this is an update
state := a.srv.fsm.State()
var existing *structs.ACL
if args.ACL.ID != "" {
_, existing, err = state.ACLGet(args.ACL.ID)
if err != nil {
a.srv.logger.Printf("[ERR] consul.acl: ACL lookup failed: %v", err)
return err
}
}

// If this is a create, generate a new ID. This must
// If no ID is provided, generate a new ID. This must
// be done prior to appending to the raft log, because the ID is not
// deterministic. Once the entry is in the log, the state update MUST
// be deterministic or the followers will not converge.
if existing == nil {
if args.ACL.ID == "" {
state := a.srv.fsm.State()
for {
args.ACL.ID = generateUUID()
_, acl, err := state.ACLGet(args.ACL.ID)
Expand Down
47 changes: 47 additions & 0 deletions consul/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,53 @@ func TestACLEndpoint_Update_PurgeCache(t *testing.T) {
}
}

func TestACLEndpoint_Apply_CustomID(t *testing.T) {
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLMasterToken = "root"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
client := rpcClient(t, s1)
defer client.Close()

testutil.WaitForLeader(t, client.Call, "dc1")

arg := structs.ACLRequest{
Datacenter: "dc1",
Op: structs.ACLSet,
ACL: structs.ACL{
ID: "foobarbaz", // Specify custom ID, does not exist
Name: "User token",
Type: structs.ACLTypeClient,
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out string
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
if out != "foobarbaz" {
t.Fatalf("bad token ID: %s", out)
}

// Verify
state := s1.fsm.State()
_, s, err := state.ACLGet(out)
if err != nil {
t.Fatalf("err: %v", err)
}
if s == nil {
t.Fatalf("should not be nil")
}
if s.ID != out {
t.Fatalf("bad: %v", s)
}
if s.Name != "User token" {
t.Fatalf("bad: %v", s)
}
}

func TestACLEndpoint_Apply_Denied(t *testing.T) {
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
Expand Down
8 changes: 6 additions & 2 deletions website/source/docs/agent/http/acl.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ None of the fields are mandatory. In fact, no body needs to be PUT if the
defaults are to be used. The `Name` and `Rules` fields default to being
blank, and the `Type` defaults to "client".

The `ID` field may be provided, and if omitted a random UUID will be generated.
The security of the ACL system depends on the difficulty of guessing the token.
Tokens should not be generated in a predictable manner or with too little entropy.

The format of the `Rules` property is [documented here](/docs/internals/acl.html).

A successful response body will return the `ID` of the newly created ACL, like so:
Expand All @@ -69,8 +73,8 @@ A successful response body will return the `ID` of the newly created ACL, like s
The update endpoint is used to modify the policy for a given ACL token. It
is very similar to the create endpoint; however, instead of generating a new
token ID, the `ID` field must be provided. As with [`/v1/acl/create`](#acl_create),
requests to this endpoint must be made with a management
token.
requests to this endpoint must be made with a management token. If the ID does not
exist, the ACL will be upserted. In this sense, create and update are identical.

In any Consul cluster, only a single datacenter is authoritative for ACLs, so
all requests are automatically routed to that datacenter regardless
Expand Down