From ff6111651dfc28632b97683573ed12f50d3e34ad Mon Sep 17 00:00:00 2001 From: Derek Menteer <105233703+hashi-derek@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:59:23 -0500 Subject: [PATCH] [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. --- .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 | 26 ++++- agent/consul/state/acl_test.go | 133 ++++++++++++++++++++---- agent/structs/acl.go | 1 + api/acl.go | 45 ++++++++ api/acl_test.go | 84 +++++++++++++++ website/content/api-docs/acl/tokens.mdx | 3 + 14 files changed, 379 insertions(+), 38 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 4b8391cbc3..9d328263f0 100644 --- a/agent/acl_endpoint.go +++ b/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 } diff --git a/agent/acl_endpoint_test.go b/agent/acl_endpoint_test.go index d7fa6df6ed..1538038a64 100644 --- a/agent/acl_endpoint_test.go +++ b/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) + }) }) } diff --git a/agent/consul/acl_endpoint.go b/agent/consul/acl_endpoint.go index 313c06924c..7e047ca221 100644 --- a/agent/consul/acl_endpoint.go +++ b/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 } diff --git a/agent/consul/acl_replication_test.go b/agent/consul/acl_replication_test.go index 102a7b4b26..623b68380c 100644 --- a/agent/consul/acl_replication_test.go +++ b/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) diff --git a/agent/consul/acl_replication_types.go b/agent/consul/acl_replication_types.go index a395270d6e..20e4456988 100644 --- a/agent/consul/acl_replication_types.go +++ b/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 diff --git a/agent/consul/state/acl.go b/agent/consul/state/acl.go index d84afd65a0..1281e2bb25 100644 --- a/agent/consul/state/acl.go +++ b/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 } diff --git a/agent/consul/state/acl_ce.go b/agent/consul/state/acl_ce.go index 6867130713..d1a176c0ad 100644 --- a/agent/consul/state/acl_ce.go +++ b/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 { diff --git a/agent/consul/state/acl_schema.go b/agent/consul/state/acl_schema.go index 485b5b92d9..8d24d4e390 100644 --- a/agent/consul/state/acl_schema.go +++ b/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, diff --git a/agent/consul/state/acl_test.go b/agent/consul/state/acl_test.go index 9a61aed221..f2f53c4f8c 100644 --- a/agent/consul/state/acl_test.go +++ b/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) diff --git a/agent/structs/acl.go b/agent/structs/acl.go index a5274f145e..026a9a8891 100644 --- a/agent/structs/acl.go +++ b/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 diff --git a/api/acl.go b/api/acl.go index bd6d825632..dc4b8d2e74 100644 --- a/api/acl.go +++ b/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) { diff --git a/api/acl_test.go b/api/acl_test.go index 3deafbaeb5..943d138fff 100644 --- a/api/acl_test.go +++ b/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) diff --git a/website/content/api-docs/acl/tokens.mdx b/website/content/api-docs/acl/tokens.mdx index b6f6c2d7c4..2310a91a29 100644 --- a/website/content/api-docs/acl/tokens.mdx +++ b/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.