From 56917eb4c9688df86e69aa99e13897fa203ecfb0 Mon Sep 17 00:00:00 2001 From: Derek Menteer <105233703+hashi-derek@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:52:45 -0500 Subject: [PATCH] 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. --- .changelog/18667.txt | 3 + agent/acl_endpoint.go | 1 + agent/acl_endpoint_test.go | 32 ++++++ agent/consul/acl_endpoint.go | 14 ++- agent/consul/acl_replication_test.go | 5 + agent/consul/acl_replication_types.go | 1 + agent/consul/state/acl.go | 65 +++++++++--- agent/consul/state/acl_ce.go | 4 + agent/consul/state/acl_schema.go | 25 +++++ agent/consul/state/acl_test.go | 133 ++++++++++++++++++++---- agent/structs/acl.go | 1 + api/acl.go | 45 ++++++++ api/acl_test.go | 83 +++++++++++++++ website/content/api-docs/acl/tokens.mdx | 3 + 14 files changed, 378 insertions(+), 37 deletions(-) create mode 100644 .changelog/18667.txt diff --git a/.changelog/18667.txt b/.changelog/18667.txt new file mode 100644 index 0000000000..c9ef7b4551 --- /dev/null +++ b/.changelog/18667.txt @@ -0,0 +1,3 @@ +```release-note:improvement +api: Add support for listing ACL tokens by service name. +``` diff --git a/agent/acl_endpoint.go b/agent/acl_endpoint.go index 9f38f41f15..afef4330e4 100644 --- a/agent/acl_endpoint.go +++ b/agent/acl_endpoint.go @@ -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 } diff --git a/agent/acl_endpoint_test.go b/agent/acl_endpoint_test.go index 82b2a9574f..a1aaba52a5 100644 --- a/agent/acl_endpoint_test.go +++ b/agent/acl_endpoint_test.go @@ -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) + }) }) } diff --git a/agent/consul/acl_endpoint.go b/agent/consul/acl_endpoint.go index 97189bb45d..ba9a081ea7 100644 --- a/agent/consul/acl_endpoint.go +++ b/agent/consul/acl_endpoint.go @@ -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 } diff --git a/agent/consul/acl_replication_test.go b/agent/consul/acl_replication_test.go index 7a8e8efa12..70ed426729 100644 --- a/agent/consul/acl_replication_test.go +++ b/agent/consul/acl_replication_test.go @@ -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) diff --git a/agent/consul/acl_replication_types.go b/agent/consul/acl_replication_types.go index de9e8cf763..8a672cb90d 100644 --- a/agent/consul/acl_replication_types.go +++ b/agent/consul/acl_replication_types.go @@ -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 diff --git a/agent/consul/state/acl.go b/agent/consul/state/acl.go index f82b671b18..5114d09e46 100644 --- a/agent/consul/state/acl.go +++ b/agent/consul/state/acl.go @@ -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 } diff --git a/agent/consul/state/acl_ce.go b/agent/consul/state/acl_ce.go index ee0a8eb5ee..e8f6450e09 100644 --- a/agent/consul/state/acl_ce.go +++ b/agent/consul/state/acl_ce.go @@ -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 { diff --git a/agent/consul/state/acl_schema.go b/agent/consul/state/acl_schema.go index cdee3ed9ff..17c772b37e 100644 --- a/agent/consul/state/acl_schema.go +++ b/agent/consul/state/acl_schema.go @@ -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, diff --git a/agent/consul/state/acl_test.go b/agent/consul/state/acl_test.go index 330c0689aa..7614991c41 100644 --- a/agent/consul/state/acl_test.go +++ b/agent/consul/state/acl_test.go @@ -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,18 +851,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{ @@ -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,12 +1081,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) + } } }) } @@ -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) diff --git a/agent/structs/acl.go b/agent/structs/acl.go index 813a3b1758..8b5e041378 100644 --- a/agent/structs/acl.go +++ b/agent/structs/acl.go @@ -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 diff --git a/api/acl.go b/api/acl.go index 93087cd31c..48d2e66ee9 100644 --- a/api/acl.go +++ b/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) { diff --git a/api/acl_test.go b/api/acl_test.go index f515878290..d5ff37c1f7 100644 --- a/api/acl_test.go +++ b/api/acl_test.go @@ -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) diff --git a/website/content/api-docs/acl/tokens.mdx b/website/content/api-docs/acl/tokens.mdx index 42b254ddff..d1e6d609af 100644 --- a/website/content/api-docs/acl/tokens.mdx +++ b/website/content/api-docs/acl/tokens.mdx @@ -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.