Browse Source

[NET-5399] Add support for querying tokens by service name. (#18691)

* 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.
backport/spatel/emit-consul-version-periodically/mentally-choice-bug
Derek Menteer 1 year ago committed by GitHub
parent
commit
ff6111651d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .changelog/18667.txt
  2. 1
      agent/acl_endpoint.go
  3. 32
      agent/acl_endpoint_test.go
  4. 14
      agent/consul/acl_endpoint.go
  5. 5
      agent/consul/acl_replication_test.go
  6. 1
      agent/consul/acl_replication_types.go
  7. 65
      agent/consul/state/acl.go
  8. 4
      agent/consul/state/acl_ce.go
  9. 26
      agent/consul/state/acl_schema.go
  10. 133
      agent/consul/state/acl_test.go
  11. 1
      agent/structs/acl.go
  12. 45
      api/acl.go
  13. 84
      api/acl_test.go
  14. 3
      website/content/api-docs/acl/tokens.mdx

3
.changelog/18667.txt

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

1
agent/acl_endpoint.go

@ -275,6 +275,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
}

32
agent/acl_endpoint_test.go

@ -1177,6 +1177,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)
})
})
}

14
agent/consul/acl_endpoint.go

@ -656,8 +656,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
}

5
agent/consul/acl_replication_test.go

@ -379,8 +379,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)
@ -484,6 +486,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)
@ -822,9 +825,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)

1
agent/consul/acl_replication_types.go

@ -35,6 +35,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

65
agent/consul/state/acl.go

@ -640,8 +640,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()
@ -654,43 +681,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 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 = aclTokenListByRole(tx, role, 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 policy == "" && role == "" && methodName != "" {
iter, err = aclTokenListByAuthMethod(tx, methodName, methodMeta, entMeta)
} 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
}
@ -715,7 +750,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
}

4
agent/consul/state/acl_ce.go

@ -73,6 +73,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 {

26
agent/consul/state/acl_schema.go

@ -19,6 +19,7 @@ const (
indexAccessor = "accessor"
indexPolicies = "policies"
indexRoles = "roles"
indexServiceName = "service-name"
indexAuthMethod = "authmethod"
indexLocality = "locality"
indexName = "name"
@ -104,7 +105,6 @@ func tokensTableSchema() *memdb.TableSchema {
writeIndex: indexExpiresLocalFromACLToken,
},
},
// DEPRECATED (ACL-Legacy-Compat) - This index is only needed while we support upgrading v1 to v2 acls
// This table indexes all the ACL tokens that do not have an AccessorID
// TODO(ACL-Legacy-Compat): remove in phase 2
@ -121,6 +121,15 @@ func tokensTableSchema() *memdb.TableSchema {
},
},
},
indexServiceName: {
Name: indexServiceName,
AllowMissing: true,
Unique: false,
Indexer: indexerMulti[Query, *structs.ACLToken]{
readIndex: indexFromQuery,
writeIndexMulti: indexServiceNameFromACLToken,
},
},
},
}
}
@ -413,6 +422,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,

133
agent/consul/state/acl_test.go

@ -216,6 +216,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)
@ -230,6 +231,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)
@ -957,18 +959,36 @@ 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{}))
type testCase struct {
name string
local bool
global bool
policy string
role string
methodName string
accessors []string
name string
local bool
global bool
policy string
role string
methodName string
serviceName string
accessors []string
}
cases := []testCase{
@ -984,6 +1004,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
},
},
@ -997,6 +1018,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
},
@ -1091,6 +1113,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,
@ -1105,6 +1151,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
@ -1112,14 +1160,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)
})
}
@ -1128,12 +1189,33 @@ func TestStateStore_ACLToken_List(t *testing.T) {
tc := tc // capture range variable
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, 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))
tokens.Sort()
for i, token := range tokens {
require.Equal(t, tc.accessors[i], token.AccessorID)
// 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))
tokens.Sort()
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)
}
}
})
}
@ -1189,6 +1271,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)
@ -1233,6 +1316,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)
@ -1318,6 +1402,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)
@ -1362,6 +1447,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)
@ -2802,16 +2888,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))
@ -2823,16 +2912,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))
@ -3625,6 +3717,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)

1
agent/structs/acl.go

@ -1328,6 +1328,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

@ -270,6 +270,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 {
@ -878,6 +885,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) {

84
api/acl_test.go

@ -518,6 +518,90 @@ 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.NotNil(t, err)
require.Contains(t, err.Error(), "can only filter by one of")
}
func TestAPI_ACLToken_Clone(t *testing.T) {
t.Parallel()
c, s := makeACLClient(t)

3
website/content/api-docs/acl/tokens.mdx

@ -684,6 +684,9 @@ The corresponding CLI command is [`consul acl token list`](/commands/acl/token/l
- `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…
Cancel
Save