sso: add ACL auth-method HTTP API CRUD endpoints (#15338)

* core: remove custom auth-method TTLS and use ACL token TTLS.

* agent: add ACL auth-method HTTP endpoints for CRUD actions.

* api: add ACL auth-method client.
This commit is contained in:
James Rasell
2022-11-23 09:38:02 +01:00
committed by GitHub
parent 142043382e
commit 84b79aa87d
8 changed files with 674 additions and 14 deletions

View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

@@ -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)))