mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
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:
148
api/acl.go
148
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"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user