feat(api): allow filtering apps by client id and entity id (#11385)

# Which Problems Are Solved

There's currently no way to retrieve a project based on the client_id of
an application. A developer would need to iterate through all projects
and their applications to make the correlation.

# How the Problems Are Solved

- Added a possibility to filter for `client_id` and `entity_id` on the
ListApplications endpoint.
- Added the `project_id` on the `Application` response (used on the
ListApplications endpoint).

# Additional Changes

None

# Additional Context

- closes #11340 
- requires backport to v4.x
This commit is contained in:
Livio Spring
2026-01-22 07:41:35 +00:00
committed by GitHub
parent 597f385ca4
commit 7d6a2950b4
6 changed files with 171 additions and 5 deletions
@@ -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")
}
@@ -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,
},
},
},
@@ -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
},
},
@@ -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
}