Merge pull request #3384 from hashicorp/f-self-policies
Ability to introspect self token
This commit is contained in:
commit
c3f06b2134
10
api/acl.go
10
api/acl.go
|
@ -144,6 +144,16 @@ func (a *ACLTokens) Info(accessorID string, q *QueryOptions) (*ACLToken, *QueryM
|
|||
return &resp, wm, nil
|
||||
}
|
||||
|
||||
// Self is used to query our own token
|
||||
func (a *ACLTokens) Self(q *QueryOptions) (*ACLToken, *QueryMeta, error) {
|
||||
var resp ACLToken
|
||||
wm, err := a.client.query("/v1/acl/token/self", &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &resp, wm, nil
|
||||
}
|
||||
|
||||
// ACLPolicyListStub is used to for listing ACL policies
|
||||
type ACLPolicyListStub struct {
|
||||
Name string
|
||||
|
|
|
@ -182,6 +182,36 @@ func TestACLTokens_Info(t *testing.T) {
|
|||
assert.Equal(t, out, out2)
|
||||
}
|
||||
|
||||
func TestACLTokens_Self(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 token
|
||||
out, wm, err := at.Create(token, nil)
|
||||
assert.Nil(t, err)
|
||||
assertWriteMeta(t, wm)
|
||||
assert.NotNil(t, out)
|
||||
|
||||
// Set the clients token to the new token
|
||||
c.SetSecretID(out.SecretID)
|
||||
at = c.ACLTokens()
|
||||
|
||||
// Query the token
|
||||
out2, qm, err := at.Self(nil)
|
||||
if assert.Nil(t, err) {
|
||||
assertQueryMeta(t, qm)
|
||||
assert.Equal(t, out, out2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLTokens_Delete(t *testing.T) {
|
||||
t.Parallel()
|
||||
c, s, _ := makeACLClient(t, nil, nil)
|
||||
|
|
|
@ -154,29 +154,35 @@ func (s *HTTPServer) ACLTokenBootstrap(resp http.ResponseWriter, req *http.Reque
|
|||
}
|
||||
|
||||
func (s *HTTPServer) ACLTokenSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
accessor := strings.TrimPrefix(req.URL.Path, "/v1/acl/token")
|
||||
path := req.URL.Path
|
||||
|
||||
// If there is no accessor, this must be a create
|
||||
if len(accessor) == 0 {
|
||||
switch path {
|
||||
case "/v1/acl/token":
|
||||
if !(req.Method == "PUT" || req.Method == "POST") {
|
||||
return nil, CodedError(405, ErrInvalidMethod)
|
||||
}
|
||||
return s.aclTokenUpdate(resp, req, "")
|
||||
case "/v1/acl/token/self":
|
||||
return s.aclTokenSelf(resp, req)
|
||||
}
|
||||
|
||||
// Check if no accessor is given past the slash
|
||||
accessor = accessor[1:]
|
||||
if accessor == "" {
|
||||
accessor := strings.TrimPrefix(path, "/v1/acl/token/")
|
||||
return s.aclTokenCrud(resp, req, accessor)
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclTokenCrud(resp http.ResponseWriter, req *http.Request,
|
||||
tokenAccessor string) (interface{}, error) {
|
||||
if tokenAccessor == "" {
|
||||
return nil, CodedError(400, "Missing Token Accessor")
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
return s.aclTokenQuery(resp, req, accessor)
|
||||
return s.aclTokenQuery(resp, req, tokenAccessor)
|
||||
case "PUT", "POST":
|
||||
return s.aclTokenUpdate(resp, req, accessor)
|
||||
return s.aclTokenUpdate(resp, req, tokenAccessor)
|
||||
case "DELETE":
|
||||
return s.aclTokenDelete(resp, req, accessor)
|
||||
return s.aclTokenDelete(resp, req, tokenAccessor)
|
||||
default:
|
||||
return nil, CodedError(405, ErrInvalidMethod)
|
||||
}
|
||||
|
@ -203,6 +209,29 @@ func (s *HTTPServer) aclTokenQuery(resp http.ResponseWriter, req *http.Request,
|
|||
return out.Token, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclTokenSelf(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
if req.Method != "GET" {
|
||||
return nil, CodedError(405, ErrInvalidMethod)
|
||||
}
|
||||
args := structs.ResolveACLTokenRequest{}
|
||||
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
args.SecretID = args.AuthToken
|
||||
|
||||
var out structs.ResolveACLTokenResponse
|
||||
if err := s.agent.RPC("ACL.ResolveToken", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setMeta(resp, &out.QueryMeta)
|
||||
if out.Token == nil {
|
||||
return nil, CodedError(404, "ACL token not found")
|
||||
}
|
||||
return out.Token, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclTokenUpdate(resp http.ResponseWriter, req *http.Request,
|
||||
tokenAccessor string) (interface{}, error) {
|
||||
// Parse the token
|
||||
|
|
|
@ -322,6 +322,55 @@ func TestHTTP_ACLTokenQuery(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestHTTP_ACLTokenSelf(t *testing.T) {
|
||||
t.Parallel()
|
||||
httpACLTest(t, nil, func(s *TestAgent) {
|
||||
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
|
||||
if err := s.Agent.RPC("ACL.UpsertTokens", &args, &resp); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
out := resp.Tokens[0]
|
||||
|
||||
// Make the HTTP request
|
||||
req, err := http.NewRequest("GET", "/v1/acl/token/self", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
respW := httptest.NewRecorder()
|
||||
setToken(req, out)
|
||||
|
||||
// Make the request
|
||||
obj, err := s.Server.ACLTokenSpecificRequest(respW, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Check for the index
|
||||
if respW.HeaderMap.Get("X-Nomad-Index") == "" {
|
||||
t.Fatalf("missing index")
|
||||
}
|
||||
if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" {
|
||||
t.Fatalf("missing known leader")
|
||||
}
|
||||
if respW.HeaderMap.Get("X-Nomad-LastContact") == "" {
|
||||
t.Fatalf("missing last contact")
|
||||
}
|
||||
|
||||
// Check the output
|
||||
n := obj.(*structs.ACLToken)
|
||||
assert.Equal(t, out, n)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTP_ACLTokenCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
httpACLTest(t, nil, func(s *TestAgent) {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
namespace "default" {
|
||||
policy = "write"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
node {
|
||||
policy = "read"
|
||||
}
|
|
@ -121,12 +121,36 @@ func (a *ACL) ListPolicies(args *structs.ACLPolicyListRequest, reply *structs.AC
|
|||
defer metrics.MeasureSince([]string{"nomad", "acl", "list_policies"}, time.Now())
|
||||
|
||||
// Check management level permissions
|
||||
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
|
||||
acl, err := a.srv.ResolveToken(args.AuthToken)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if acl == nil || !acl.IsManagement() {
|
||||
} else if acl == nil {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// If it is not a management token determine the policies that may be listed
|
||||
mgt := acl.IsManagement()
|
||||
var policies map[string]struct{}
|
||||
if !mgt {
|
||||
snap, err := a.srv.fsm.State().Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := snap.ACLTokenBySecretID(nil, args.AuthToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if token == nil {
|
||||
return structs.ErrTokenNotFound
|
||||
}
|
||||
|
||||
policies = make(map[string]struct{}, len(token.Policies))
|
||||
for _, p := range token.Policies {
|
||||
policies[p] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the blocking query
|
||||
opts := blockingOptions{
|
||||
queryOpts: &args.QueryOptions,
|
||||
|
@ -152,7 +176,9 @@ func (a *ACL) ListPolicies(args *structs.ACLPolicyListRequest, reply *structs.AC
|
|||
break
|
||||
}
|
||||
policy := raw.(*structs.ACLPolicy)
|
||||
reply.Policies = append(reply.Policies, policy.Stub())
|
||||
if _, ok := policies[policy.Name]; ok || mgt {
|
||||
reply.Policies = append(reply.Policies, policy.Stub())
|
||||
}
|
||||
}
|
||||
|
||||
// Use the last index that affected the policy table
|
||||
|
@ -183,12 +209,42 @@ func (a *ACL) GetPolicy(args *structs.ACLPolicySpecificRequest, reply *structs.S
|
|||
defer metrics.MeasureSince([]string{"nomad", "acl", "get_policy"}, time.Now())
|
||||
|
||||
// Check management level permissions
|
||||
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
|
||||
acl, err := a.srv.ResolveToken(args.AuthToken)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if acl == nil || !acl.IsManagement() {
|
||||
} else if acl == nil {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// If it is not a management token determine if it can get this policy
|
||||
mgt := acl.IsManagement()
|
||||
if !mgt {
|
||||
snap, err := a.srv.fsm.State().Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := snap.ACLTokenBySecretID(nil, args.AuthToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if token == nil {
|
||||
return structs.ErrTokenNotFound
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, p := range token.Policies {
|
||||
if p == args.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the blocking query
|
||||
opts := blockingOptions{
|
||||
queryOpts: &args.QueryOptions,
|
||||
|
|
|
@ -28,6 +28,11 @@ func TestACLEndpoint_GetPolicy(t *testing.T) {
|
|||
policy := mock.ACLPolicy()
|
||||
s1.fsm.State().UpsertACLPolicies(1000, []*structs.ACLPolicy{policy})
|
||||
|
||||
// Create a token with one the policy
|
||||
token := mock.ACLToken()
|
||||
token.Policies = []string{policy.Name}
|
||||
s1.fsm.State().UpsertACLTokens(1001, []*structs.ACLToken{token})
|
||||
|
||||
// Lookup the policy
|
||||
get := &structs.ACLPolicySpecificRequest{
|
||||
Name: policy.Name,
|
||||
|
@ -50,6 +55,21 @@ func TestACLEndpoint_GetPolicy(t *testing.T) {
|
|||
}
|
||||
assert.Equal(t, uint64(1000), resp.Index)
|
||||
assert.Nil(t, resp.Policy)
|
||||
|
||||
// Lookup the policy with the token
|
||||
get = &structs.ACLPolicySpecificRequest{
|
||||
Name: policy.Name,
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: "global",
|
||||
AuthToken: token.SecretID,
|
||||
},
|
||||
}
|
||||
var resp2 structs.SingleACLPolicyResponse
|
||||
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", get, &resp2); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
assert.EqualValues(t, 1000, resp2.Index)
|
||||
assert.Equal(t, policy, resp2.Policy)
|
||||
}
|
||||
|
||||
func TestACLEndpoint_GetPolicy_Blocking(t *testing.T) {
|
||||
|
@ -290,6 +310,7 @@ func TestACLEndpoint_GetPolicies_Blocking(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestACLEndpoint_ListPolicies(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
t.Parallel()
|
||||
s1, root := testACLServer(t, nil)
|
||||
defer s1.Shutdown()
|
||||
|
@ -304,6 +325,11 @@ func TestACLEndpoint_ListPolicies(t *testing.T) {
|
|||
p2.Name = "aaaabbbb-3350-4b4b-d185-0e1992ed43e9"
|
||||
s1.fsm.State().UpsertACLPolicies(1000, []*structs.ACLPolicy{p1, p2})
|
||||
|
||||
// Create a token with one of those policies
|
||||
token := mock.ACLToken()
|
||||
token.Policies = []string{p1.Name}
|
||||
s1.fsm.State().UpsertACLTokens(1001, []*structs.ACLToken{token})
|
||||
|
||||
// Lookup the policies
|
||||
get := &structs.ACLPolicyListRequest{
|
||||
QueryOptions: structs.QueryOptions{
|
||||
|
@ -315,8 +341,8 @@ func TestACLEndpoint_ListPolicies(t *testing.T) {
|
|||
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", get, &resp); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
assert.Equal(t, uint64(1000), resp.Index)
|
||||
assert.Equal(t, 2, len(resp.Policies))
|
||||
assert.EqualValues(1000, resp.Index)
|
||||
assert.Len(resp.Policies, 2)
|
||||
|
||||
// Lookup the policies by prefix
|
||||
get = &structs.ACLPolicyListRequest{
|
||||
|
@ -330,8 +356,24 @@ func TestACLEndpoint_ListPolicies(t *testing.T) {
|
|||
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", get, &resp2); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
assert.Equal(t, uint64(1000), resp2.Index)
|
||||
assert.Equal(t, 1, len(resp2.Policies))
|
||||
assert.EqualValues(1000, resp2.Index)
|
||||
assert.Len(resp2.Policies, 1)
|
||||
|
||||
// List policies using the created token
|
||||
get = &structs.ACLPolicyListRequest{
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: "global",
|
||||
AuthToken: token.SecretID,
|
||||
},
|
||||
}
|
||||
var resp3 structs.ACLPolicyListResponse
|
||||
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", get, &resp3); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
assert.EqualValues(1000, resp3.Index)
|
||||
if assert.Len(resp3.Policies, 1) {
|
||||
assert.Equal(resp3.Policies[0].Name, p1.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLEndpoint_ListPolicies_Blocking(t *testing.T) {
|
||||
|
|
|
@ -26,7 +26,7 @@ The table below shows this endpoint's support for
|
|||
|
||||
| Blocking Queries | Consistency Modes | ACL Required |
|
||||
| ---------------- | ----------------- | ------------ |
|
||||
| `YES` | `all` | `management` |
|
||||
| `YES` | `all` | `management` for all policies.<br>Output when given a non-management token will be limited to the policies on the token itself |
|
||||
|
||||
|
||||
### Sample Request
|
||||
|
@ -110,7 +110,7 @@ The table below shows this endpoint's support for
|
|||
|
||||
| Blocking Queries | Consistency Modes | ACL Required |
|
||||
| ---------------- | ----------------- | ------------ |
|
||||
| `YES` | `all` | `management` |
|
||||
| `YES` | `all` | `management` or token with access to policy |
|
||||
|
||||
### Sample Request
|
||||
|
||||
|
|
|
@ -268,6 +268,49 @@ $ curl \
|
|||
}
|
||||
```
|
||||
|
||||
## Read Self Token
|
||||
|
||||
This endpoint reads the ACL token given by the passed SecretID. If the token is a global token
|
||||
which has been replicated to the region it may lag behind the authoritative region.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| ------ | ---------------------------- | -------------------------- |
|
||||
| `GET` | `/acl/token/self` | `application/json` |
|
||||
|
||||
The table below shows this endpoint's support for
|
||||
[blocking queries](/api/index.html#blocking-queries), [consistency modes](/api/index.html#consistency-modes) and
|
||||
[required ACLs](/api/index.html#acls).
|
||||
|
||||
| Blocking Queries | Consistency Modes | ACL Required |
|
||||
| ---------------- | ----------------- | ------------ |
|
||||
| `YES` | `all` | Any valid ACL token |
|
||||
|
||||
### Sample Request
|
||||
|
||||
```text
|
||||
$ curl \
|
||||
--header "X-Nomad-Token: 8176afd3-772d-0b71-8f85-7fa5d903e9d4"
|
||||
https://nomad.rocks/v1/acl/token/self
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"AccessorID": "aa534e09-6a07-0a45-2295-a7f77063d429",
|
||||
"SecretID": "8176afd3-772d-0b71-8f85-7fa5d903e9d4",
|
||||
"Name": "Read-write token",
|
||||
"Type": "client",
|
||||
"Policies": [
|
||||
"readwrite"
|
||||
],
|
||||
"Global": false,
|
||||
"CreateTime": "2017-08-23T23:25:41.429154233Z",
|
||||
"CreateIndex": 52,
|
||||
"ModifyIndex": 64
|
||||
}
|
||||
```
|
||||
|
||||
## Delete Token
|
||||
|
||||
This endpoint deletes the ACL token by accessor. This request is forwarded to the
|
||||
|
|
Loading…
Reference in New Issue