diff --git a/api/acl.go b/api/acl.go index 67a9a04ae..ab7aaa117 100644 --- a/api/acl.go +++ b/api/acl.go @@ -206,6 +206,10 @@ var ( // errMissingACLRoleID is the generic errors to use when a call is missing // the required ACL Role ID parameter. errMissingACLRoleID = errors.New("missing ACL role ID") + + // errMissingACLAuthMethodName is the generic error to use when a call is + // missing the required ACL auth-method name parameter. + errMissingACLAuthMethodName = errors.New("missing ACL auth-method name") ) // ACLRoles is used to query the ACL Role endpoints. @@ -292,6 +296,76 @@ func (a *ACLRoles) GetByName(roleName string, q *QueryOptions) (*ACLRole, *Query return &resp, qm, nil } +// ACLAuthMethods is used to query the ACL auth-methods endpoints. +type ACLAuthMethods struct { + client *Client +} + +// ACLAuthMethods returns a new handle on the ACL auth-methods API client. +func (c *Client) ACLAuthMethods() *ACLAuthMethods { + return &ACLAuthMethods{client: c} +} + +// List is used to detail all the ACL auth-methods currently stored within +// state. +func (a *ACLAuthMethods) List(q *QueryOptions) ([]*ACLAuthMethodListStub, *QueryMeta, error) { + var resp []*ACLAuthMethodListStub + qm, err := a.client.query("/v1/acl/auth-methods", &resp, q) + if err != nil { + return nil, nil, err + } + return resp, qm, nil +} + +// Create is used to create an ACL auth-method. +func (a *ACLAuthMethods) Create(authMethod *ACLAuthMethod, w *WriteOptions) (*WriteMeta, error) { + if authMethod.Name == "" { + return nil, errMissingACLAuthMethodName + } + wm, err := a.client.write("/v1/acl/auth-method", authMethod, nil, w) + if err != nil { + return nil, err + } + return wm, nil +} + +// Update is used to update an existing ACL auth-method. +func (a *ACLAuthMethods) Update(authMethod *ACLAuthMethod, w *WriteOptions) (*WriteMeta, error) { + if authMethod.Name == "" { + return nil, errMissingACLAuthMethodName + } + wm, err := a.client.write("/v1/acl/auth-method/"+authMethod.Name, authMethod, nil, w) + if err != nil { + return nil, err + } + return wm, nil +} + +// Delete is used to delete an ACL auth-method. +func (a *ACLAuthMethods) Delete(authMethodName string, w *WriteOptions) (*WriteMeta, error) { + if authMethodName == "" { + return nil, errMissingACLAuthMethodName + } + wm, err := a.client.delete("/v1/acl/auth-method/"+authMethodName, nil, nil, w) + if err != nil { + return nil, err + } + return wm, nil +} + +// Get is used to look up an ACL auth-method. +func (a *ACLAuthMethods) Get(authMethodName string, q *QueryOptions) (*ACLAuthMethod, *QueryMeta, error) { + if authMethodName == "" { + return nil, nil, errMissingACLAuthMethodName + } + var resp ACLAuthMethod + qm, err := a.client.query("/v1/acl/auth-method/"+authMethodName, &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, qm, nil +} + // ACLPolicyListStub is used to for listing ACL policies type ACLPolicyListStub struct { Name string @@ -475,3 +549,77 @@ type ACLRoleListStub struct { CreateIndex uint64 ModifyIndex uint64 } + +// ACLAuthMethod is used to capture the properties of an authentication method +// used for single sing-on. +type ACLAuthMethod struct { + + // Name is the identifier for this auth-method and is a required parameter. + Name string + + // Type is the SSO identifier this auth-method is. Nomad currently only + // supports "oidc" and the API contains ACLAuthMethodTypeOIDC for + // convenience. + Type string + + // Defines whether the auth-method creates a local or global token when + // performing SSO login. This should be set to either "local" or "global" + // and the API contains ACLAuthMethodTokenLocalityLocal and + // ACLAuthMethodTokenLocalityGlobal for convenience. + TokenLocality string + + // MaxTokenTTL is the maximum life of a token created by this method. + MaxTokenTTL time.Duration + + // Default identifies whether this is the default auth-method to use when + // attempting to login without specifying an auth-method name to use. + Default bool + + // Config contains the detailed configuration which is specific to the + // auth-method. + Config *ACLAuthMethodConfig + + CreateTime time.Time + ModifyTime time.Time + CreateIndex uint64 + ModifyIndex uint64 +} + +// ACLAuthMethodConfig is used to store configuration of an auth method. +type ACLAuthMethodConfig struct { + OIDCDiscoveryURL string + OIDCClientID string + OIDCClientSecret string + BoundAudiences []string + AllowedRedirectURIs []string + DiscoveryCaPem []string + SigningAlgs []string + ClaimMappings map[string]string + ListClaimMappings map[string]string +} + +// ACLAuthMethodListStub is the stub object returned when performing a listing +// of ACL auth-methods. It is intentionally minimal due to the unauthenticated +// nature of the list endpoint. +type ACLAuthMethodListStub struct { + Name string + Default bool + + CreateIndex uint64 + ModifyIndex uint64 +} + +const ( + // ACLAuthMethodTokenLocalityLocal is the ACLAuthMethod.TokenLocality that + // will generate ACL tokens which can only be used on the local cluster the + // request was made. + ACLAuthMethodTokenLocalityLocal = "local" + + // ACLAuthMethodTokenLocalityGlobal is the ACLAuthMethod.TokenLocality that + // will generate ACL tokens which can be used on all federated clusters. + ACLAuthMethodTokenLocalityGlobal = "global" + + // ACLAuthMethodTypeOIDC the ACLAuthMethod.Type and represents an + // auth-method which uses the OIDC protocol. + ACLAuthMethodTypeOIDC = "OIDC" +) diff --git a/api/acl_test.go b/api/acl_test.go index 4a045c6a0..c55e702e1 100644 --- a/api/acl_test.go +++ b/api/acl_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/hashicorp/nomad/api/internal/testutil" + "github.com/shoenig/test/must" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -586,3 +587,70 @@ func TestACLRoles(t *testing.T) { require.Empty(t, aclRoleListResp) assertQueryMeta(t, queryMeta) } + +func TestACLAuthMethods(t *testing.T) { + testutil.Parallel(t) + + testClient, testServer, _ := makeACLClient(t, nil, nil) + defer testServer.Stop() + + // An initial listing shouldn't return any results. + aclAuthMethodsListResp, queryMeta, err := testClient.ACLAuthMethods().List(nil) + must.NoError(t, err) + must.Len(t, 0, aclAuthMethodsListResp) + assertQueryMeta(t, queryMeta) + + // Create an ACL auth-method. + authMethod := ACLAuthMethod{ + Name: "acl-auth-method-api-test", + Type: ACLAuthMethodTypeOIDC, + TokenLocality: ACLAuthMethodTokenLocalityLocal, + MaxTokenTTL: 15 * time.Minute, + Default: true, + } + writeMeta, err := testClient.ACLAuthMethods().Create(&authMethod, nil) + must.NoError(t, err) + assertWriteMeta(t, writeMeta) + + // Another listing should return one result. + aclAuthMethodsListResp, queryMeta, err = testClient.ACLAuthMethods().List(nil) + must.NoError(t, err) + must.Len(t, 1, aclAuthMethodsListResp) + must.Eq(t, authMethod.Name, aclAuthMethodsListResp[0].Name) + must.True(t, aclAuthMethodsListResp[0].Default) + assertQueryMeta(t, queryMeta) + + // Read the auth-method. + aclAuthMethodReadResp, queryMeta, err := testClient.ACLAuthMethods().Get(authMethod.Name, nil) + must.NoError(t, err) + assertQueryMeta(t, queryMeta) + must.NotNil(t, aclAuthMethodReadResp) + must.Eq(t, authMethod.Name, aclAuthMethodReadResp.Name) + must.Eq(t, authMethod.TokenLocality, aclAuthMethodReadResp.TokenLocality) + must.Eq(t, authMethod.Type, aclAuthMethodReadResp.Type) + + // Update the auth-method token locality. + authMethod.TokenLocality = ACLAuthMethodTokenLocalityGlobal + writeMeta, err = testClient.ACLAuthMethods().Update(&authMethod, nil) + must.NoError(t, err) + assertWriteMeta(t, writeMeta) + + // Re-read the auth-method and check the locality. + aclAuthMethodReadResp, queryMeta, err = testClient.ACLAuthMethods().Get(authMethod.Name, nil) + must.NoError(t, err) + assertQueryMeta(t, queryMeta) + must.NotNil(t, aclAuthMethodReadResp) + must.Eq(t, authMethod.Name, aclAuthMethodReadResp.Name) + must.Eq(t, authMethod.TokenLocality, aclAuthMethodReadResp.TokenLocality) + + // Delete the role. + writeMeta, err = testClient.ACLAuthMethods().Delete(authMethod.Name, nil) + must.NoError(t, err) + assertWriteMeta(t, writeMeta) + + // Make sure there are no ACL auth-methods now present. + aclAuthMethodsListResp, queryMeta, err = testClient.ACLAuthMethods().List(nil) + must.NoError(t, err) + must.Len(t, 0, aclAuthMethodsListResp) + assertQueryMeta(t, queryMeta) +} diff --git a/command/agent/acl_endpoint.go b/command/agent/acl_endpoint.go index 9cb171b88..bbfc006f6 100644 --- a/command/agent/acl_endpoint.go +++ b/command/agent/acl_endpoint.go @@ -466,7 +466,6 @@ func (s *HTTPServer) aclRoleDeleteRequest( } setIndex(resp, reply.Index) return nil, nil - } // aclRoleUpsertRequest handles upserting an ACL to the Nomad servers. It can @@ -502,7 +501,6 @@ func (s *HTTPServer) aclRoleUpsertRequest( return out.ACLRoles[0], nil } return nil, nil - } func (s *HTTPServer) aclRoleGetByNameRequest( @@ -526,3 +524,151 @@ func (s *HTTPServer) aclRoleGetByNameRequest( } return reply.ACLRole, nil } + +// ACLAuthMethodListRequest performs a listing of ACL auth-methods and is +// callable via the /v1/acl/auth-methods HTTP API. +func (s *HTTPServer) ACLAuthMethodListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + + // The endpoint only supports GET requests. + if req.Method != http.MethodGet { + return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod) + } + + // Set up the request args and parse this to ensure the query options are + // set. + args := structs.ACLAuthMethodListRequest{} + + if s.parse(resp, req, &args.Region, &args.QueryOptions) { + return nil, nil + } + + // Perform the RPC request. + var reply structs.ACLAuthMethodListResponse + if err := s.agent.RPC(structs.ACLListAuthMethodsRPCMethod, &args, &reply); err != nil { + return nil, err + } + + setMeta(resp, &reply.QueryMeta) + + if reply.AuthMethods == nil { + reply.AuthMethods = make([]*structs.ACLAuthMethodStub, 0) + } + return reply.AuthMethods, nil +} + +// ACLAuthMethodRequest creates a new ACL auth-method and is callable via the +// /v1/acl/auth-method HTTP API. +func (s *HTTPServer) ACLAuthMethodRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + + // // The endpoint only supports PUT or POST requests. + if !(req.Method == http.MethodPut || req.Method == http.MethodPost) { + return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod) + } + + // Use the generic upsert function without setting an ID as this will be + // handled by the Nomad leader. + return s.aclAuthMethodUpsertRequest(resp, req, "") +} + +// ACLAuthMethodSpecificRequest is callable via the /v1/acl/auth-method/ HTTP +// API and handles reads, updates, and deletions of named methods. +func (s *HTTPServer) ACLAuthMethodSpecificRequest( + resp http.ResponseWriter, req *http.Request) (interface{}, error) { + + // Grab the suffix of the request, so we can further understand it. + methodName := strings.TrimPrefix(req.URL.Path, "/v1/acl/auth-method/") + + // Ensure the auth-method name is not an empty string which is possible if + // the caller requested "/v1/acl/role/auth-method/". + if methodName == "" { + return nil, CodedError(http.StatusBadRequest, "missing ACL auth-method name") + } + + // Identify the method which indicates which downstream function should be + // called. + switch req.Method { + case http.MethodGet: + return s.aclAuthMethodGetRequest(resp, req, methodName) + case http.MethodDelete: + return s.aclAuthMethodDeleteRequest(resp, req, methodName) + case http.MethodPost, http.MethodPut: + return s.aclAuthMethodUpsertRequest(resp, req, methodName) + default: + return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod) + } +} + +// aclAuthMethodGetRequest is callable via the /v1/acl/auth-method/ HTTP API +// and is used for reading the named auth-method from state. +func (s *HTTPServer) aclAuthMethodGetRequest( + resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) { + + args := structs.ACLAuthMethodGetRequest{ + MethodName: methodName, + } + if s.parse(resp, req, &args.Region, &args.QueryOptions) { + return nil, nil + } + + var reply structs.ACLAuthMethodGetResponse + if err := s.agent.RPC(structs.ACLGetAuthMethodRPCMethod, &args, &reply); err != nil { + return nil, err + } + setMeta(resp, &reply.QueryMeta) + + if reply.AuthMethod == nil { + return nil, CodedError(http.StatusNotFound, "ACL auth-method not found") + } + return reply.AuthMethod, nil +} + +// aclAuthMethodDeleteRequest is callable via the /v1/acl/auth-method/ HTTP API +// and is responsible for deleting the named auth-method from state. +func (s *HTTPServer) aclAuthMethodDeleteRequest( + resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) { + + args := structs.ACLAuthMethodDeleteRequest{ + Names: []string{methodName}, + } + s.parseWriteRequest(req, &args.WriteRequest) + + var reply structs.ACLAuthMethodDeleteResponse + if err := s.agent.RPC(structs.ACLDeleteAuthMethodsRPCMethod, &args, &reply); err != nil { + return nil, err + } + setIndex(resp, reply.Index) + return nil, nil +} + +// aclAuthMethodUpsertRequest handles upserting an ACL auth-method to the Nomad +// servers. It can handle both new creations, and updates to existing +// auth-methods. +func (s *HTTPServer) aclAuthMethodUpsertRequest( + resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) { + + // Decode the ACL auth-method. + var aclAuthMethod structs.ACLAuthMethod + if err := decodeBody(req, &aclAuthMethod); err != nil { + return nil, CodedError(http.StatusBadRequest, err.Error()) + } + + // Ensure the request path name matches the ACL auth-method name that was + // decoded. Only perform this check on updates as a generic error on + // creation might be confusing to operators as there is no specific + // auth-method request path. + if methodName != "" && methodName != aclAuthMethod.Name { + return nil, CodedError(http.StatusBadRequest, "ACL auth-method name does not match request path") + } + + args := structs.ACLAuthMethodUpsertRequest{ + AuthMethods: []*structs.ACLAuthMethod{&aclAuthMethod}, + } + s.parseWriteRequest(req, &args.WriteRequest) + + var out structs.ACLAuthMethodUpsertResponse + if err := s.agent.RPC(structs.ACLUpsertAuthMethodsRPCMethod, &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 6cbc07667..973a0a5ee 100644 --- a/command/agent/acl_endpoint_test.go +++ b/command/agent/acl_endpoint_test.go @@ -5,11 +5,13 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/helper/uuid" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" + "github.com/shoenig/test/must" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -994,3 +996,302 @@ func TestHTTPServer_ACLRoleSpecificRequest(t *testing.T) { }) } } + +func TestHTTPServer_ACLAuthMethodListRequest(t *testing.T) { + ci.Parallel(t) + + testCases := []struct { + name string + testFn func(srv *TestAgent) + }{ + { + name: "no auth token set", + testFn: func(srv *TestAgent) { + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodGet, "/v1/acl/auth-methods", nil) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + obj, err := srv.Server.ACLAuthMethodListRequest(respW, req) + must.NoError(t, err) + must.Len(t, 0, obj.([]*structs.ACLAuthMethodStub)) + }, + }, + { + name: "invalid method", + testFn: func(srv *TestAgent) { + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodConnect, "/v1/acl/auth-methods", nil) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Ensure we have a token set. + setToken(req, srv.RootToken) + + // Send the HTTP request. + obj, err := srv.Server.ACLAuthMethodListRequest(respW, req) + must.Error(t, err) + must.StrContains(t, err.Error(), "Invalid method") + must.Nil(t, obj) + }, + }, + { + name: "no auth-methods in state", + testFn: func(srv *TestAgent) { + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodGet, "/v1/acl/auth-methods", nil) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Ensure we have a token set. + setToken(req, srv.RootToken) + + // Send the HTTP request. + obj, err := srv.Server.ACLAuthMethodListRequest(respW, req) + must.NoError(t, err) + must.Len(t, 0, obj.([]*structs.ACLAuthMethodStub)) + }, + }, + { + name: "auth-methods in state", + testFn: func(srv *TestAgent) { + + // Upsert two auth-methods into state. + must.NoError(t, srv.server.State().UpsertACLAuthMethods( + 10, []*structs.ACLAuthMethod{mock.ACLAuthMethod(), mock.ACLAuthMethod()})) + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodGet, "/v1/acl/auth-methods", nil) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Ensure we have a token set. + setToken(req, srv.RootToken) + + // Send the HTTP request. + obj, err := srv.Server.ACLAuthMethodListRequest(respW, req) + must.NoError(t, err) + must.Len(t, 2, obj.([]*structs.ACLAuthMethodStub)) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + httpACLTest(t, nil, tc.testFn) + }) + } +} + +func TestHTTPServer_ACLAuthMethodRequest(t *testing.T) { + ci.Parallel(t) + + testCases := []struct { + name string + testFn func(srv *TestAgent) + }{ + { + name: "no auth token set", + testFn: func(srv *TestAgent) { + + // Create a mock role to use in the request body. + mockACLRole := mock.ACLRole() + mockACLRole.ID = "" + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodPut, "/v1/acl/auth-method", encodeReq(mockACLRole)) + require.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + obj, err := srv.Server.ACLAuthMethodRequest(respW, req) + require.Error(t, err) + require.ErrorContains(t, err, "Permission denied") + require.Nil(t, obj) + }, + }, + { + name: "invalid method", + testFn: func(srv *TestAgent) { + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodConnect, "/v1/acl/auth-method", nil) + require.NoError(t, err) + respW := httptest.NewRecorder() + + // Ensure we have a token set. + setToken(req, srv.RootToken) + + // Send the HTTP request. + obj, err := srv.Server.ACLAuthMethodRequest(respW, req) + require.Error(t, err) + require.ErrorContains(t, err, "Invalid method") + require.Nil(t, obj) + }, + }, + { + name: "successful upsert", + testFn: func(srv *TestAgent) { + + // Create a mock auth-method to use in the request body. + mockACLAuthMethod := mock.ACLAuthMethod() + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodPut, "/v1/acl/auth-method", encodeReq(mockACLAuthMethod)) + require.NoError(t, err) + respW := httptest.NewRecorder() + + // Ensure we have a token set. + setToken(req, srv.RootToken) + + // Send the HTTP request. + obj, err := srv.Server.ACLAuthMethodRequest(respW, req) + require.NoError(t, err) + require.Nil(t, obj) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + httpACLTest(t, nil, tc.testFn) + }) + } +} + +func TestHTTPServer_ACLAuthMethodSpecificRequest(t *testing.T) { + ci.Parallel(t) + + testCases := []struct { + name string + testFn func(srv *TestAgent) + }{ + { + name: "missing auth-method name", + testFn: func(srv *TestAgent) { + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodGet, "/v1/acl/auth-method/", nil) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + obj, err := srv.Server.ACLAuthMethodSpecificRequest(respW, req) + must.Error(t, err) + must.StrContains(t, err.Error(), "missing ACL auth-method name") + must.Nil(t, obj) + }, + }, + { + name: "incorrect method", + testFn: func(srv *TestAgent) { + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodConnect, "/v1/acl/auth-method/foobar", nil) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + obj, err := srv.Server.ACLAuthMethodSpecificRequest(respW, req) + must.Error(t, err) + must.StrContains(t, err.Error(), "Invalid method") + must.Nil(t, obj) + }, + }, + { + name: "get auth-method", + testFn: func(srv *TestAgent) { + + // Create a mock auth-method and put directly into state. + mockACLAuthMethod := mock.ACLAuthMethod() + must.NoError(t, srv.server.State().UpsertACLAuthMethods( + 20, []*structs.ACLAuthMethod{mockACLAuthMethod})) + + url := "/v1/acl/auth-method/" + mockACLAuthMethod.Name + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodGet, url, nil) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Ensure we have a token set. + setToken(req, srv.RootToken) + + // Send the HTTP request. + obj, err := srv.Server.ACLAuthMethodSpecificRequest(respW, req) + must.NoError(t, err) + must.Eq(t, obj.(*structs.ACLAuthMethod).Hash, mockACLAuthMethod.Hash) + }, + }, + { + name: "get, update, and delete auth-method", + testFn: func(srv *TestAgent) { + + // Create a mock auth-method and put directly into state. + mockACLAuthMethod := mock.ACLAuthMethod() + must.NoError(t, srv.server.State().UpsertACLAuthMethods( + 20, []*structs.ACLAuthMethod{mockACLAuthMethod})) + + url := "/v1/acl/auth-method/" + mockACLAuthMethod.Name + + // Build the HTTP request to read the auth-method. + req, err := http.NewRequest(http.MethodGet, url, nil) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Ensure we have a token set. + setToken(req, srv.RootToken) + + // Send the HTTP request. + obj, err := srv.Server.ACLAuthMethodSpecificRequest(respW, req) + must.NoError(t, err) + must.Eq(t, obj.(*structs.ACLAuthMethod).Hash, mockACLAuthMethod.Hash) + + // Update the auth-method and make the request via the HTTP + // API. + mockACLAuthMethod.MaxTokenTTL = 3600 * time.Hour + mockACLAuthMethod.SetHash() + + req, err = http.NewRequest(http.MethodPost, url, encodeReq(mockACLAuthMethod)) + must.NoError(t, err) + respW = httptest.NewRecorder() + + // Ensure we have a token set. + setToken(req, srv.RootToken) + + // Send the HTTP request. + _, err = srv.Server.ACLAuthMethodSpecificRequest(respW, req) + must.NoError(t, err) + + // Delete the ACL auth-method. + req, err = http.NewRequest(http.MethodDelete, url, nil) + must.NoError(t, err) + respW = httptest.NewRecorder() + + // Ensure we have a token set. + setToken(req, srv.RootToken) + + // Send the HTTP request. + obj, err = srv.Server.ACLAuthMethodSpecificRequest(respW, req) + must.NoError(t, err) + must.Nil(t, obj) + + // Ensure the ACL auth-method is no longer stored within state. + aclAuthMethod, err := srv.server.State().GetACLAuthMethodByName(nil, mockACLAuthMethod.Name) + must.NoError(t, err) + must.Nil(t, aclAuthMethod) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cb := func(c *Config) { c.NomadConfig.ACLTokenMaxExpirationTTL = 3600 * time.Hour } + httpACLTest(t, cb, tc.testFn) + }) + } +} diff --git a/command/agent/http.go b/command/agent/http.go index 3322caa64..f6ecc8f61 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -387,6 +387,11 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/acl/role", s.wrap(s.ACLRoleRequest)) s.mux.HandleFunc("/v1/acl/role/", s.wrap(s.ACLRoleSpecificRequest)) + // Register our ACL auth-method handlers. + s.mux.HandleFunc("/v1/acl/auth-methods", s.wrap(s.ACLAuthMethodListRequest)) + s.mux.HandleFunc("/v1/acl/auth-method", s.wrap(s.ACLAuthMethodRequest)) + s.mux.HandleFunc("/v1/acl/auth-method/", s.wrap(s.ACLAuthMethodSpecificRequest)) + s.mux.Handle("/v1/client/fs/", wrapCORS(s.wrap(s.FsRequest))) s.mux.HandleFunc("/v1/client/gc", s.wrap(s.ClientGCRequest)) s.mux.Handle("/v1/client/stats", wrapCORS(s.wrap(s.ClientStatsRequest))) diff --git a/nomad/acl_endpoint.go b/nomad/acl_endpoint.go index defa78d42..026a69ad6 100644 --- a/nomad/acl_endpoint.go +++ b/nomad/acl_endpoint.go @@ -1706,8 +1706,8 @@ func (a *ACL) UpsertAuthMethods( // Validate each auth method, compute hash for idx, authMethod := range args.AuthMethods { if err := authMethod.Validate( - a.srv.config.ACLAuthMethodMinExpirationTTL, - a.srv.config.ACLAuthMethodMaxExpirationTTL); err != nil { + a.srv.config.ACLTokenMinExpirationTTL, + a.srv.config.ACLTokenMaxExpirationTTL); err != nil { return structs.NewErrRPCCodedf(http.StatusBadRequest, "auth method %d invalid: %v", idx, err) } authMethod.SetHash() diff --git a/nomad/acl_endpoint_test.go b/nomad/acl_endpoint_test.go index 003daa567..4a69c085b 100644 --- a/nomad/acl_endpoint_test.go +++ b/nomad/acl_endpoint_test.go @@ -3017,8 +3017,8 @@ func TestACLEndpoint_UpsertACLAuthMethods(t *testing.T) { minTTL, _ := time.ParseDuration("10s") maxTTL, _ := time.ParseDuration("24h") - s1.config.ACLAuthMethodMinExpirationTTL = minTTL - s1.config.ACLAuthMethodMaxExpirationTTL = maxTTL + s1.config.ACLTokenMinExpirationTTL = minTTL + s1.config.ACLTokenMaxExpirationTTL = maxTTL // Create the register request am1 := mock.ACLAuthMethod() diff --git a/nomad/config.go b/nomad/config.go index 3d9a9bb73..2bcb8714b 100644 --- a/nomad/config.go +++ b/nomad/config.go @@ -328,14 +328,6 @@ type Config struct { // for ACL token expiration. ACLTokenMaxExpirationTTL time.Duration - // ACLAuthMethodMinExpirationTTL is used to enforce the lowest acceptable - // value for ACL auth method expiration. - ACLAuthMethodMinExpirationTTL time.Duration - - // ACLAuthMethodMaxExpirationTTL is used to enforce the highest acceptable - // value for ACL auth method expiration. - ACLAuthMethodMaxExpirationTTL time.Duration - // SentinelGCInterval is the interval that we GC unused policies. SentinelGCInterval time.Duration