HTTP API support for 'nomad ui -login'
Endpoints for requesting and exchanging one-time tokens via the HTTP API. Includes documentation updates.
This commit is contained in:
parent
7010a344d6
commit
75878f978e
50
api/acl.go
50
api/acl.go
|
@ -154,6 +154,36 @@ func (a *ACLTokens) Self(q *QueryOptions) (*ACLToken, *QueryMeta, error) {
|
|||
return &resp, wm, nil
|
||||
}
|
||||
|
||||
// UpsertOneTimeToken is used to create a one-time token
|
||||
func (a *ACLTokens) UpsertOneTimeToken(q *WriteOptions) (*OneTimeToken, *WriteMeta, error) {
|
||||
var resp *OneTimeTokenUpsertResponse
|
||||
wm, err := a.client.write("/v1/acl/token/onetime", nil, &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, nil, fmt.Errorf("no one-time token returned")
|
||||
}
|
||||
return resp.OneTimeToken, wm, nil
|
||||
}
|
||||
|
||||
// ExchangeOneTimeToken is used to create a one-time token
|
||||
func (a *ACLTokens) ExchangeOneTimeToken(secret string, q *WriteOptions) (*ACLToken, *WriteMeta, error) {
|
||||
if secret == "" {
|
||||
return nil, nil, fmt.Errorf("missing secret ID")
|
||||
}
|
||||
req := &OneTimeTokenExchangeRequest{OneTimeSecretID: secret}
|
||||
var resp *OneTimeTokenExchangeResponse
|
||||
wm, err := a.client.write("/v1/acl/token/onetime/exchange", req, &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, nil, fmt.Errorf("no ACL token returned")
|
||||
}
|
||||
return resp.Token, wm, nil
|
||||
}
|
||||
|
||||
// ACLPolicyListStub is used to for listing ACL policies
|
||||
type ACLPolicyListStub struct {
|
||||
Name string
|
||||
|
@ -194,3 +224,23 @@ type ACLTokenListStub struct {
|
|||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
type OneTimeToken struct {
|
||||
OneTimeSecretID string
|
||||
AccessorID string
|
||||
ExpiresAt time.Time
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
type OneTimeTokenUpsertResponse struct {
|
||||
OneTimeToken *OneTimeToken
|
||||
}
|
||||
|
||||
type OneTimeTokenExchangeRequest struct {
|
||||
OneTimeSecretID string
|
||||
}
|
||||
|
||||
type OneTimeTokenExchangeResponse struct {
|
||||
Token *ACLToken
|
||||
}
|
||||
|
|
|
@ -235,3 +235,36 @@ func TestACLTokens_Delete(t *testing.T) {
|
|||
assert.Nil(t, err)
|
||||
assertWriteMeta(t, wm)
|
||||
}
|
||||
|
||||
func TestACL_OneTimeToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
c, s, _ := makeACLClient(t, nil, nil)
|
||||
defer s.Stop()
|
||||
at := c.ACLTokens()
|
||||
|
||||
token := &ACLToken{
|
||||
Name: "foo",
|
||||
Type: "client",
|
||||
Policies: []string{"foo1"},
|
||||
}
|
||||
|
||||
// Create the ACL token
|
||||
out, wm, err := at.Create(token, nil)
|
||||
assert.Nil(t, err)
|
||||
assertWriteMeta(t, wm)
|
||||
assert.NotNil(t, out)
|
||||
|
||||
// Get a one-time token
|
||||
c.SetSecretID(out.SecretID)
|
||||
out2, wm, err := at.UpsertOneTimeToken(nil)
|
||||
assert.Nil(t, err)
|
||||
assertWriteMeta(t, wm)
|
||||
assert.NotNil(t, out2)
|
||||
|
||||
// Exchange the one-time token
|
||||
out3, wm, err := at.ExchangeOneTimeToken(out2.OneTimeSecretID, nil)
|
||||
assert.Nil(t, err)
|
||||
assertWriteMeta(t, wm)
|
||||
assert.NotNil(t, out3)
|
||||
assert.Equal(t, out3.AccessorID, out.AccessorID)
|
||||
}
|
||||
|
|
|
@ -277,3 +277,42 @@ func (s *HTTPServer) aclTokenDelete(resp http.ResponseWriter, req *http.Request,
|
|||
setIndex(resp, out.Index)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) UpsertOneTimeToken(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
// Ensure this is a PUT or POST
|
||||
if !(req.Method == "PUT" || req.Method == "POST") {
|
||||
return nil, CodedError(405, ErrInvalidMethod)
|
||||
}
|
||||
|
||||
// the request body is empty but we need to parse to get the auth token
|
||||
args := structs.OneTimeTokenUpsertRequest{}
|
||||
s.parseWriteRequest(req, &args.WriteRequest)
|
||||
|
||||
var out structs.OneTimeTokenUpsertResponse
|
||||
if err := s.agent.RPC("ACL.UpsertOneTimeToken", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setIndex(resp, out.Index)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) ExchangeOneTimeToken(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
// Ensure this is a PUT or POST
|
||||
if !(req.Method == "PUT" || req.Method == "POST") {
|
||||
return nil, CodedError(405, ErrInvalidMethod)
|
||||
}
|
||||
|
||||
var args structs.OneTimeTokenExchangeRequest
|
||||
if err := decodeBody(req, &args); err != nil {
|
||||
return nil, CodedError(500, err.Error())
|
||||
}
|
||||
|
||||
s.parseWriteRequest(req, &args.WriteRequest)
|
||||
|
||||
var out structs.OneTimeTokenExchangeResponse
|
||||
if err := s.agent.RPC("ACL.ExchangeOneTimeToken", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setIndex(resp, out.Index)
|
||||
return out, nil
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHTTP_ACLPolicyList(t *testing.T) {
|
||||
|
@ -448,3 +449,66 @@ func TestHTTP_ACLTokenDelete(t *testing.T) {
|
|||
assert.Nil(t, out)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTP_OneTimeToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
httpACLTest(t, nil, func(s *TestAgent) {
|
||||
|
||||
// Setup the ACL token
|
||||
|
||||
p1 := mock.ACLToken()
|
||||
p1.AccessorID = ""
|
||||
args := structs.ACLTokenUpsertRequest{
|
||||
Tokens: []*structs.ACLToken{p1},
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: "global",
|
||||
AuthToken: s.RootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var resp structs.ACLTokenUpsertResponse
|
||||
err := s.Agent.RPC("ACL.UpsertTokens", &args, &resp)
|
||||
require.NoError(t, err)
|
||||
aclID := resp.Tokens[0].AccessorID
|
||||
aclSecret := resp.Tokens[0].SecretID
|
||||
|
||||
// Make a HTTP request to get a one-time token
|
||||
|
||||
req, err := http.NewRequest("POST", "/v1/acl/token/onetime", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("X-Nomad-Token", aclSecret)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
obj, err := s.Server.UpsertOneTimeToken(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, obj)
|
||||
|
||||
ott := obj.(structs.OneTimeTokenUpsertResponse)
|
||||
require.Equal(t, aclID, ott.OneTimeToken.AccessorID)
|
||||
require.NotEqual(t, "", ott.OneTimeToken.OneTimeSecretID)
|
||||
|
||||
// Make a HTTP request to exchange that token
|
||||
|
||||
buf := encodeReq(structs.OneTimeTokenExchangeRequest{
|
||||
OneTimeSecretID: ott.OneTimeToken.OneTimeSecretID})
|
||||
req, err = http.NewRequest("POST", "/v1/acl/token/onetime/exchange", buf)
|
||||
respW = httptest.NewRecorder()
|
||||
|
||||
obj, err = s.Server.ExchangeOneTimeToken(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, obj)
|
||||
|
||||
token := obj.(structs.OneTimeTokenExchangeResponse)
|
||||
require.Equal(t, aclID, token.Token.AccessorID)
|
||||
require.Equal(t, aclSecret, token.Token.SecretID)
|
||||
|
||||
// Making the same request a second time should return an error
|
||||
|
||||
buf = encodeReq(structs.OneTimeTokenExchangeRequest{
|
||||
OneTimeSecretID: ott.OneTimeToken.OneTimeSecretID})
|
||||
req, err = http.NewRequest("POST", "/v1/acl/token/onetime/exchange", buf)
|
||||
respW = httptest.NewRecorder()
|
||||
|
||||
obj, err = s.Server.ExchangeOneTimeToken(respW, req)
|
||||
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -272,6 +272,8 @@ 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/token/onetime", s.wrap(s.UpsertOneTimeToken))
|
||||
s.mux.HandleFunc("/v1/acl/token/onetime/exchange", s.wrap(s.ExchangeOneTimeToken))
|
||||
s.mux.HandleFunc("/v1/acl/bootstrap", s.wrap(s.ACLTokenBootstrap))
|
||||
s.mux.HandleFunc("/v1/acl/tokens", s.wrap(s.ACLTokensRequest))
|
||||
s.mux.HandleFunc("/v1/acl/token", s.wrap(s.ACLTokenSpecificRequest))
|
||||
|
|
|
@ -154,6 +154,36 @@ func (a *ACLTokens) Self(q *QueryOptions) (*ACLToken, *QueryMeta, error) {
|
|||
return &resp, wm, nil
|
||||
}
|
||||
|
||||
// UpsertOneTimeToken is used to create a one-time token
|
||||
func (a *ACLTokens) UpsertOneTimeToken(q *WriteOptions) (*OneTimeToken, *WriteMeta, error) {
|
||||
var resp *OneTimeTokenUpsertResponse
|
||||
wm, err := a.client.write("/v1/acl/token/onetime", nil, &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, nil, fmt.Errorf("no one-time token returned")
|
||||
}
|
||||
return resp.OneTimeToken, wm, nil
|
||||
}
|
||||
|
||||
// ExchangeOneTimeToken is used to create a one-time token
|
||||
func (a *ACLTokens) ExchangeOneTimeToken(secret string, q *WriteOptions) (*ACLToken, *WriteMeta, error) {
|
||||
if secret == "" {
|
||||
return nil, nil, fmt.Errorf("missing secret ID")
|
||||
}
|
||||
req := &OneTimeTokenExchangeRequest{OneTimeSecretID: secret}
|
||||
var resp *OneTimeTokenExchangeResponse
|
||||
wm, err := a.client.write("/v1/acl/token/onetime/exchange", req, &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, nil, fmt.Errorf("no ACL token returned")
|
||||
}
|
||||
return resp.Token, wm, nil
|
||||
}
|
||||
|
||||
// ACLPolicyListStub is used to for listing ACL policies
|
||||
type ACLPolicyListStub struct {
|
||||
Name string
|
||||
|
@ -194,3 +224,23 @@ type ACLTokenListStub struct {
|
|||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
type OneTimeToken struct {
|
||||
OneTimeSecretID string
|
||||
AccessorID string
|
||||
ExpiresAt time.Time
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
type OneTimeTokenUpsertResponse struct {
|
||||
OneTimeToken *OneTimeToken
|
||||
}
|
||||
|
||||
type OneTimeTokenExchangeRequest struct {
|
||||
OneTimeSecretID string
|
||||
}
|
||||
|
||||
type OneTimeTokenExchangeResponse struct {
|
||||
Token *ACLToken
|
||||
}
|
||||
|
|
|
@ -342,3 +342,94 @@ $ curl \
|
|||
--request DELETE \
|
||||
https://localhost:4646/v1/acl/token/aa534e09-6a07-0a45-2295-a7f77063d429
|
||||
```
|
||||
|
||||
## Upsert One-Time Token
|
||||
|
||||
This endpoint creates a one-time token for the ACL token provided in the
|
||||
`X-Nomad-Token` header. Returns 403 if the token header is not set.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| -------- | ------------------------- | -------------- |
|
||||
| `POST` | `/acl/token/onetime` | `application/json` |
|
||||
|
||||
The table below shows this endpoint's support for
|
||||
[blocking queries](/api-docs#blocking-queries) and
|
||||
[required ACLs](/api-docs#acls).
|
||||
|
||||
| Blocking Queries | ACL Required |
|
||||
| ---------------- | ------------ |
|
||||
| `NO` | `any` |
|
||||
|
||||
|
||||
### Sample Request
|
||||
|
||||
```shell-session
|
||||
$ curl \
|
||||
--request POST \
|
||||
-H "X-Nomad-Token: aa534e09-6a07-0a45-2295-a7f77063d429" \
|
||||
https://localhost:4646/v1/acl/token/onetime
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"Index": 15,
|
||||
"OneTimeToken": {
|
||||
"AccessorID": "b780e702-98ce-521f-2e5f-c6b87de05b24",
|
||||
"CreateIndex": 7,
|
||||
"ExpiresAt": "2017-08-23T22:47:14.695408057Z",
|
||||
"ModifyIndex": 7,
|
||||
"OneTimeSecretID": "3f4a0fcd-7c42-773c-25db-2d31ba0c05fe"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Exchange One-Time Token
|
||||
|
||||
This endpoint exchanges a one-time token for the original ACL token used to
|
||||
create it.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| -------- | ------------------------- | -------------- |
|
||||
| `POST` | `/acl/token/onetime/exchange` | `application/json` |
|
||||
|
||||
The table below shows this endpoint's support for
|
||||
[blocking queries](/api-docs#blocking-queries) and
|
||||
[required ACLs](/api-docs#acls).
|
||||
|
||||
| Blocking Queries | ACL Required |
|
||||
| ---------------- | ------------ |
|
||||
| `NO` | `any` |
|
||||
|
||||
|
||||
### Sample Request
|
||||
|
||||
```shell-session
|
||||
$ curl \
|
||||
--request POST \
|
||||
-d '{ "OneTimeSecretID": "aa534e09-6a07-0a45-2295-a7f77063d429" } \
|
||||
https://localhost:4646/v1/acl/token/onetime/exchange
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"Index": 17,
|
||||
"Token": {
|
||||
"AccessorID": "b780e702-98ce-521f-2e5f-c6b87de05b24",
|
||||
"CreateIndex": 7,
|
||||
"CreateTime": "2017-08-23T22:47:14.695408057Z",
|
||||
"Global": true,
|
||||
"Hash": "UhZESkSFGFfX7eBgq5Uwph30OctbUbpe8+dlH2i4whA=",
|
||||
"ModifyIndex": 7,
|
||||
"Name": "Developer token",
|
||||
"Policies": [
|
||||
"developer"
|
||||
],
|
||||
"SecretID": "3f4a0fcd-7c42-773c-25db-2d31ba0c05fe",
|
||||
"Type": "client"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue