mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
server: Add RPC and HTTP functionality for node intro token gen. (#26320)
The node introduction workflow will utilise JWT's that can be used as authentication tokens on initial client registration. This change implements the basic builder for this JWT claim type and the RPC and HTTP handler functionality that will expose this to the operator.
This commit is contained in:
@@ -945,3 +945,31 @@ func (s *HTTPServer) ACLLoginRequest(resp http.ResponseWriter, req *http.Request
|
||||
setIndex(resp, out.Index)
|
||||
return out.ACLToken, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) ACLCreateClientIntroductionTokenRequest(
|
||||
_ http.ResponseWriter,
|
||||
req *http.Request) (any, error) {
|
||||
|
||||
// The endpoint only supports PUT or POST requests.
|
||||
if req.Method != http.MethodPost && req.Method != http.MethodPut {
|
||||
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
||||
}
|
||||
|
||||
args := structs.ACLCreateClientIntroductionTokenRequest{}
|
||||
|
||||
// Perform the parsing of the write request. The parameters can be passed
|
||||
// using just headers, or within the request body.
|
||||
s.parseWriteRequest(req, &args.WriteRequest)
|
||||
|
||||
if req.Body != nil {
|
||||
if err := decodeBody(req, &args); err != nil {
|
||||
return nil, CodedError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var out structs.ACLCreateClientIntroductionTokenResponse
|
||||
if err := s.agent.RPC(structs.ACLCreateClientIntroductionTokenRPCMethod, &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
@@ -2126,3 +2126,140 @@ func requestAuthState(t *testing.T, server *HTTPServer, authMethod *structs.ACLA
|
||||
must.NoError(t, err)
|
||||
return u.Query().Get("state")
|
||||
}
|
||||
|
||||
func TestHTTPServer_ACLClientIntroductionTokenRequest(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFn func(srv *TestAgent)
|
||||
}{
|
||||
{
|
||||
name: "incorrect method",
|
||||
testFn: func(testAgent *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(
|
||||
http.MethodConnect,
|
||||
"/v1/acl/identity/client-introduction-token",
|
||||
nil,
|
||||
)
|
||||
must.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := testAgent.Server.ACLCreateClientIntroductionTokenRequest(respW, req)
|
||||
must.Error(t, err)
|
||||
must.ErrorContains(t, err, ErrInvalidMethod)
|
||||
must.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "incorrect permissions",
|
||||
testFn: func(testAgent *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(
|
||||
http.MethodPost,
|
||||
"/v1/acl/identity/client-introduction-token",
|
||||
nil,
|
||||
)
|
||||
must.NoError(t, err)
|
||||
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := testAgent.Server.ACLCreateClientIntroductionTokenRequest(respW, req)
|
||||
must.Error(t, err)
|
||||
must.ErrorContains(t, err, "Permission denied")
|
||||
must.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid request with body",
|
||||
testFn: func(testAgent *TestAgent) {
|
||||
|
||||
nodeWriteToken := mock.CreatePolicyAndToken(
|
||||
t,
|
||||
testAgent.Agent.Server().State(),
|
||||
10,
|
||||
fmt.Sprintf("policy-%s-%s", t.Name(), uuid.Generate()),
|
||||
`node{policy = "write"}`,
|
||||
)
|
||||
|
||||
requestBody := structs.ACLCreateClientIntroductionTokenRequest{
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: testAgent.config().Region,
|
||||
AuthToken: nodeWriteToken.SecretID,
|
||||
},
|
||||
}
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(
|
||||
http.MethodPost,
|
||||
"/v1/acl/identity/client-introduction-token",
|
||||
encodeReq(&requestBody),
|
||||
)
|
||||
must.NoError(t, err)
|
||||
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := testAgent.Server.ACLCreateClientIntroductionTokenRequest(respW, req)
|
||||
must.NoError(t, err)
|
||||
must.NotNil(t, obj)
|
||||
|
||||
// We do not have access to the encrypter, so we cannot verify
|
||||
// the JWT content. Tests in the RPC layer cover this.
|
||||
nodeIntroTokenResp, ok := obj.(*structs.ACLCreateClientIntroductionTokenResponse)
|
||||
must.True(t, ok)
|
||||
must.NotNil(t, nodeIntroTokenResp)
|
||||
must.NotEq(t, "", nodeIntroTokenResp.JWT)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid request with headers",
|
||||
testFn: func(testAgent *TestAgent) {
|
||||
|
||||
nodeWriteToken := mock.CreatePolicyAndToken(
|
||||
t,
|
||||
testAgent.Agent.Server().State(),
|
||||
10,
|
||||
fmt.Sprintf("policy-%s-%s", t.Name(), uuid.Generate()),
|
||||
`node{policy = "write"}`,
|
||||
)
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(
|
||||
http.MethodPost,
|
||||
"/v1/acl/identity/client-introduction-token",
|
||||
nil,
|
||||
)
|
||||
must.NoError(t, err)
|
||||
|
||||
req.Header.Add("X-Nomad-Token", nodeWriteToken.SecretID)
|
||||
req.Header.Add("X-Nomad-Region", testAgent.config().Region)
|
||||
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := testAgent.Server.ACLCreateClientIntroductionTokenRequest(respW, req)
|
||||
must.NoError(t, err)
|
||||
must.NotNil(t, obj)
|
||||
|
||||
// We do not have access to the encrypter, so we cannot verify
|
||||
// the JWT content. Tests in the RPC layer cover this.
|
||||
nodeIntroTokenResp, ok := obj.(*structs.ACLCreateClientIntroductionTokenResponse)
|
||||
must.True(t, ok)
|
||||
must.NotNil(t, nodeIntroTokenResp)
|
||||
must.NotEq(t, "", nodeIntroTokenResp.JWT)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
httpACLTest(t, nil, tc.testFn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,6 +444,7 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
|
||||
s.mux.HandleFunc("/v1/acl/oidc/auth-url", s.wrap(s.ACLOIDCAuthURLRequest))
|
||||
s.mux.HandleFunc("/v1/acl/oidc/complete-auth", s.wrap(s.ACLOIDCCompleteAuthRequest))
|
||||
s.mux.HandleFunc("/v1/acl/login", s.wrap(s.ACLLoginRequest))
|
||||
s.mux.HandleFunc("/v1/acl/identity/client-introduction-token", s.wrap(s.ACLCreateClientIntroductionTokenRequest))
|
||||
|
||||
s.mux.Handle("/v1/client/fs/", wrapCORS(s.wrap(s.FsRequest)))
|
||||
s.mux.HandleFunc("/v1/client/gc", s.wrap(s.ClientGCRequest))
|
||||
|
||||
Reference in New Issue
Block a user