From a637ae5aa5a826759c9cfb1e06de0a35875e6684 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Fri, 15 Aug 2025 14:51:43 +0200 Subject: [PATCH] fix(api): fix for ListAppKeys() not returning app keys (#10465) # Which Problems Are Solved `ListAppKeys()` does not work properly, in that it does not return any app keys. # How the Problems Are Solved The issue stems from a mistake SQL query not joining the `projections.authn_keys2` table to `projections.projects4` instead of joining to `projections.apps7` # Additional Changes `ListAppKeys()` returns the app key IDs in order of their creation - Closes https://github.com/zitadel/zitadel/issues/10420 - backport to v4.x --------- Co-authored-by: Livio Spring --- .../integration_test/app_key_test.go | 109 ++++++++++++++++++ .../integration_test/server_test.go | 7 +- internal/query/authn_key.go | 7 +- 3 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 internal/api/grpc/management/integration_test/app_key_test.go diff --git a/internal/api/grpc/management/integration_test/app_key_test.go b/internal/api/grpc/management/integration_test/app_key_test.go new file mode 100644 index 0000000000..a08c552709 --- /dev/null +++ b/internal/api/grpc/management/integration_test/app_key_test.go @@ -0,0 +1,109 @@ +//go:build integration + +package management_test + +import ( + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/management" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_ListAppKeys(t *testing.T) { + // create project + prjName := gofakeit.Name() + createPrjRes, err := Instance.Client.Projectv2Beta.CreateProject(IAMOwnerCTX, &project.CreateProjectRequest{ + Name: prjName, + OrganizationId: Instance.DefaultOrg.Id, + }) + require.NoError(t, err) + prjId := createPrjRes.Id + + // add app to project + createAppjRes, err := Instance.Client.AppV2Beta.CreateApplication(IAMOwnerCTX, &app.CreateApplicationRequest{ + ProjectId: prjId, + Name: gofakeit.Name(), + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }) + require.NoError(t, err) + appId := createAppjRes.AppId + + type test struct { + name string + expectedKeyIdsFunc func() []string + } + + tests := []test{ + { + name: "happy path", + expectedKeyIdsFunc: func() []string { + // add other app to project + createOtherAppjRes, err := Instance.Client.AppV2Beta.CreateApplication(IAMOwnerCTX, &app.CreateApplicationRequest{ + ProjectId: prjId, + Name: gofakeit.Name(), + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }) + require.NoError(t, err) + otherAppId := createOtherAppjRes.AppId + // add other project key ids - These SHOULD NOT be returned when calling ListAppKeys() + for range 5 { + _, err := Instance.Client.AppV2Beta.CreateApplicationKey(IAMOwnerCTX, &app.CreateApplicationKeyRequest{ + AppId: otherAppId, + ProjectId: prjId, + ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()), + }) + require.NoError(t, err) + } + + // create app keys we expect to be rturned form ListAppKeys() + keyIDs := make([]string, 5) + for i := range len(keyIDs) { + res, err := Instance.Client.AppV2Beta.CreateApplicationKey(IAMOwnerCTX, &app.CreateApplicationKeyRequest{ + AppId: appId, + ProjectId: prjId, + ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()), + }) + require.NoError(t, err) + keyIDs[i] = res.Id + } + return keyIDs + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + assert.EventuallyWithT(t, func(ct *assert.CollectT) { + expectedKeyIds := tt.expectedKeyIdsFunc() + + res, err := Client.ListAppKeys(IAMOwnerCTX, &management.ListAppKeysRequest{ + AppId: appId, + ProjectId: prjId, + }) + require.NoError(t, err) + assert.Equal(t, len(expectedKeyIds), len(res.GetResult())) + + for _, key := range res.GetResult() { + assert.True(t, slices.Contains(expectedKeyIds, key.Id)) + } + }, retryDuration, tick) + }) + } +} diff --git a/internal/api/grpc/management/integration_test/server_test.go b/internal/api/grpc/management/integration_test/server_test.go index 498fdfe852..ac7ab4ea6e 100644 --- a/internal/api/grpc/management/integration_test/server_test.go +++ b/internal/api/grpc/management/integration_test/server_test.go @@ -13,9 +13,9 @@ import ( ) var ( - CTX, OrgCTX context.Context - Instance *integration.Instance - Client mgmt_pb.ManagementServiceClient + CTX, IAMOwnerCTX, OrgCTX context.Context + Instance *integration.Instance + Client mgmt_pb.ManagementServiceClient ) func TestMain(m *testing.M) { @@ -25,6 +25,7 @@ func TestMain(m *testing.M) { Instance = integration.NewInstance(ctx) CTX = ctx + IAMOwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) OrgCTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner) Client = Instance.Client.Mgmt return m.Run() diff --git a/internal/query/authn_key.go b/internal/query/authn_key.go index abda5f011e..a66212b329 100644 --- a/internal/query/authn_key.go +++ b/internal/query/authn_key.go @@ -169,17 +169,16 @@ func (q *Queries) searchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQu case JoinFilterUnspecified: // Select all authN keys case JoinFilterApp: - joinCol := ProjectColumnID - query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier()) + query = query.Join(join(AppColumnID, AuthNKeyColumnObjectID)) case JoinFilterUserMachine: - joinCol := MachineUserIDCol - query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier()) + query = query.Join(join(MachineUserIDCol, AuthNKeyColumnIdentifier)) query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, AuthNKeyColumnResourceOwner, AuthNKeyColumnIdentifier) } eq := sq.Eq{ AuthNKeyColumnEnabled.identifier(): true, AuthNKeyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } + stmt, args, err := query.Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-SAf3f", "Errors.Query.InvalidRequest")