diff --git a/api/acl.go b/api/acl.go index bac698237..02086b155 100644 --- a/api/acl.go +++ b/api/acl.go @@ -144,6 +144,16 @@ func (a *ACLTokens) Info(accessorID string, q *QueryOptions) (*ACLToken, *QueryM return &resp, wm, nil } +// Self is used to query our own token +func (a *ACLTokens) Self(q *QueryOptions) (*ACLToken, *QueryMeta, error) { + var resp ACLToken + wm, err := a.client.query("/v1/acl/token/self", &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, wm, nil +} + // ACLPolicyListStub is used to for listing ACL policies type ACLPolicyListStub struct { Name string diff --git a/api/acl_test.go b/api/acl_test.go index b987486ef..ddbdc08ab 100644 --- a/api/acl_test.go +++ b/api/acl_test.go @@ -182,6 +182,36 @@ func TestACLTokens_Info(t *testing.T) { assert.Equal(t, out, out2) } +func TestACLTokens_Self(t *testing.T) { + t.Parallel() + c, s, _ := makeACLClient(t, nil, nil) + defer s.Stop() + at := c.ACLTokens() + + token := &ACLToken{ + Name: "foo", + Type: "client", + Policies: []string{"foo1"}, + } + + // Create the token + out, wm, err := at.Create(token, nil) + assert.Nil(t, err) + assertWriteMeta(t, wm) + assert.NotNil(t, out) + + // Set the clients token to the new token + c.SetSecretID(out.SecretID) + at = c.ACLTokens() + + // Query the token + out2, qm, err := at.Self(nil) + if assert.Nil(t, err) { + assertQueryMeta(t, qm) + assert.Equal(t, out, out2) + } +} + func TestACLTokens_Delete(t *testing.T) { t.Parallel() c, s, _ := makeACLClient(t, nil, nil) diff --git a/command/agent/acl_endpoint.go b/command/agent/acl_endpoint.go index 07b57fef2..40b18048f 100644 --- a/command/agent/acl_endpoint.go +++ b/command/agent/acl_endpoint.go @@ -154,29 +154,35 @@ func (s *HTTPServer) ACLTokenBootstrap(resp http.ResponseWriter, req *http.Reque } func (s *HTTPServer) ACLTokenSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - accessor := strings.TrimPrefix(req.URL.Path, "/v1/acl/token") + path := req.URL.Path - // If there is no accessor, this must be a create - if len(accessor) == 0 { + switch path { + case "/v1/acl/token": if !(req.Method == "PUT" || req.Method == "POST") { return nil, CodedError(405, ErrInvalidMethod) } return s.aclTokenUpdate(resp, req, "") + case "/v1/acl/token/self": + return s.aclTokenSelf(resp, req) } - // Check if no accessor is given past the slash - accessor = accessor[1:] - if accessor == "" { + accessor := strings.TrimPrefix(path, "/v1/acl/token/") + return s.aclTokenCrud(resp, req, accessor) +} + +func (s *HTTPServer) aclTokenCrud(resp http.ResponseWriter, req *http.Request, + tokenAccessor string) (interface{}, error) { + if tokenAccessor == "" { return nil, CodedError(400, "Missing Token Accessor") } switch req.Method { case "GET": - return s.aclTokenQuery(resp, req, accessor) + return s.aclTokenQuery(resp, req, tokenAccessor) case "PUT", "POST": - return s.aclTokenUpdate(resp, req, accessor) + return s.aclTokenUpdate(resp, req, tokenAccessor) case "DELETE": - return s.aclTokenDelete(resp, req, accessor) + return s.aclTokenDelete(resp, req, tokenAccessor) default: return nil, CodedError(405, ErrInvalidMethod) } @@ -203,6 +209,29 @@ func (s *HTTPServer) aclTokenQuery(resp http.ResponseWriter, req *http.Request, return out.Token, nil } +func (s *HTTPServer) aclTokenSelf(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, CodedError(405, ErrInvalidMethod) + } + args := structs.ResolveACLTokenRequest{} + if s.parse(resp, req, &args.Region, &args.QueryOptions) { + return nil, nil + } + + args.SecretID = args.AuthToken + + var out structs.ResolveACLTokenResponse + if err := s.agent.RPC("ACL.ResolveToken", &args, &out); err != nil { + return nil, err + } + + setMeta(resp, &out.QueryMeta) + if out.Token == nil { + return nil, CodedError(404, "ACL token not found") + } + return out.Token, nil +} + func (s *HTTPServer) aclTokenUpdate(resp http.ResponseWriter, req *http.Request, tokenAccessor string) (interface{}, error) { // Parse the token diff --git a/command/agent/acl_endpoint_test.go b/command/agent/acl_endpoint_test.go index 4ae3a8429..e574857ae 100644 --- a/command/agent/acl_endpoint_test.go +++ b/command/agent/acl_endpoint_test.go @@ -322,6 +322,55 @@ func TestHTTP_ACLTokenQuery(t *testing.T) { }) } +func TestHTTP_ACLTokenSelf(t *testing.T) { + t.Parallel() + httpACLTest(t, nil, func(s *TestAgent) { + p1 := mock.ACLToken() + p1.AccessorID = "" + args := structs.ACLTokenUpsertRequest{ + Tokens: []*structs.ACLToken{p1}, + WriteRequest: structs.WriteRequest{ + Region: "global", + AuthToken: s.RootToken.SecretID, + }, + } + var resp structs.ACLTokenUpsertResponse + if err := s.Agent.RPC("ACL.UpsertTokens", &args, &resp); err != nil { + t.Fatalf("err: %v", err) + } + out := resp.Tokens[0] + + // Make the HTTP request + req, err := http.NewRequest("GET", "/v1/acl/token/self", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + respW := httptest.NewRecorder() + setToken(req, out) + + // Make the request + obj, err := s.Server.ACLTokenSpecificRequest(respW, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Check for the index + if respW.HeaderMap.Get("X-Nomad-Index") == "" { + t.Fatalf("missing index") + } + if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { + t.Fatalf("missing known leader") + } + if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { + t.Fatalf("missing last contact") + } + + // Check the output + n := obj.(*structs.ACLToken) + assert.Equal(t, out, n) + }) +} + func TestHTTP_ACLTokenCreate(t *testing.T) { t.Parallel() httpACLTest(t, nil, func(s *TestAgent) { diff --git a/dev/acls/default-ns.hcl b/dev/acls/default-ns.hcl new file mode 100644 index 000000000..6af4c50d0 --- /dev/null +++ b/dev/acls/default-ns.hcl @@ -0,0 +1,3 @@ +namespace "default" { + policy = "write" +} diff --git a/dev/acls/node-read.hcl b/dev/acls/node-read.hcl new file mode 100644 index 000000000..94f712b6f --- /dev/null +++ b/dev/acls/node-read.hcl @@ -0,0 +1,3 @@ +node { + policy = "read" +} diff --git a/nomad/acl_endpoint.go b/nomad/acl_endpoint.go index 135e77ad7..e77cd1048 100644 --- a/nomad/acl_endpoint.go +++ b/nomad/acl_endpoint.go @@ -121,12 +121,36 @@ func (a *ACL) ListPolicies(args *structs.ACLPolicyListRequest, reply *structs.AC defer metrics.MeasureSince([]string{"nomad", "acl", "list_policies"}, time.Now()) // Check management level permissions - if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil { + acl, err := a.srv.ResolveToken(args.AuthToken) + if err != nil { return err - } else if acl == nil || !acl.IsManagement() { + } else if acl == nil { return structs.ErrPermissionDenied } + // If it is not a management token determine the policies that may be listed + mgt := acl.IsManagement() + var policies map[string]struct{} + if !mgt { + snap, err := a.srv.fsm.State().Snapshot() + if err != nil { + return err + } + + token, err := snap.ACLTokenBySecretID(nil, args.AuthToken) + if err != nil { + return err + } + if token == nil { + return structs.ErrTokenNotFound + } + + policies = make(map[string]struct{}, len(token.Policies)) + for _, p := range token.Policies { + policies[p] = struct{}{} + } + } + // Setup the blocking query opts := blockingOptions{ queryOpts: &args.QueryOptions, @@ -152,7 +176,9 @@ func (a *ACL) ListPolicies(args *structs.ACLPolicyListRequest, reply *structs.AC break } policy := raw.(*structs.ACLPolicy) - reply.Policies = append(reply.Policies, policy.Stub()) + if _, ok := policies[policy.Name]; ok || mgt { + reply.Policies = append(reply.Policies, policy.Stub()) + } } // Use the last index that affected the policy table @@ -183,12 +209,42 @@ func (a *ACL) GetPolicy(args *structs.ACLPolicySpecificRequest, reply *structs.S defer metrics.MeasureSince([]string{"nomad", "acl", "get_policy"}, time.Now()) // Check management level permissions - if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil { + acl, err := a.srv.ResolveToken(args.AuthToken) + if err != nil { return err - } else if acl == nil || !acl.IsManagement() { + } else if acl == nil { return structs.ErrPermissionDenied } + // If it is not a management token determine if it can get this policy + mgt := acl.IsManagement() + if !mgt { + snap, err := a.srv.fsm.State().Snapshot() + if err != nil { + return err + } + + token, err := snap.ACLTokenBySecretID(nil, args.AuthToken) + if err != nil { + return err + } + if token == nil { + return structs.ErrTokenNotFound + } + + found := false + for _, p := range token.Policies { + if p == args.Name { + found = true + break + } + } + + if !found { + return structs.ErrPermissionDenied + } + } + // Setup the blocking query opts := blockingOptions{ queryOpts: &args.QueryOptions, diff --git a/nomad/acl_endpoint_test.go b/nomad/acl_endpoint_test.go index e9e7e9286..8c668aa75 100644 --- a/nomad/acl_endpoint_test.go +++ b/nomad/acl_endpoint_test.go @@ -28,6 +28,11 @@ func TestACLEndpoint_GetPolicy(t *testing.T) { policy := mock.ACLPolicy() s1.fsm.State().UpsertACLPolicies(1000, []*structs.ACLPolicy{policy}) + // Create a token with one the policy + token := mock.ACLToken() + token.Policies = []string{policy.Name} + s1.fsm.State().UpsertACLTokens(1001, []*structs.ACLToken{token}) + // Lookup the policy get := &structs.ACLPolicySpecificRequest{ Name: policy.Name, @@ -50,6 +55,21 @@ func TestACLEndpoint_GetPolicy(t *testing.T) { } assert.Equal(t, uint64(1000), resp.Index) assert.Nil(t, resp.Policy) + + // Lookup the policy with the token + get = &structs.ACLPolicySpecificRequest{ + Name: policy.Name, + QueryOptions: structs.QueryOptions{ + Region: "global", + AuthToken: token.SecretID, + }, + } + var resp2 structs.SingleACLPolicyResponse + if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", get, &resp2); err != nil { + t.Fatalf("err: %v", err) + } + assert.EqualValues(t, 1000, resp2.Index) + assert.Equal(t, policy, resp2.Policy) } func TestACLEndpoint_GetPolicy_Blocking(t *testing.T) { @@ -290,6 +310,7 @@ func TestACLEndpoint_GetPolicies_Blocking(t *testing.T) { } func TestACLEndpoint_ListPolicies(t *testing.T) { + assert := assert.New(t) t.Parallel() s1, root := testACLServer(t, nil) defer s1.Shutdown() @@ -304,6 +325,11 @@ func TestACLEndpoint_ListPolicies(t *testing.T) { p2.Name = "aaaabbbb-3350-4b4b-d185-0e1992ed43e9" s1.fsm.State().UpsertACLPolicies(1000, []*structs.ACLPolicy{p1, p2}) + // Create a token with one of those policies + token := mock.ACLToken() + token.Policies = []string{p1.Name} + s1.fsm.State().UpsertACLTokens(1001, []*structs.ACLToken{token}) + // Lookup the policies get := &structs.ACLPolicyListRequest{ QueryOptions: structs.QueryOptions{ @@ -315,8 +341,8 @@ func TestACLEndpoint_ListPolicies(t *testing.T) { if err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", get, &resp); err != nil { t.Fatalf("err: %v", err) } - assert.Equal(t, uint64(1000), resp.Index) - assert.Equal(t, 2, len(resp.Policies)) + assert.EqualValues(1000, resp.Index) + assert.Len(resp.Policies, 2) // Lookup the policies by prefix get = &structs.ACLPolicyListRequest{ @@ -330,8 +356,24 @@ func TestACLEndpoint_ListPolicies(t *testing.T) { if err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", get, &resp2); err != nil { t.Fatalf("err: %v", err) } - assert.Equal(t, uint64(1000), resp2.Index) - assert.Equal(t, 1, len(resp2.Policies)) + assert.EqualValues(1000, resp2.Index) + assert.Len(resp2.Policies, 1) + + // List policies using the created token + get = &structs.ACLPolicyListRequest{ + QueryOptions: structs.QueryOptions{ + Region: "global", + AuthToken: token.SecretID, + }, + } + var resp3 structs.ACLPolicyListResponse + if err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", get, &resp3); err != nil { + t.Fatalf("err: %v", err) + } + assert.EqualValues(1000, resp3.Index) + if assert.Len(resp3.Policies, 1) { + assert.Equal(resp3.Policies[0].Name, p1.Name) + } } func TestACLEndpoint_ListPolicies_Blocking(t *testing.T) { diff --git a/website/source/api/acl-policies.html.md b/website/source/api/acl-policies.html.md index a9891b18a..3150a30a0 100644 --- a/website/source/api/acl-policies.html.md +++ b/website/source/api/acl-policies.html.md @@ -26,7 +26,7 @@ The table below shows this endpoint's support for | Blocking Queries | Consistency Modes | ACL Required | | ---------------- | ----------------- | ------------ | -| `YES` | `all` | `management` | +| `YES` | `all` | `management` for all policies.
Output when given a non-management token will be limited to the policies on the token itself | ### Sample Request @@ -110,7 +110,7 @@ The table below shows this endpoint's support for | Blocking Queries | Consistency Modes | ACL Required | | ---------------- | ----------------- | ------------ | -| `YES` | `all` | `management` | +| `YES` | `all` | `management` or token with access to policy | ### Sample Request diff --git a/website/source/api/acl-tokens.html.md b/website/source/api/acl-tokens.html.md index e964927d6..58206af13 100644 --- a/website/source/api/acl-tokens.html.md +++ b/website/source/api/acl-tokens.html.md @@ -268,6 +268,49 @@ $ curl \ } ``` +## Read Self Token + +This endpoint reads the ACL token given by the passed SecretID. If the token is a global token +which has been replicated to the region it may lag behind the authoritative region. + +| Method | Path | Produces | +| ------ | ---------------------------- | -------------------------- | +| `GET` | `/acl/token/self` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries), [consistency modes](/api/index.html#consistency-modes) and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | Consistency Modes | ACL Required | +| ---------------- | ----------------- | ------------ | +| `YES` | `all` | Any valid ACL token | + +### Sample Request + +```text +$ curl \ + --header "X-Nomad-Token: 8176afd3-772d-0b71-8f85-7fa5d903e9d4" + https://nomad.rocks/v1/acl/token/self +``` + +### Sample Response + +```json +{ + "AccessorID": "aa534e09-6a07-0a45-2295-a7f77063d429", + "SecretID": "8176afd3-772d-0b71-8f85-7fa5d903e9d4", + "Name": "Read-write token", + "Type": "client", + "Policies": [ + "readwrite" + ], + "Global": false, + "CreateTime": "2017-08-23T23:25:41.429154233Z", + "CreateIndex": 52, + "ModifyIndex": 64 +} +``` + ## Delete Token This endpoint deletes the ACL token by accessor. This request is forwarded to the