diff --git a/internal/api/grpc/application/v2/convert/convert.go b/internal/api/grpc/application/v2/convert/convert.go index 63c8fd2ae..d27efaee6 100644 --- a/internal/api/grpc/application/v2/convert/convert.go +++ b/internal/api/grpc/application/v2/convert/convert.go @@ -28,6 +28,7 @@ func AppToPb(queryApp *query.App) *application.Application { State: appStateToPb(queryApp.State), Name: queryApp.Name, Configuration: appConfigToPb(queryApp), + ProjectId: queryApp.ProjectID, } } @@ -153,6 +154,10 @@ func applicationFilterToQuery(applicationFilter *application.ApplicationSearchFi return query.NewAppStateSearchQuery(domain.AppState(q.StateFilter)) case *application.ApplicationSearchFilter_TypeFilter: return applicationTypeFilterToQuery(q.TypeFilter) + case *application.ApplicationSearchFilter_ClientIdFilter: + return query.NewAppClientIDSearchQuery(strings.TrimSpace(q.ClientIdFilter.GetClientId())) + case *application.ApplicationSearchFilter_EntityIdFilter: + return query.NewAppSAMLEntityIDSearchQuery(strings.TrimSpace(q.EntityIdFilter.GetEntityId())) default: return nil, zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid") } diff --git a/internal/api/grpc/application/v2/convert/convert_test.go b/internal/api/grpc/application/v2/convert/convert_test.go index fa2f27810..1b77ad3dd 100644 --- a/internal/api/grpc/application/v2/convert/convert_test.go +++ b/internal/api/grpc/application/v2/convert/convert_test.go @@ -42,6 +42,7 @@ func TestAppToPb(t *testing.T) { State: domain.AppStateActive, Name: "test-application", APIConfig: &query.APIApp{}, + ProjectID: "project1", }, expectedPbApp: &application.Application{ ApplicationId: "id", @@ -52,6 +53,7 @@ func TestAppToPb(t *testing.T) { Configuration: &application.Application_ApiConfiguration{ ApiConfiguration: &application.APIConfiguration{}, }, + ProjectId: "project1", }, }, { @@ -82,6 +84,12 @@ func TestListApplicationsRequestToModel(t *testing.T) { validSearchByProjectQuery, err := query.NewAppProjectIDSearchQuery("project1") require.NoError(t, err) + validSearchByClientIDQuery, err := query.NewAppClientIDSearchQuery("clientID") + require.NoError(t, err) + + validSearchByEntityIDQuery, err := query.NewAppSAMLEntityIDSearchQuery("entityID") + require.NoError(t, err) + sysDefaults := systemdefaults.SystemDefaults{DefaultQueryLimit: 100, MaxQueryLimit: 150} tt := []struct { @@ -125,6 +133,12 @@ func TestListApplicationsRequestToModel(t *testing.T) { { Filter: &application.ApplicationSearchFilter_NameFilter{NameFilter: &application.ApplicationNameFilter{Name: "test"}}, }, + { + Filter: &application.ApplicationSearchFilter_ClientIdFilter{ClientIdFilter: &application.ClientIDFilter{ClientId: "clientID"}}, + }, + { + Filter: &application.ApplicationSearchFilter_EntityIdFilter{EntityIdFilter: &application.EntityIDFilter{EntityId: "entityID"}}, + }, }, SortingColumn: application.ApplicationSorting_APPLICATION_SORT_BY_NAME, Pagination: &filter_pb_v2.PaginationRequest{Asc: true}, @@ -140,6 +154,8 @@ func TestListApplicationsRequestToModel(t *testing.T) { Queries: []query.SearchQuery{ validSearchByProjectQuery, validSearchByNameQuery, + validSearchByClientIDQuery, + validSearchByEntityIDQuery, }, }, }, diff --git a/internal/api/grpc/application/v2/integration_test/query_test.go b/internal/api/grpc/application/v2/integration_test/query_test.go index c3791dceb..8718fab0d 100644 --- a/internal/api/grpc/application/v2/integration_test/query_test.go +++ b/internal/api/grpc/application/v2/integration_test/query_test.go @@ -169,7 +169,7 @@ func TestListApplications(t *testing.T) { createdDeactivatedApiApp, deactivatedApiAppName := createAPIAppWithName(t, IAMOwnerCtx, instance, p.GetId()) deactivateApp(t, createdDeactivatedApiApp, p.GetId()) - _, createdSAMLApp, samlAppName := createSAMLAppWithName(t, integration.URL(), p.GetId()) + _, createdSAMLApp, samlAppName, samlEntityID := createSAMLAppWithName(t, integration.URL(), p.GetId()) createdOIDCApp, oidcAppName := createOIDCAppWithName(t, integration.URL(), p.GetId()) @@ -480,6 +480,99 @@ func TestListApplications(t *testing.T) { creationDates[i] = a.GetCreationDate().AsTime() } + return creationDates + }, + }, + { + testName: "when filtering by clientID should return matching OIDC app only", + inputCtx: IAMOwnerCtx, + inputRequest: &application.ListApplicationsRequest{ + Pagination: &filter.PaginationRequest{Asc: true}, + Filters: []*application.ApplicationSearchFilter{ + { + Filter: &application.ApplicationSearchFilter_ClientIdFilter{ + ClientIdFilter: &application.ClientIDFilter{ClientId: createdOIDCApp.GetOidcConfiguration().GetClientId()}, + }, + }, + }, + }, + expectedOrderedList: []appWithName{{name: oidcAppName, app: createdOIDCApp}}, + expectedOrderedKeys: func(apps []appWithName) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.app.GetCreationDate().AsTime() + } + + return creationDates + }, + actualOrderedKeys: func(apps []*application.Application) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.GetCreationDate().AsTime() + } + + return creationDates + }, + }, + { + testName: "when filtering by clientID should return matching API app only", + inputCtx: IAMOwnerCtx, + inputRequest: &application.ListApplicationsRequest{ + Pagination: &filter.PaginationRequest{Asc: true}, + Filters: []*application.ApplicationSearchFilter{ + { + Filter: &application.ApplicationSearchFilter_ClientIdFilter{ + ClientIdFilter: &application.ClientIDFilter{ClientId: createdApiApp.GetApiConfiguration().GetClientId()}, + }, + }, + }, + }, + expectedOrderedList: []appWithName{{name: apiAppName, app: createdApiApp}}, + expectedOrderedKeys: func(apps []appWithName) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.app.GetCreationDate().AsTime() + } + + return creationDates + }, + actualOrderedKeys: func(apps []*application.Application) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.GetCreationDate().AsTime() + } + + return creationDates + }, + }, + { + testName: "when filtering by entityID should return matching SAML app only", + inputCtx: IAMOwnerCtx, + inputRequest: &application.ListApplicationsRequest{ + Pagination: &filter.PaginationRequest{Asc: true}, + Filters: []*application.ApplicationSearchFilter{ + { + Filter: &application.ApplicationSearchFilter_EntityIdFilter{ + EntityIdFilter: &application.EntityIDFilter{EntityId: samlEntityID}, + }, + }, + }, + }, + expectedOrderedList: []appWithName{{name: samlAppName, app: createdSAMLApp}}, + expectedOrderedKeys: func(apps []appWithName) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.app.GetCreationDate().AsTime() + } + + return creationDates + }, + actualOrderedKeys: func(apps []*application.Application) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.GetCreationDate().AsTime() + } + return creationDates }, }, diff --git a/internal/api/grpc/application/v2/integration_test/server_test.go b/internal/api/grpc/application/v2/integration_test/server_test.go index 9448cae81..3ceb78114 100644 --- a/internal/api/grpc/application/v2/integration_test/server_test.go +++ b/internal/api/grpc/application/v2/integration_test/server_test.go @@ -80,8 +80,9 @@ func samlMetadataGen(entityID string) []byte { return []byte(str) } -func createSAMLAppWithName(t *testing.T, baseURI, projectID string) ([]byte, *application.CreateApplicationResponse, string) { - samlMetas := samlMetadataGen(integration.URL()) +func createSAMLAppWithName(t *testing.T, baseURI, projectID string) ([]byte, *application.CreateApplicationResponse, string, string) { + entityID := integration.URL() + samlMetas := samlMetadataGen(entityID) appName := integration.ApplicationName() appForSAMLConfigChange, appSAMLConfigChangeErr := instance.Client.ApplicationV2.CreateApplication(IAMOwnerCtx, &application.CreateApplicationRequest{ @@ -104,11 +105,11 @@ func createSAMLAppWithName(t *testing.T, baseURI, projectID string) ([]byte, *ap }) require.Nil(t, appSAMLConfigChangeErr) - return samlMetas, appForSAMLConfigChange, appName + return samlMetas, appForSAMLConfigChange, appName, entityID } func createSAMLApp(t *testing.T, baseURI, projectID string) ([]byte, *application.CreateApplicationResponse) { - metas, app, _ := createSAMLAppWithName(t, baseURI, projectID) + metas, app, _, _ := createSAMLAppWithName(t, baseURI, projectID) return metas, app } diff --git a/internal/query/app.go b/internal/query/app.go index 777c29513..99c68b929 100644 --- a/internal/query/app.go +++ b/internal/query/app.go @@ -670,6 +670,22 @@ func NewAppProjectIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(AppColumnProjectID, id, TextEquals) } +func NewAppClientIDSearchQuery(clientID string) (SearchQuery, error) { + oidcClientIDQuery, err := NewTextQuery(AppOIDCConfigColumnClientID, clientID, TextEquals) + if err != nil { + return nil, err + } + apiClientIDQuery, err := NewTextQuery(AppAPIConfigColumnClientID, clientID, TextEquals) + if err != nil { + return nil, err + } + return NewOrQuery(oidcClientIDQuery, apiClientIDQuery) +} + +func NewAppSAMLEntityIDSearchQuery(entityID string) (SearchQuery, error) { + return NewTextQuery(AppSAMLConfigColumnEntityID, entityID, TextEquals) +} + func prepareAppQuery(activeOnly bool) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { query := sq.Select( AppColumnID.identifier(), diff --git a/proto/zitadel/application/v2/application.proto b/proto/zitadel/application/v2/application.proto index 9c32df68f..a42a104fc 100644 --- a/proto/zitadel/application/v2/application.proto +++ b/proto/zitadel/application/v2/application.proto @@ -37,6 +37,9 @@ message Application { APIConfiguration api_configuration = 7; SAMLConfiguration saml_configuration = 8; } + + // The ProjectID represents the ID of the project the application belongs to. + string project_id = 9; } enum ApplicationState { @@ -69,6 +72,14 @@ message ApplicationSearchFilter { // Filter for specific application types. ApplicationType type_filter = 4 [(validate.rules).enum = {defined_only: true, not_in:[0]}]; + + // Filter the applications by the clientID + // This is only applicable for OIDC and API applications. + ClientIDFilter client_id_filter = 5; + + // Filter the applications by their entityID + // This is only applicable for SAML applications. + EntityIDFilter entity_id_filter = 6; } } @@ -95,6 +106,30 @@ message ApplicationNameFilter { zitadel.filter.v2.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; } +message ClientIDFilter { + // The clientID to search for. The search is performed as an exact match. + string client_id = 1 [ + (validate.rules).string = { + min_len: 1, + max_len: 200 + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"client-12345\""}, + (google.api.field_behavior) = REQUIRED + ]; +} + +message EntityIDFilter { + // The entityID to search for. The search is performed as an exact match. + string entity_id = 1 [ + (validate.rules).string = { + min_len: 1, + max_len: 200 + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"https://example.com/saml/v2/metadata\""}, + (google.api.field_behavior) = REQUIRED + ]; +} + enum ApplicationType { APPLICATION_TYPE_UNSPECIFIED = 0; APPLICATION_TYPE_OIDC = 1;