mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user