From 70cdccf6432365a11845f571dab031c253b2d90b Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sat, 12 Aug 2017 16:08:00 -0700 Subject: [PATCH] agent: Adding HTTP endpoints for ACL tokens --- command/agent/acl_endpoint.go | 115 ++++++++++++++++++++ command/agent/acl_endpoint_test.go | 164 +++++++++++++++++++++++++++++ command/agent/http.go | 3 + nomad/acl_endpoint.go | 2 +- nomad/structs/structs.go | 26 ++++- 5 files changed, 308 insertions(+), 2 deletions(-) diff --git a/command/agent/acl_endpoint.go b/command/agent/acl_endpoint.go index 422fe3d59..0398212f0 100644 --- a/command/agent/acl_endpoint.go +++ b/command/agent/acl_endpoint.go @@ -109,3 +109,118 @@ func (s *HTTPServer) aclPolicyDelete(resp http.ResponseWriter, req *http.Request setIndex(resp, out.Index) return nil, nil } + +func (s *HTTPServer) ACLTokensRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, CodedError(405, ErrInvalidMethod) + } + + args := structs.ACLTokenListRequest{} + if s.parse(resp, req, &args.Region, &args.QueryOptions) { + return nil, nil + } + + var out structs.ACLTokenListResponse + if err := s.agent.RPC("ACL.ListTokens", &args, &out); err != nil { + return nil, err + } + + setMeta(resp, &out.QueryMeta) + if out.Tokens == nil { + out.Tokens = make([]*structs.ACLTokenListStub, 0) + } + return out.Tokens, nil +} + +func (s *HTTPServer) ACLTokenSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + accessor := strings.TrimPrefix(req.URL.Path, "/v1/acl/token") + + // If there is no accessor, this must be a create + if len(accessor) == 0 { + if !(req.Method == "PUT" || req.Method == "POST") { + return nil, CodedError(405, ErrInvalidMethod) + } + return s.aclTokenUpdate(resp, req, "") + } + + // Check if no accessor is given past the slash + accessor = accessor[1:] + if accessor == "" { + return nil, CodedError(400, "Missing Token Accessor") + } + + switch req.Method { + case "GET": + return s.aclTokenQuery(resp, req, accessor) + case "PUT", "POST": + return s.aclTokenUpdate(resp, req, accessor) + case "DELETE": + return s.aclTokenDelete(resp, req, accessor) + default: + return nil, CodedError(405, ErrInvalidMethod) + } +} + +func (s *HTTPServer) aclTokenQuery(resp http.ResponseWriter, req *http.Request, + tokenAccessor string) (interface{}, error) { + args := structs.ACLTokenSpecificRequest{ + AccessorID: tokenAccessor, + } + if s.parse(resp, req, &args.Region, &args.QueryOptions) { + return nil, nil + } + + var out structs.SingleACLTokenResponse + if err := s.agent.RPC("ACL.GetToken", &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 + var token structs.ACLToken + if err := decodeBody(req, &token); err != nil { + return nil, CodedError(500, err.Error()) + } + + // Ensure the token accessor matches + if tokenAccessor != "" && (token.AccessorID != tokenAccessor) { + return nil, CodedError(400, "ACL token accessor does not match request path") + } + + // Format the request + args := structs.ACLTokenUpsertRequest{ + Tokens: []*structs.ACLToken{&token}, + } + s.parseRegion(req, &args.Region) + + var out structs.GenericResponse + if err := s.agent.RPC("ACL.UpsertTokens", &args, &out); err != nil { + return nil, err + } + setIndex(resp, out.Index) + return nil, nil +} + +func (s *HTTPServer) aclTokenDelete(resp http.ResponseWriter, req *http.Request, + tokenAccessor string) (interface{}, error) { + + args := structs.ACLTokenDeleteRequest{ + AccessorIDs: []string{tokenAccessor}, + } + s.parseRegion(req, &args.Region) + + var out structs.GenericResponse + if err := s.agent.RPC("ACL.DeleteTokens", &args, &out); err != nil { + return nil, err + } + setIndex(resp, out.Index) + return nil, nil +} diff --git a/command/agent/acl_endpoint_test.go b/command/agent/acl_endpoint_test.go index e7203f211..606b4bd53 100644 --- a/command/agent/acl_endpoint_test.go +++ b/command/agent/acl_endpoint_test.go @@ -173,3 +173,167 @@ func TestHTTP_ACLPolicyDelete(t *testing.T) { assert.Nil(t, out) }) } + +func TestHTTP_ACLTokenList(t *testing.T) { + t.Parallel() + httpTest(t, nil, func(s *TestAgent) { + p1 := mock.ACLToken() + p2 := mock.ACLToken() + p3 := mock.ACLToken() + args := structs.ACLTokenUpsertRequest{ + Tokens: []*structs.ACLToken{p1, p2, p3}, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + var resp structs.GenericResponse + if err := s.Agent.RPC("ACL.UpsertTokens", &args, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + // Make the HTTP request + req, err := http.NewRequest("GET", "/v1/acl/tokens", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.ACLTokensRequest(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.ACLTokenListStub) + if len(n) != 3 { + t.Fatalf("bad: %#v", n) + } + }) +} + +func TestHTTP_ACLTokenQuery(t *testing.T) { + t.Parallel() + httpTest(t, nil, func(s *TestAgent) { + p1 := mock.ACLToken() + args := structs.ACLTokenUpsertRequest{ + Tokens: []*structs.ACLToken{p1}, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + var resp structs.GenericResponse + if err := s.Agent.RPC("ACL.UpsertTokens", &args, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + // Make the HTTP request + req, err := http.NewRequest("GET", "/v1/acl/token/"+p1.AccessorID, nil) + if err != nil { + t.Fatalf("err: %v", err) + } + respW := httptest.NewRecorder() + + // 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) + if n.AccessorID != p1.AccessorID { + t.Fatalf("bad: %#v", n) + } + }) +} + +func TestHTTP_ACLTokenCreate(t *testing.T) { + t.Parallel() + httpTest(t, nil, func(s *TestAgent) { + // Make the HTTP request + p1 := mock.ACLToken() + buf := encodeReq(p1) + req, err := http.NewRequest("PUT", "/v1/acl/token", buf) + if err != nil { + t.Fatalf("err: %v", err) + } + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.ACLTokenSpecificRequest(respW, req) + assert.Nil(t, err) + assert.Nil(t, obj) + + // Check for the index + if respW.HeaderMap.Get("X-Nomad-Index") == "" { + t.Fatalf("missing index") + } + + // Check token was created + state := s.Agent.server.State() + out, err := state.ACLTokenByAccessorID(nil, p1.AccessorID) + assert.Nil(t, err) + assert.NotNil(t, out) + + p1.CreateIndex, p1.ModifyIndex = out.CreateIndex, out.ModifyIndex + assert.Equal(t, p1.Name, out.Name) + assert.Equal(t, p1, out) + }) +} + +func TestHTTP_ACLTokenDelete(t *testing.T) { + t.Parallel() + httpTest(t, nil, func(s *TestAgent) { + p1 := mock.ACLToken() + args := structs.ACLTokenUpsertRequest{ + Tokens: []*structs.ACLToken{p1}, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + var resp structs.GenericResponse + if err := s.Agent.RPC("ACL.UpsertTokens", &args, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + // Make the HTTP request + req, err := http.NewRequest("DELETE", "/v1/acl/token/"+p1.AccessorID, nil) + if err != nil { + t.Fatalf("err: %v", err) + } + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.ACLTokenSpecificRequest(respW, req) + assert.Nil(t, err) + assert.Nil(t, obj) + + // Check for the index + if respW.HeaderMap.Get("X-Nomad-Index") == "" { + t.Fatalf("missing index") + } + + // Check token was created + state := s.Agent.server.State() + out, err := state.ACLTokenByAccessorID(nil, p1.AccessorID) + assert.Nil(t, err) + assert.Nil(t, out) + }) +} diff --git a/command/agent/http.go b/command/agent/http.go index 74505f4a8..0efbfe818 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -151,6 +151,9 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/acl/policies", s.wrap(s.ACLPoliciesRequest)) s.mux.HandleFunc("/v1/acl/policy/", s.wrap(s.ACLPolicySpecificRequest)) + s.mux.HandleFunc("/v1/acl/tokens", s.wrap(s.ACLTokensRequest)) + s.mux.HandleFunc("/v1/acl/token", s.wrap(s.ACLTokenSpecificRequest)) + s.mux.HandleFunc("/v1/client/fs/", s.wrap(s.FsRequest)) s.mux.HandleFunc("/v1/client/stats", s.wrap(s.ClientStatsRequest)) s.mux.HandleFunc("/v1/client/allocation/", s.wrap(s.ClientAllocRequest)) diff --git a/nomad/acl_endpoint.go b/nomad/acl_endpoint.go index 2e80cf2e8..cc3183adb 100644 --- a/nomad/acl_endpoint.go +++ b/nomad/acl_endpoint.go @@ -234,7 +234,7 @@ func (a *ACL) ListTokens(args *structs.ACLTokenListRequest, reply *structs.ACLTo break } token := raw.(*structs.ACLToken) - reply.Tokens = append(reply.Tokens, token) + reply.Tokens = append(reply.Tokens, token.Stub()) } // Use the last index that affected the token table diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 505ddef75..9e8e9114c 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -5434,6 +5434,30 @@ type ACLToken struct { ModifyIndex uint64 } +type ACLTokenListStub struct { + AccessorID string + Name string + Type string + Policies []string + Global bool + CreateTime time.Time + CreateIndex uint64 + ModifyIndex uint64 +} + +func (a *ACLToken) Stub() *ACLTokenListStub { + return &ACLTokenListStub{ + AccessorID: a.AccessorID, + Name: a.Name, + Type: a.Type, + Policies: a.Policies, + Global: a.Global, + CreateTime: a.CreateTime, + CreateIndex: a.CreateIndex, + ModifyIndex: a.ModifyIndex, + } +} + // Validate is used to sanity check a token func (a *ACLToken) Validate() error { var mErr multierror.Error @@ -5468,7 +5492,7 @@ type ACLTokenSpecificRequest struct { // ACLTokenListResponse is used for a list request type ACLTokenListResponse struct { - Tokens []*ACLToken + Tokens []*ACLTokenListStub QueryMeta }