[NET-5399] Add support for querying tokens by service name. (#18689)
Add support for querying tokens by service name. (#18667) Add support for querying tokens by service name The consul-k8s endpoints controller has a workflow where it fetches all tokens. This is not performant for large clusters, where there may be a sizable number of tokens. This commit attempts to alleviate that problem and introduces a new way to query by the token's service name.
This commit is contained in:
parent
35bf77c387
commit
b06927c0e3
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
api: Add support for listing ACL tokens by service name.
|
||||
```
|
|
@ -294,6 +294,7 @@ func (s *HTTPHandlers) ACLTokenList(resp http.ResponseWriter, req *http.Request)
|
|||
args.Policy = req.URL.Query().Get("policy")
|
||||
args.Role = req.URL.Query().Get("role")
|
||||
args.AuthMethod = req.URL.Query().Get("authmethod")
|
||||
args.ServiceName = req.URL.Query().Get("servicename")
|
||||
if err := parseACLAuthMethodEnterpriseMeta(req, &args.ACLAuthMethodEnterpriseMeta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1328,6 +1328,38 @@ func TestACL_HTTP(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
testutil.RequireErrorContains(t, err, "Only lowercase alphanumeric")
|
||||
})
|
||||
|
||||
t.Run("Create with valid service identity", func(t *testing.T) {
|
||||
tokenInput := &structs.ACLToken{
|
||||
Description: "token for service identity sn1",
|
||||
ServiceIdentities: []*structs.ACLServiceIdentity{
|
||||
{
|
||||
ServiceName: "sn1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
|
||||
req.Header.Add("X-Consul-Token", "root")
|
||||
resp := httptest.NewRecorder()
|
||||
_, err := a.srv.ACLTokenCreate(resp, req)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("List by ServiceName", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/v1/acl/tokens?servicename=sn1", nil)
|
||||
req.Header.Add("X-Consul-Token", "root")
|
||||
resp := httptest.NewRecorder()
|
||||
raw, err := a.srv.ACLTokenList(resp, req)
|
||||
require.NoError(t, err)
|
||||
tokens, ok := raw.(structs.ACLTokenListStubs)
|
||||
require.True(t, ok)
|
||||
require.Len(t, tokens, 1)
|
||||
token := tokens[0]
|
||||
require.Equal(t, "token for service identity sn1", token.Description)
|
||||
require.Len(t, token.ServiceIdentities, 1)
|
||||
require.Equal(t, "sn1", token.ServiceIdentities[0].ServiceName)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -680,8 +680,18 @@ func (a *ACL) TokenList(args *structs.ACLTokenListRequest, reply *structs.ACLTok
|
|||
}
|
||||
|
||||
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
|
||||
func(ws memdb.WatchSet, state *state.Store) error {
|
||||
index, tokens, err := state.ACLTokenList(ws, args.IncludeLocal, args.IncludeGlobal, args.Policy, args.Role, args.AuthMethod, methodMeta, &args.EnterpriseMeta)
|
||||
func(ws memdb.WatchSet, s *state.Store) error {
|
||||
index, tokens, err := s.ACLTokenListWithParameters(ws, state.ACLTokenListParameters{
|
||||
Local: args.IncludeLocal,
|
||||
Global: args.IncludeGlobal,
|
||||
Policy: args.Policy,
|
||||
Role: args.Role,
|
||||
MethodName: args.AuthMethod,
|
||||
ServiceName: args.ServiceName,
|
||||
MethodMeta: methodMeta,
|
||||
EnterpriseMeta: &args.EnterpriseMeta,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -378,8 +378,10 @@ func TestACLReplication_Tokens(t *testing.T) {
|
|||
|
||||
checkSame := func(t *retry.R) {
|
||||
// only account for global tokens - local tokens shouldn't be replicated
|
||||
// nolint:staticcheck
|
||||
index, remote, err := s1.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
// nolint:staticcheck
|
||||
_, local, err := s2.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -483,6 +485,7 @@ func TestACLReplication_Tokens(t *testing.T) {
|
|||
})
|
||||
|
||||
// verify dc2 local tokens didn't get blown away
|
||||
// nolint:staticcheck
|
||||
_, local, err := s2.fsm.State().ACLTokenList(nil, true, false, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, local, 50)
|
||||
|
@ -821,9 +824,11 @@ func TestACLReplication_AllTypes(t *testing.T) {
|
|||
|
||||
checkSameTokens := func(t *retry.R) {
|
||||
// only account for global tokens - local tokens shouldn't be replicated
|
||||
// nolint:staticcheck
|
||||
index, remote, err := s1.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
// Query for all of them, so that we can prove that no globals snuck in.
|
||||
// nolint:staticcheck
|
||||
_, local, err := s2.fsm.State().ACLTokenList(nil, true, true, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ func (r *aclTokenReplicator) FetchRemote(srv *Server, lastRemoteIndex uint64) (i
|
|||
func (r *aclTokenReplicator) FetchLocal(srv *Server) (int, uint64, error) {
|
||||
r.local = nil
|
||||
|
||||
// nolint:staticcheck
|
||||
idx, local, err := srv.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, srv.replicationEnterpriseMeta())
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
|
|
|
@ -623,8 +623,35 @@ func aclTokenGetTxn(tx ReadTxn, ws memdb.WatchSet, value, index string, entMeta
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
type ACLTokenListParameters struct {
|
||||
Local bool
|
||||
Global bool
|
||||
Policy string
|
||||
Role string
|
||||
ServiceName string
|
||||
MethodName string
|
||||
MethodMeta *acl.EnterpriseMeta
|
||||
EnterpriseMeta *acl.EnterpriseMeta
|
||||
}
|
||||
|
||||
// ACLTokenList return a list of ACL Tokens that match the policy, role, and method.
|
||||
// This function should be treated as deprecated, and ACLTokenListWithParameters should be preferred.
|
||||
//
|
||||
// Deprecated: use ACLTokenListWithParameters
|
||||
func (s *Store) ACLTokenList(ws memdb.WatchSet, local, global bool, policy, role, methodName string, methodMeta, entMeta *acl.EnterpriseMeta) (uint64, structs.ACLTokens, error) {
|
||||
return s.ACLTokenListWithParameters(ws, ACLTokenListParameters{
|
||||
Local: local,
|
||||
Global: global,
|
||||
Policy: policy,
|
||||
Role: role,
|
||||
MethodName: methodName,
|
||||
MethodMeta: methodMeta,
|
||||
EnterpriseMeta: entMeta,
|
||||
})
|
||||
}
|
||||
|
||||
// ACLTokenListWithParameters returns a list of ACL Tokens that match the provided parameters.
|
||||
func (s *Store) ACLTokenListWithParameters(ws memdb.WatchSet, params ACLTokenListParameters) (uint64, structs.ACLTokens, error) {
|
||||
tx := s.db.Txn(false)
|
||||
defer tx.Abort()
|
||||
|
||||
|
@ -637,43 +664,51 @@ func (s *Store) ACLTokenList(ws memdb.WatchSet, local, global bool, policy, role
|
|||
|
||||
needLocalityFilter := false
|
||||
|
||||
if policy == "" && role == "" && methodName == "" {
|
||||
if global == local {
|
||||
iter, err = aclTokenListAll(tx, entMeta)
|
||||
if params.Policy == "" && params.Role == "" && params.MethodName == "" && params.ServiceName == "" {
|
||||
if params.Global == params.Local {
|
||||
iter, err = aclTokenListAll(tx, params.EnterpriseMeta)
|
||||
} else {
|
||||
iter, err = aclTokenList(tx, entMeta, local)
|
||||
iter, err = aclTokenList(tx, params.EnterpriseMeta, params.Local)
|
||||
}
|
||||
|
||||
} else if policy != "" && role == "" && methodName == "" {
|
||||
iter, err = aclTokenListByPolicy(tx, policy, entMeta)
|
||||
} else if params.Policy != "" && params.Role == "" && params.MethodName == "" && params.ServiceName == "" {
|
||||
// Find by policy
|
||||
iter, err = aclTokenListByPolicy(tx, params.Policy, params.EnterpriseMeta)
|
||||
needLocalityFilter = true
|
||||
|
||||
} else if policy == "" && role != "" && methodName == "" {
|
||||
iter, err = aclTokenListByRole(tx, role, entMeta)
|
||||
} else if params.Policy == "" && params.Role != "" && params.MethodName == "" && params.ServiceName == "" {
|
||||
// Find by role
|
||||
iter, err = aclTokenListByRole(tx, params.Role, params.EnterpriseMeta)
|
||||
needLocalityFilter = true
|
||||
|
||||
} else if policy == "" && role == "" && methodName != "" {
|
||||
iter, err = aclTokenListByAuthMethod(tx, methodName, methodMeta, entMeta)
|
||||
} else if params.Policy == "" && params.Role == "" && params.MethodName != "" && params.ServiceName == "" {
|
||||
// Find by methodName
|
||||
iter, err = aclTokenListByAuthMethod(tx, params.MethodName, params.MethodMeta, params.EnterpriseMeta)
|
||||
needLocalityFilter = true
|
||||
|
||||
} else if params.Policy == "" && params.Role == "" && params.MethodName == "" && params.ServiceName != "" {
|
||||
// Find by the service identity's serviceName
|
||||
iter, err = aclTokenListByServiceName(tx, params.ServiceName, params.EnterpriseMeta)
|
||||
needLocalityFilter = true
|
||||
|
||||
} else {
|
||||
return 0, nil, fmt.Errorf("can only filter by one of policy, role, or methodName at a time")
|
||||
return 0, nil, fmt.Errorf("can only filter by one of policy, role, serviceName, or methodName at a time")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("failed acl token lookup: %v", err)
|
||||
}
|
||||
|
||||
if needLocalityFilter && global != local {
|
||||
if needLocalityFilter && params.Global != params.Local {
|
||||
iter = memdb.NewFilterIterator(iter, func(raw interface{}) bool {
|
||||
token, ok := raw.(*structs.ACLToken)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if global && !token.Local {
|
||||
if params.Global && !token.Local {
|
||||
return false
|
||||
} else if local && token.Local {
|
||||
} else if params.Local && token.Local {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -698,7 +733,7 @@ func (s *Store) ACLTokenList(ws memdb.WatchSet, local, global bool, policy, role
|
|||
}
|
||||
|
||||
// Get the table index.
|
||||
idx := aclTokenMaxIndex(tx, nil, entMeta)
|
||||
idx := aclTokenMaxIndex(tx, nil, params.EnterpriseMeta)
|
||||
return idx, result, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -76,6 +76,10 @@ func aclTokenListByAuthMethod(tx ReadTxn, authMethod string, _, _ *acl.Enterpris
|
|||
return tx.Get(tableACLTokens, indexAuthMethod, AuthMethodQuery{Value: authMethod})
|
||||
}
|
||||
|
||||
func aclTokenListByServiceName(tx ReadTxn, serviceName string, entMeta *acl.EnterpriseMeta) (memdb.ResultIterator, error) {
|
||||
return tx.Get(tableACLTokens, indexServiceName, Query{Value: serviceName})
|
||||
}
|
||||
|
||||
func aclTokenDeleteWithToken(tx WriteTxn, token *structs.ACLToken, idx uint64) error {
|
||||
// remove the token
|
||||
if err := tx.Delete(tableACLTokens, token); err != nil {
|
||||
|
|
|
@ -22,6 +22,7 @@ const (
|
|||
indexAccessor = "accessor"
|
||||
indexPolicies = "policies"
|
||||
indexRoles = "roles"
|
||||
indexServiceName = "service-name"
|
||||
indexAuthMethod = "authmethod"
|
||||
indexLocality = "locality"
|
||||
indexName = "name"
|
||||
|
@ -106,6 +107,15 @@ func tokensTableSchema() *memdb.TableSchema {
|
|||
writeIndex: indexExpiresLocalFromACLToken,
|
||||
},
|
||||
},
|
||||
indexServiceName: {
|
||||
Name: indexServiceName,
|
||||
AllowMissing: true,
|
||||
Unique: false,
|
||||
Indexer: indexerMulti[Query, *structs.ACLToken]{
|
||||
readIndex: indexFromQuery,
|
||||
writeIndexMulti: indexServiceNameFromACLToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -398,6 +408,21 @@ func indexExpiresFromACLToken(t *structs.ACLToken, local bool) ([]byte, error) {
|
|||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func indexServiceNameFromACLToken(token *structs.ACLToken) ([][]byte, error) {
|
||||
vals := make([][]byte, 0, len(token.ServiceIdentities))
|
||||
for _, id := range token.ServiceIdentities {
|
||||
if id != nil && id.ServiceName != "" {
|
||||
var b indexBuilder
|
||||
b.String(strings.ToLower(id.ServiceName))
|
||||
vals = append(vals, b.Bytes())
|
||||
}
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
return nil, errMissingValueForIndex
|
||||
}
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func authMethodsTableSchema() *memdb.TableSchema {
|
||||
return &memdb.TableSchema{
|
||||
Name: tableACLAuthMethods,
|
||||
|
|
|
@ -214,6 +214,7 @@ func TestStateStore_ACLBootstrap(t *testing.T) {
|
|||
require.Equal(t, uint64(3), index)
|
||||
|
||||
// Make sure the ACLs are in an expected state.
|
||||
// nolint:staticcheck
|
||||
_, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 1)
|
||||
|
@ -228,6 +229,7 @@ func TestStateStore_ACLBootstrap(t *testing.T) {
|
|||
err = s.ACLBootstrap(32, index, token2.Clone())
|
||||
require.NoError(t, err)
|
||||
|
||||
// nolint:staticcheck
|
||||
_, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 2)
|
||||
|
@ -849,6 +851,23 @@ func TestStateStore_ACLToken_List(t *testing.T) {
|
|||
AuthMethod: "test",
|
||||
Local: true,
|
||||
},
|
||||
// the serviceName specific token
|
||||
&structs.ACLToken{
|
||||
AccessorID: "80c900e1-2fc5-4685-ae29-1b2d17fc30e4",
|
||||
SecretID: "9d229cfd-ec4b-4d31-a6fd-ecbcb2a41d41",
|
||||
ServiceIdentities: []*structs.ACLServiceIdentity{
|
||||
{ServiceName: "sn1"},
|
||||
},
|
||||
},
|
||||
// the serviceName specific token and local
|
||||
&structs.ACLToken{
|
||||
AccessorID: "a14fa45e-0afe-4b44-961d-a430030ccfe2",
|
||||
SecretID: "17f696b9-448a-4bd3-936b-08c92c66530f",
|
||||
ServiceIdentities: []*structs.ACLServiceIdentity{
|
||||
{ServiceName: "sn1"},
|
||||
},
|
||||
Local: true,
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, s.ACLTokenBatchSet(2, tokens, ACLTokenSetOptions{}))
|
||||
|
@ -860,6 +879,7 @@ func TestStateStore_ACLToken_List(t *testing.T) {
|
|||
policy string
|
||||
role string
|
||||
methodName string
|
||||
serviceName string
|
||||
accessors []string
|
||||
}
|
||||
|
||||
|
@ -876,6 +896,7 @@ func TestStateStore_ACLToken_List(t *testing.T) {
|
|||
"47eea4da-bda1-48a6-901c-3e36d2d9262f", // policy + global
|
||||
"54866514-3cf2-4fec-8a8a-710583831834", // mgmt + global
|
||||
"74277ae1-6a9b-4035-b444-2370fe6a2cb5", // authMethod + global
|
||||
"80c900e1-2fc5-4685-ae29-1b2d17fc30e4", // serviceName + global
|
||||
"a7715fde-8954-4c92-afbc-d84c6ecdc582", // role + global
|
||||
},
|
||||
},
|
||||
|
@ -889,6 +910,7 @@ func TestStateStore_ACLToken_List(t *testing.T) {
|
|||
accessors: []string{
|
||||
"211f0360-ef53-41d3-9d4d-db84396eb6c0", // authMethod + local
|
||||
"4915fc9d-3726-4171-b588-6c271f45eecd", // policy + local
|
||||
"a14fa45e-0afe-4b44-961d-a430030ccfe2", // serviceName + local
|
||||
"cadb4f13-f62a-49ab-ab3f-5a7e01b925d9", // role + local
|
||||
"f1093997-b6c7-496d-bfb8-6b1b1895641b", // mgmt + local
|
||||
},
|
||||
|
@ -983,6 +1005,30 @@ func TestStateStore_ACLToken_List(t *testing.T) {
|
|||
"74277ae1-6a9b-4035-b444-2370fe6a2cb5", // authMethod + global
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ServiceName - Local",
|
||||
local: true,
|
||||
global: false,
|
||||
policy: "",
|
||||
role: "",
|
||||
methodName: "",
|
||||
serviceName: "sn1",
|
||||
accessors: []string{
|
||||
"a14fa45e-0afe-4b44-961d-a430030ccfe2", // serviceName + local
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ServiceName - Global",
|
||||
local: false,
|
||||
global: true,
|
||||
policy: "",
|
||||
role: "",
|
||||
methodName: "",
|
||||
serviceName: "sn1",
|
||||
accessors: []string{
|
||||
"80c900e1-2fc5-4685-ae29-1b2d17fc30e4", // serviceName + global
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "All",
|
||||
local: true,
|
||||
|
@ -997,6 +1043,8 @@ func TestStateStore_ACLToken_List(t *testing.T) {
|
|||
"4915fc9d-3726-4171-b588-6c271f45eecd", // policy + local
|
||||
"54866514-3cf2-4fec-8a8a-710583831834", // mgmt + global
|
||||
"74277ae1-6a9b-4035-b444-2370fe6a2cb5", // authMethod + global
|
||||
"80c900e1-2fc5-4685-ae29-1b2d17fc30e4", // serviceName + global
|
||||
"a14fa45e-0afe-4b44-961d-a430030ccfe2", // serviceName + local
|
||||
"a7715fde-8954-4c92-afbc-d84c6ecdc582", // role + global
|
||||
"cadb4f13-f62a-49ab-ab3f-5a7e01b925d9", // role + local
|
||||
"f1093997-b6c7-496d-bfb8-6b1b1895641b", // mgmt + local
|
||||
|
@ -1004,14 +1052,27 @@ func TestStateStore_ACLToken_List(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
for _, tc := range []struct{ policy, role, methodName string }{
|
||||
{testPolicyID_A, testRoleID_A, "test"},
|
||||
{"", testRoleID_A, "test"},
|
||||
{testPolicyID_A, "", "test"},
|
||||
{testPolicyID_A, testRoleID_A, ""},
|
||||
for _, tc := range []struct{ policy, role, methodName, serviceName string }{
|
||||
{testPolicyID_A, testRoleID_A, "test", ""},
|
||||
{"", testRoleID_A, "test", ""},
|
||||
{testPolicyID_A, "", "test", ""},
|
||||
{testPolicyID_A, testRoleID_A, "", ""},
|
||||
{testPolicyID_A, "", "", "test"},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("can't filter on more than one: %s/%s/%s", tc.policy, tc.role, tc.methodName), func(t *testing.T) {
|
||||
_, _, err := s.ACLTokenList(nil, false, false, tc.policy, tc.role, tc.methodName, nil, nil)
|
||||
t.Run(fmt.Sprintf("can't filter on more than one: %s/%s/%s/%s", tc.policy, tc.role, tc.methodName, tc.serviceName), func(t *testing.T) {
|
||||
var err error
|
||||
if tc.serviceName == "" {
|
||||
// The legacy call can only be tested when the serviceName is not specified
|
||||
// nolint:staticcheck
|
||||
_, _, err = s.ACLTokenList(nil, false, false, tc.policy, tc.role, tc.methodName, nil, nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
_, _, err = s.ACLTokenListWithParameters(nil, ACLTokenListParameters{
|
||||
Policy: tc.policy,
|
||||
Role: tc.role,
|
||||
MethodName: tc.methodName,
|
||||
ServiceName: tc.serviceName,
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
@ -1020,6 +1081,9 @@ func TestStateStore_ACLToken_List(t *testing.T) {
|
|||
tc := tc // capture range variable
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Test old function
|
||||
if tc.serviceName == "" {
|
||||
// nolint:staticcheck
|
||||
_, tokens, err := s.ACLTokenList(nil, tc.local, tc.global, tc.policy, tc.role, tc.methodName, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, len(tc.accessors))
|
||||
|
@ -1027,6 +1091,24 @@ func TestStateStore_ACLToken_List(t *testing.T) {
|
|||
for i, token := range tokens {
|
||||
require.Equal(t, tc.accessors[i], token.AccessorID)
|
||||
}
|
||||
}
|
||||
// Test new function
|
||||
{
|
||||
_, tokens, err := s.ACLTokenListWithParameters(nil, ACLTokenListParameters{
|
||||
Local: tc.local,
|
||||
Global: tc.global,
|
||||
Policy: tc.policy,
|
||||
Role: tc.role,
|
||||
ServiceName: tc.serviceName,
|
||||
MethodName: tc.methodName,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, len(tc.accessors))
|
||||
tokens.Sort()
|
||||
for i, token := range tokens {
|
||||
require.Equal(t, tc.accessors[i], token.AccessorID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1080,6 +1162,7 @@ func TestStateStore_ACLToken_FixupPolicyLinks(t *testing.T) {
|
|||
require.Equal(t, "node-read-renamed", retrieved.Policies[0].Name)
|
||||
|
||||
// list tokens without stale links
|
||||
// nolint:staticcheck
|
||||
_, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -1124,6 +1207,7 @@ func TestStateStore_ACLToken_FixupPolicyLinks(t *testing.T) {
|
|||
require.Len(t, retrieved.Policies, 0)
|
||||
|
||||
// list tokens without stale links
|
||||
// nolint:staticcheck
|
||||
_, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -1209,6 +1293,7 @@ func TestStateStore_ACLToken_FixupRoleLinks(t *testing.T) {
|
|||
require.Equal(t, "node-read-role-renamed", retrieved.Roles[0].Name)
|
||||
|
||||
// list tokens without stale links
|
||||
// nolint:staticcheck
|
||||
_, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -1253,6 +1338,7 @@ func TestStateStore_ACLToken_FixupRoleLinks(t *testing.T) {
|
|||
require.Len(t, retrieved.Roles, 0)
|
||||
|
||||
// list tokens without stale links
|
||||
// nolint:staticcheck
|
||||
_, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -2688,16 +2774,19 @@ func TestStateStore_ACLAuthMethod_GlobalNameShadowing_TokenTest(t *testing.T) {
|
|||
}
|
||||
|
||||
require.True(t, t.Run("list local only", func(t *testing.T) {
|
||||
// nolint:staticcheck
|
||||
_, got, err := s.ACLTokenList(nil, true, false, "", "", "test", defaultEntMeta, defaultEntMeta)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []string{methodDC2_tok1, methodDC2_tok2}, toList(got))
|
||||
}))
|
||||
require.True(t, t.Run("list global only", func(t *testing.T) {
|
||||
// nolint:staticcheck
|
||||
_, got, err := s.ACLTokenList(nil, false, true, "", "", "test", defaultEntMeta, defaultEntMeta)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []string{methodDC1_tok1, methodDC1_tok2}, toList(got))
|
||||
}))
|
||||
require.True(t, t.Run("list both", func(t *testing.T) {
|
||||
// nolint:staticcheck
|
||||
_, got, err := s.ACLTokenList(nil, true, true, "", "", "test", defaultEntMeta, defaultEntMeta)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []string{methodDC1_tok1, methodDC1_tok2, methodDC2_tok1, methodDC2_tok2}, toList(got))
|
||||
|
@ -2709,16 +2798,19 @@ func TestStateStore_ACLAuthMethod_GlobalNameShadowing_TokenTest(t *testing.T) {
|
|||
}))
|
||||
|
||||
require.True(t, t.Run("list local only (after dc2 delete)", func(t *testing.T) {
|
||||
// nolint:staticcheck
|
||||
_, got, err := s.ACLTokenList(nil, true, false, "", "", "test", defaultEntMeta, defaultEntMeta)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, got)
|
||||
}))
|
||||
require.True(t, t.Run("list global only (after dc2 delete)", func(t *testing.T) {
|
||||
// nolint:staticcheck
|
||||
_, got, err := s.ACLTokenList(nil, false, true, "", "", "test", defaultEntMeta, defaultEntMeta)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []string{methodDC1_tok1, methodDC1_tok2}, toList(got))
|
||||
}))
|
||||
require.True(t, t.Run("list both (after dc2 delete)", func(t *testing.T) {
|
||||
// nolint:staticcheck
|
||||
_, got, err := s.ACLTokenList(nil, true, true, "", "", "test", defaultEntMeta, defaultEntMeta)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []string{methodDC1_tok1, methodDC1_tok2}, toList(got))
|
||||
|
@ -3509,6 +3601,7 @@ func TestStateStore_ACLTokens_Snapshot_Restore(t *testing.T) {
|
|||
require.NoError(t, s.ACLRoleBatchSet(2, roles, false))
|
||||
|
||||
// Read the restored ACLs back out and verify that they match.
|
||||
// nolint:staticcheck
|
||||
idx, res, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(4), idx)
|
||||
|
|
|
@ -1305,6 +1305,7 @@ type ACLTokenListRequest struct {
|
|||
Policy string // Policy filter
|
||||
Role string // Role filter
|
||||
AuthMethod string // Auth Method filter
|
||||
ServiceName string // Service name (from service identities) filter
|
||||
Datacenter string // The datacenter to perform the request within
|
||||
ACLAuthMethodEnterpriseMeta
|
||||
acl.EnterpriseMeta
|
||||
|
|
45
api/acl.go
45
api/acl.go
|
@ -272,6 +272,13 @@ type ACLAuthMethod struct {
|
|||
Partition string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ACLTokenFilterOptions struct {
|
||||
AuthMethod string `json:",omitempty"`
|
||||
Policy string `json:",omitempty"`
|
||||
Role string `json:",omitempty"`
|
||||
ServiceName string `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (m *ACLAuthMethod) MarshalJSON() ([]byte, error) {
|
||||
type Alias ACLAuthMethod
|
||||
exported := &struct {
|
||||
|
@ -895,6 +902,44 @@ func (a *ACL) TokenList(q *QueryOptions) ([]*ACLTokenListEntry, *QueryMeta, erro
|
|||
return entries, qm, nil
|
||||
}
|
||||
|
||||
// TokenListFiltered lists all tokens that match the given filter options.
|
||||
// The listing does not contain any SecretIDs as those may only be retrieved by a call to TokenRead.
|
||||
func (a *ACL) TokenListFiltered(t ACLTokenFilterOptions, q *QueryOptions) ([]*ACLTokenListEntry, *QueryMeta, error) {
|
||||
r := a.c.newRequest("GET", "/v1/acl/tokens")
|
||||
r.setQueryOptions(q)
|
||||
|
||||
if t.AuthMethod != "" {
|
||||
r.params.Set("authmethod", t.AuthMethod)
|
||||
}
|
||||
if t.Policy != "" {
|
||||
r.params.Set("policy", t.Policy)
|
||||
}
|
||||
if t.Role != "" {
|
||||
r.params.Set("role", t.Role)
|
||||
}
|
||||
if t.ServiceName != "" {
|
||||
r.params.Set("servicename", t.ServiceName)
|
||||
}
|
||||
|
||||
rtt, resp, err := a.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer closeResponseBody(resp)
|
||||
if err := requireOK(resp); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var entries []*ACLTokenListEntry
|
||||
if err := decodeBody(resp, &entries); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return entries, qm, nil
|
||||
}
|
||||
|
||||
// PolicyCreate will create a new policy. It is not allowed for the policy parameters
|
||||
// ID field to be set as this will be generated by Consul while processing the request.
|
||||
func (a *ACL) PolicyCreate(policy *ACLPolicy, q *WriteOptions) (*ACLPolicy, *WriteMeta, error) {
|
||||
|
|
|
@ -553,6 +553,89 @@ func TestAPI_ACLToken_List(t *testing.T) {
|
|||
require.NotNil(t, token5)
|
||||
}
|
||||
|
||||
func TestAPI_ACLToken_ListFiltered(t *testing.T) {
|
||||
t.Parallel()
|
||||
c, s := makeACLClient(t)
|
||||
defer s.Stop()
|
||||
|
||||
acl := c.ACL()
|
||||
s.WaitForSerfCheck(t)
|
||||
|
||||
created1, _, err := acl.TokenCreate(&ACLToken{
|
||||
Description: "token1",
|
||||
ServiceIdentities: []*ACLServiceIdentity{
|
||||
{ServiceName: "s1"},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created1)
|
||||
require.NotEqual(t, "", created1.AccessorID)
|
||||
require.NotEqual(t, "", created1.SecretID)
|
||||
|
||||
created2, _, err := acl.TokenCreate(&ACLToken{
|
||||
Description: "token2",
|
||||
ServiceIdentities: []*ACLServiceIdentity{
|
||||
{ServiceName: "s2"},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created2)
|
||||
require.NotEqual(t, "", created2.AccessorID)
|
||||
require.NotEqual(t, "", created2.SecretID)
|
||||
|
||||
created3, _, err := acl.TokenCreate(&ACLToken{
|
||||
Description: "token3",
|
||||
ServiceIdentities: []*ACLServiceIdentity{
|
||||
{ServiceName: "s1"},
|
||||
{ServiceName: "s2"},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created3)
|
||||
require.NotEqual(t, "", created3.AccessorID)
|
||||
require.NotEqual(t, "", created3.SecretID)
|
||||
|
||||
tokens, qm, err := acl.TokenListFiltered(ACLTokenFilterOptions{
|
||||
ServiceName: "s1",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, 0, qm.LastIndex)
|
||||
require.True(t, qm.KnownLeader)
|
||||
require.Len(t, tokens, 2)
|
||||
found := make([]string, 0, 2)
|
||||
for _, token := range tokens {
|
||||
found = append(found, token.Description)
|
||||
}
|
||||
require.ElementsMatch(t, []string{"token1", "token3"}, found)
|
||||
|
||||
tokens, qm, err = acl.TokenListFiltered(ACLTokenFilterOptions{
|
||||
ServiceName: "s2",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, 0, qm.LastIndex)
|
||||
require.True(t, qm.KnownLeader)
|
||||
require.Len(t, tokens, 2)
|
||||
found = make([]string, 0, 2)
|
||||
for _, token := range tokens {
|
||||
found = append(found, token.Description)
|
||||
}
|
||||
require.ElementsMatch(t, []string{"token2", "token3"}, found)
|
||||
|
||||
tokens, qm, err = acl.TokenListFiltered(ACLTokenFilterOptions{
|
||||
ServiceName: "nothing",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, 0, qm.LastIndex)
|
||||
require.True(t, qm.KnownLeader)
|
||||
require.Empty(t, tokens)
|
||||
|
||||
_, _, err = acl.TokenListFiltered(ACLTokenFilterOptions{
|
||||
ServiceName: "s",
|
||||
AuthMethod: "a",
|
||||
}, nil)
|
||||
require.ErrorContains(t, err, "can only filter by one of")
|
||||
}
|
||||
|
||||
func TestAPI_ACLToken_Clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
c, s := makeACLClient(t)
|
||||
|
|
|
@ -682,6 +682,9 @@ The corresponding CLI command is [`consul acl token list`](/consul/commands/acl/
|
|||
- `role` `(string: "")` - Filters the token list to those tokens that are
|
||||
linked with this specific role ID.
|
||||
|
||||
- `servicename` `(string: "")` - Filters the token list to those tokens that are
|
||||
linked with this specific service name in their service identity.
|
||||
|
||||
- `authmethod` `(string: "")` - Filters the token list to those tokens that are
|
||||
linked with this specific named auth method.
|
||||
|
||||
|
|
Loading…
Reference in New Issue