[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:
Derek Menteer 2023-09-06 13:16:27 -05:00 committed by GitHub
parent 35bf77c387
commit b06927c0e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 378 additions and 37 deletions

3
.changelog/18667.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
api: Add support for listing ACL tokens by service name.
```

View File

@ -294,6 +294,7 @@ func (s *HTTPHandlers) ACLTokenList(resp http.ResponseWriter, req *http.Request)
args.Policy = req.URL.Query().Get("policy") args.Policy = req.URL.Query().Get("policy")
args.Role = req.URL.Query().Get("role") args.Role = req.URL.Query().Get("role")
args.AuthMethod = req.URL.Query().Get("authmethod") args.AuthMethod = req.URL.Query().Get("authmethod")
args.ServiceName = req.URL.Query().Get("servicename")
if err := parseACLAuthMethodEnterpriseMeta(req, &args.ACLAuthMethodEnterpriseMeta); err != nil { if err := parseACLAuthMethodEnterpriseMeta(req, &args.ACLAuthMethodEnterpriseMeta); err != nil {
return nil, err return nil, err
} }

View File

@ -1328,6 +1328,38 @@ func TestACL_HTTP(t *testing.T) {
require.Error(t, err) require.Error(t, err)
testutil.RequireErrorContains(t, err, "Only lowercase alphanumeric") 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)
})
}) })
} }

View File

@ -680,8 +680,18 @@ func (a *ACL) TokenList(args *structs.ACLTokenListRequest, reply *structs.ACLTok
} }
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta, return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error { func(ws memdb.WatchSet, s *state.Store) error {
index, tokens, err := state.ACLTokenList(ws, args.IncludeLocal, args.IncludeGlobal, args.Policy, args.Role, args.AuthMethod, methodMeta, &args.EnterpriseMeta) 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 { if err != nil {
return err return err
} }

View File

@ -378,8 +378,10 @@ func TestACLReplication_Tokens(t *testing.T) {
checkSame := func(t *retry.R) { checkSame := func(t *retry.R) {
// only account for global tokens - local tokens shouldn't be replicated // 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) index, remote, err := s1.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
// nolint:staticcheck
_, local, err := s2.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil) _, local, err := s2.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -483,6 +485,7 @@ func TestACLReplication_Tokens(t *testing.T) {
}) })
// verify dc2 local tokens didn't get blown away // verify dc2 local tokens didn't get blown away
// nolint:staticcheck
_, local, err := s2.fsm.State().ACLTokenList(nil, true, false, "", "", "", nil, nil) _, local, err := s2.fsm.State().ACLTokenList(nil, true, false, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, local, 50) require.Len(t, local, 50)
@ -821,9 +824,11 @@ func TestACLReplication_AllTypes(t *testing.T) {
checkSameTokens := func(t *retry.R) { checkSameTokens := func(t *retry.R) {
// only account for global tokens - local tokens shouldn't be replicated // 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) index, remote, err := s1.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
// Query for all of them, so that we can prove that no globals snuck in. // 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) _, local, err := s2.fsm.State().ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)

View File

@ -38,6 +38,7 @@ func (r *aclTokenReplicator) FetchRemote(srv *Server, lastRemoteIndex uint64) (i
func (r *aclTokenReplicator) FetchLocal(srv *Server) (int, uint64, error) { func (r *aclTokenReplicator) FetchLocal(srv *Server) (int, uint64, error) {
r.local = nil r.local = nil
// nolint:staticcheck
idx, local, err := srv.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, srv.replicationEnterpriseMeta()) idx, local, err := srv.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, srv.replicationEnterpriseMeta())
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err

View File

@ -623,8 +623,35 @@ func aclTokenGetTxn(tx ReadTxn, ws memdb.WatchSet, value, index string, entMeta
return nil, nil 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. // 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) { 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) tx := s.db.Txn(false)
defer tx.Abort() defer tx.Abort()
@ -637,43 +664,51 @@ func (s *Store) ACLTokenList(ws memdb.WatchSet, local, global bool, policy, role
needLocalityFilter := false needLocalityFilter := false
if policy == "" && role == "" && methodName == "" { if params.Policy == "" && params.Role == "" && params.MethodName == "" && params.ServiceName == "" {
if global == local { if params.Global == params.Local {
iter, err = aclTokenListAll(tx, entMeta) iter, err = aclTokenListAll(tx, params.EnterpriseMeta)
} else { } else {
iter, err = aclTokenList(tx, entMeta, local) iter, err = aclTokenList(tx, params.EnterpriseMeta, params.Local)
} }
} else if policy != "" && role == "" && methodName == "" { } else if params.Policy != "" && params.Role == "" && params.MethodName == "" && params.ServiceName == "" {
iter, err = aclTokenListByPolicy(tx, policy, entMeta) // Find by policy
iter, err = aclTokenListByPolicy(tx, params.Policy, params.EnterpriseMeta)
needLocalityFilter = true needLocalityFilter = true
} else if policy == "" && role != "" && methodName == "" { } else if params.Policy == "" && params.Role != "" && params.MethodName == "" && params.ServiceName == "" {
iter, err = aclTokenListByRole(tx, role, entMeta) // Find by role
iter, err = aclTokenListByRole(tx, params.Role, params.EnterpriseMeta)
needLocalityFilter = true needLocalityFilter = true
} else if policy == "" && role == "" && methodName != "" { } else if params.Policy == "" && params.Role == "" && params.MethodName != "" && params.ServiceName == "" {
iter, err = aclTokenListByAuthMethod(tx, methodName, methodMeta, entMeta) // 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 needLocalityFilter = true
} else { } 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 { if err != nil {
return 0, nil, fmt.Errorf("failed acl token lookup: %v", err) 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 { iter = memdb.NewFilterIterator(iter, func(raw interface{}) bool {
token, ok := raw.(*structs.ACLToken) token, ok := raw.(*structs.ACLToken)
if !ok { if !ok {
return true return true
} }
if global && !token.Local { if params.Global && !token.Local {
return false return false
} else if local && token.Local { } else if params.Local && token.Local {
return false return false
} }
@ -698,7 +733,7 @@ func (s *Store) ACLTokenList(ws memdb.WatchSet, local, global bool, policy, role
} }
// Get the table index. // Get the table index.
idx := aclTokenMaxIndex(tx, nil, entMeta) idx := aclTokenMaxIndex(tx, nil, params.EnterpriseMeta)
return idx, result, nil return idx, result, nil
} }

View File

@ -76,6 +76,10 @@ func aclTokenListByAuthMethod(tx ReadTxn, authMethod string, _, _ *acl.Enterpris
return tx.Get(tableACLTokens, indexAuthMethod, AuthMethodQuery{Value: authMethod}) 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 { func aclTokenDeleteWithToken(tx WriteTxn, token *structs.ACLToken, idx uint64) error {
// remove the token // remove the token
if err := tx.Delete(tableACLTokens, token); err != nil { if err := tx.Delete(tableACLTokens, token); err != nil {

View File

@ -22,6 +22,7 @@ const (
indexAccessor = "accessor" indexAccessor = "accessor"
indexPolicies = "policies" indexPolicies = "policies"
indexRoles = "roles" indexRoles = "roles"
indexServiceName = "service-name"
indexAuthMethod = "authmethod" indexAuthMethod = "authmethod"
indexLocality = "locality" indexLocality = "locality"
indexName = "name" indexName = "name"
@ -106,6 +107,15 @@ func tokensTableSchema() *memdb.TableSchema {
writeIndex: indexExpiresLocalFromACLToken, 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 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 { func authMethodsTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{ return &memdb.TableSchema{
Name: tableACLAuthMethods, Name: tableACLAuthMethods,

View File

@ -214,6 +214,7 @@ func TestStateStore_ACLBootstrap(t *testing.T) {
require.Equal(t, uint64(3), index) require.Equal(t, uint64(3), index)
// Make sure the ACLs are in an expected state. // Make sure the ACLs are in an expected state.
// nolint:staticcheck
_, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil) _, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, tokens, 1) require.Len(t, tokens, 1)
@ -228,6 +229,7 @@ func TestStateStore_ACLBootstrap(t *testing.T) {
err = s.ACLBootstrap(32, index, token2.Clone()) err = s.ACLBootstrap(32, index, token2.Clone())
require.NoError(t, err) require.NoError(t, err)
// nolint:staticcheck
_, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil) _, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, tokens, 2) require.Len(t, tokens, 2)
@ -849,6 +851,23 @@ func TestStateStore_ACLToken_List(t *testing.T) {
AuthMethod: "test", AuthMethod: "test",
Local: true, 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{})) require.NoError(t, s.ACLTokenBatchSet(2, tokens, ACLTokenSetOptions{}))
@ -860,6 +879,7 @@ func TestStateStore_ACLToken_List(t *testing.T) {
policy string policy string
role string role string
methodName string methodName string
serviceName string
accessors []string accessors []string
} }
@ -876,6 +896,7 @@ func TestStateStore_ACLToken_List(t *testing.T) {
"47eea4da-bda1-48a6-901c-3e36d2d9262f", // policy + global "47eea4da-bda1-48a6-901c-3e36d2d9262f", // policy + global
"54866514-3cf2-4fec-8a8a-710583831834", // mgmt + global "54866514-3cf2-4fec-8a8a-710583831834", // mgmt + global
"74277ae1-6a9b-4035-b444-2370fe6a2cb5", // authMethod + global "74277ae1-6a9b-4035-b444-2370fe6a2cb5", // authMethod + global
"80c900e1-2fc5-4685-ae29-1b2d17fc30e4", // serviceName + global
"a7715fde-8954-4c92-afbc-d84c6ecdc582", // role + global "a7715fde-8954-4c92-afbc-d84c6ecdc582", // role + global
}, },
}, },
@ -889,6 +910,7 @@ func TestStateStore_ACLToken_List(t *testing.T) {
accessors: []string{ accessors: []string{
"211f0360-ef53-41d3-9d4d-db84396eb6c0", // authMethod + local "211f0360-ef53-41d3-9d4d-db84396eb6c0", // authMethod + local
"4915fc9d-3726-4171-b588-6c271f45eecd", // policy + local "4915fc9d-3726-4171-b588-6c271f45eecd", // policy + local
"a14fa45e-0afe-4b44-961d-a430030ccfe2", // serviceName + local
"cadb4f13-f62a-49ab-ab3f-5a7e01b925d9", // role + local "cadb4f13-f62a-49ab-ab3f-5a7e01b925d9", // role + local
"f1093997-b6c7-496d-bfb8-6b1b1895641b", // mgmt + 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 "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", name: "All",
local: true, local: true,
@ -997,6 +1043,8 @@ func TestStateStore_ACLToken_List(t *testing.T) {
"4915fc9d-3726-4171-b588-6c271f45eecd", // policy + local "4915fc9d-3726-4171-b588-6c271f45eecd", // policy + local
"54866514-3cf2-4fec-8a8a-710583831834", // mgmt + global "54866514-3cf2-4fec-8a8a-710583831834", // mgmt + global
"74277ae1-6a9b-4035-b444-2370fe6a2cb5", // authMethod + 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 "a7715fde-8954-4c92-afbc-d84c6ecdc582", // role + global
"cadb4f13-f62a-49ab-ab3f-5a7e01b925d9", // role + local "cadb4f13-f62a-49ab-ab3f-5a7e01b925d9", // role + local
"f1093997-b6c7-496d-bfb8-6b1b1895641b", // mgmt + 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 }{ for _, tc := range []struct{ policy, role, methodName, serviceName string }{
{testPolicyID_A, testRoleID_A, "test"}, {testPolicyID_A, testRoleID_A, "test", ""},
{"", testRoleID_A, "test"}, {"", testRoleID_A, "test", ""},
{testPolicyID_A, "", "test"}, {testPolicyID_A, "", "test", ""},
{testPolicyID_A, testRoleID_A, ""}, {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) { 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) {
_, _, err := s.ACLTokenList(nil, false, false, tc.policy, tc.role, tc.methodName, nil, nil) 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) require.Error(t, err)
}) })
} }
@ -1020,6 +1081,9 @@ func TestStateStore_ACLToken_List(t *testing.T) {
tc := tc // capture range variable tc := tc // capture range variable
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() 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) _, tokens, err := s.ACLTokenList(nil, tc.local, tc.global, tc.policy, tc.role, tc.methodName, nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, tokens, len(tc.accessors)) require.Len(t, tokens, len(tc.accessors))
@ -1027,6 +1091,24 @@ func TestStateStore_ACLToken_List(t *testing.T) {
for i, token := range tokens { for i, token := range tokens {
require.Equal(t, tc.accessors[i], token.AccessorID) 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) require.Equal(t, "node-read-renamed", retrieved.Policies[0].Name)
// list tokens without stale links // list tokens without stale links
// nolint:staticcheck
_, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil) _, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -1124,6 +1207,7 @@ func TestStateStore_ACLToken_FixupPolicyLinks(t *testing.T) {
require.Len(t, retrieved.Policies, 0) require.Len(t, retrieved.Policies, 0)
// list tokens without stale links // list tokens without stale links
// nolint:staticcheck
_, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil) _, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) 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) require.Equal(t, "node-read-role-renamed", retrieved.Roles[0].Name)
// list tokens without stale links // list tokens without stale links
// nolint:staticcheck
_, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil) _, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -1253,6 +1338,7 @@ func TestStateStore_ACLToken_FixupRoleLinks(t *testing.T) {
require.Len(t, retrieved.Roles, 0) require.Len(t, retrieved.Roles, 0)
// list tokens without stale links // list tokens without stale links
// nolint:staticcheck
_, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil) _, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) 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) { require.True(t, t.Run("list local only", func(t *testing.T) {
// nolint:staticcheck
_, got, err := s.ACLTokenList(nil, true, false, "", "", "test", defaultEntMeta, defaultEntMeta) _, got, err := s.ACLTokenList(nil, true, false, "", "", "test", defaultEntMeta, defaultEntMeta)
require.NoError(t, err) require.NoError(t, err)
require.ElementsMatch(t, []string{methodDC2_tok1, methodDC2_tok2}, toList(got)) require.ElementsMatch(t, []string{methodDC2_tok1, methodDC2_tok2}, toList(got))
})) }))
require.True(t, t.Run("list global only", func(t *testing.T) { require.True(t, t.Run("list global only", func(t *testing.T) {
// nolint:staticcheck
_, got, err := s.ACLTokenList(nil, false, true, "", "", "test", defaultEntMeta, defaultEntMeta) _, got, err := s.ACLTokenList(nil, false, true, "", "", "test", defaultEntMeta, defaultEntMeta)
require.NoError(t, err) require.NoError(t, err)
require.ElementsMatch(t, []string{methodDC1_tok1, methodDC1_tok2}, toList(got)) require.ElementsMatch(t, []string{methodDC1_tok1, methodDC1_tok2}, toList(got))
})) }))
require.True(t, t.Run("list both", func(t *testing.T) { require.True(t, t.Run("list both", func(t *testing.T) {
// nolint:staticcheck
_, got, err := s.ACLTokenList(nil, true, true, "", "", "test", defaultEntMeta, defaultEntMeta) _, got, err := s.ACLTokenList(nil, true, true, "", "", "test", defaultEntMeta, defaultEntMeta)
require.NoError(t, err) require.NoError(t, err)
require.ElementsMatch(t, []string{methodDC1_tok1, methodDC1_tok2, methodDC2_tok1, methodDC2_tok2}, toList(got)) 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) { 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) _, got, err := s.ACLTokenList(nil, true, false, "", "", "test", defaultEntMeta, defaultEntMeta)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, got) require.Empty(t, got)
})) }))
require.True(t, t.Run("list global only (after dc2 delete)", func(t *testing.T) { 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) _, got, err := s.ACLTokenList(nil, false, true, "", "", "test", defaultEntMeta, defaultEntMeta)
require.NoError(t, err) require.NoError(t, err)
require.ElementsMatch(t, []string{methodDC1_tok1, methodDC1_tok2}, toList(got)) require.ElementsMatch(t, []string{methodDC1_tok1, methodDC1_tok2}, toList(got))
})) }))
require.True(t, t.Run("list both (after dc2 delete)", func(t *testing.T) { 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) _, got, err := s.ACLTokenList(nil, true, true, "", "", "test", defaultEntMeta, defaultEntMeta)
require.NoError(t, err) require.NoError(t, err)
require.ElementsMatch(t, []string{methodDC1_tok1, methodDC1_tok2}, toList(got)) 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)) require.NoError(t, s.ACLRoleBatchSet(2, roles, false))
// Read the restored ACLs back out and verify that they match. // Read the restored ACLs back out and verify that they match.
// nolint:staticcheck
idx, res, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil) idx, res, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, uint64(4), idx) require.Equal(t, uint64(4), idx)

View File

@ -1305,6 +1305,7 @@ type ACLTokenListRequest struct {
Policy string // Policy filter Policy string // Policy filter
Role string // Role filter Role string // Role filter
AuthMethod string // Auth Method filter AuthMethod string // Auth Method filter
ServiceName string // Service name (from service identities) filter
Datacenter string // The datacenter to perform the request within Datacenter string // The datacenter to perform the request within
ACLAuthMethodEnterpriseMeta ACLAuthMethodEnterpriseMeta
acl.EnterpriseMeta acl.EnterpriseMeta

View File

@ -272,6 +272,13 @@ type ACLAuthMethod struct {
Partition string `json:",omitempty"` 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) { func (m *ACLAuthMethod) MarshalJSON() ([]byte, error) {
type Alias ACLAuthMethod type Alias ACLAuthMethod
exported := &struct { exported := &struct {
@ -895,6 +902,44 @@ func (a *ACL) TokenList(q *QueryOptions) ([]*ACLTokenListEntry, *QueryMeta, erro
return entries, qm, nil 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 // 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. // 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) { func (a *ACL) PolicyCreate(policy *ACLPolicy, q *WriteOptions) (*ACLPolicy, *WriteMeta, error) {

View File

@ -553,6 +553,89 @@ func TestAPI_ACLToken_List(t *testing.T) {
require.NotNil(t, token5) 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) { func TestAPI_ACLToken_Clone(t *testing.T) {
t.Parallel() t.Parallel()
c, s := makeACLClient(t) c, s := makeACLClient(t)

View File

@ -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 - `role` `(string: "")` - Filters the token list to those tokens that are
linked with this specific role ID. 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 - `authmethod` `(string: "")` - Filters the token list to those tokens that are
linked with this specific named auth method. linked with this specific named auth method.