package query import ( "context" "database/sql" errs "errors" "time" sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query/projection" ) type Apps struct { SearchResponse Apps []*App } type App struct { ID string CreationDate time.Time ChangeDate time.Time ResourceOwner string State domain.AppState Sequence uint64 ProjectID string Name string OIDCConfig *OIDCApp APIConfig *APIApp } type OIDCApp struct { RedirectURIs database.StringArray ResponseTypes database.EnumArray[domain.OIDCResponseType] GrantTypes database.EnumArray[domain.OIDCGrantType] AppType domain.OIDCApplicationType ClientID string AuthMethodType domain.OIDCAuthMethodType PostLogoutRedirectURIs database.StringArray Version domain.OIDCVersion ComplianceProblems database.StringArray IsDevMode bool AccessTokenType domain.OIDCTokenType AssertAccessTokenRole bool AssertIDTokenRole bool AssertIDTokenUserinfo bool ClockSkew time.Duration AdditionalOrigins database.StringArray AllowedOrigins database.StringArray } type APIApp struct { ClientID string AuthMethodType domain.APIAuthMethodType } type AppSearchQueries struct { SearchRequest Queries []SearchQuery } func (q *AppSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { query = q.SearchRequest.toQuery(query) for _, q := range q.Queries { query = q.toQuery(query) } return query } var ( appsTable = table{ name: projection.AppProjectionTable, } AppColumnID = Column{ name: projection.AppColumnID, table: appsTable, } AppColumnName = Column{ name: projection.AppColumnName, table: appsTable, } AppColumnProjectID = Column{ name: projection.AppColumnProjectID, table: appsTable, } AppColumnCreationDate = Column{ name: projection.AppColumnCreationDate, table: appsTable, } AppColumnChangeDate = Column{ name: projection.AppColumnChangeDate, table: appsTable, } AppColumnResourceOwner = Column{ name: projection.AppColumnResourceOwner, table: appsTable, } AppColumnInstanceID = Column{ name: projection.AppColumnInstanceID, table: appsTable, } AppColumnState = Column{ name: projection.AppColumnState, table: appsTable, } AppColumnSequence = Column{ name: projection.AppColumnSequence, table: appsTable, } ) var ( appAPIConfigsTable = table{ name: projection.AppAPITable, } AppAPIConfigColumnAppID = Column{ name: projection.AppAPIConfigColumnAppID, table: appAPIConfigsTable, } AppAPIConfigColumnClientID = Column{ name: projection.AppAPIConfigColumnClientID, table: appAPIConfigsTable, } AppAPIConfigColumnAuthMethod = Column{ name: projection.AppAPIConfigColumnAuthMethod, table: appAPIConfigsTable, } ) var ( appOIDCConfigsTable = table{ name: projection.AppOIDCTable, } AppOIDCConfigColumnAppID = Column{ name: projection.AppOIDCConfigColumnAppID, table: appOIDCConfigsTable, } AppOIDCConfigColumnVersion = Column{ name: projection.AppOIDCConfigColumnVersion, table: appOIDCConfigsTable, } AppOIDCConfigColumnClientID = Column{ name: projection.AppOIDCConfigColumnClientID, table: appOIDCConfigsTable, } AppOIDCConfigColumnRedirectUris = Column{ name: projection.AppOIDCConfigColumnRedirectUris, table: appOIDCConfigsTable, } AppOIDCConfigColumnResponseTypes = Column{ name: projection.AppOIDCConfigColumnResponseTypes, table: appOIDCConfigsTable, } AppOIDCConfigColumnGrantTypes = Column{ name: projection.AppOIDCConfigColumnGrantTypes, table: appOIDCConfigsTable, } AppOIDCConfigColumnApplicationType = Column{ name: projection.AppOIDCConfigColumnApplicationType, table: appOIDCConfigsTable, } AppOIDCConfigColumnAuthMethodType = Column{ name: projection.AppOIDCConfigColumnAuthMethodType, table: appOIDCConfigsTable, } AppOIDCConfigColumnPostLogoutRedirectUris = Column{ name: projection.AppOIDCConfigColumnPostLogoutRedirectUris, table: appOIDCConfigsTable, } AppOIDCConfigColumnDevMode = Column{ name: projection.AppOIDCConfigColumnDevMode, table: appOIDCConfigsTable, } AppOIDCConfigColumnAccessTokenType = Column{ name: projection.AppOIDCConfigColumnAccessTokenType, table: appOIDCConfigsTable, } AppOIDCConfigColumnAccessTokenRoleAssertion = Column{ name: projection.AppOIDCConfigColumnAccessTokenRoleAssertion, table: appOIDCConfigsTable, } AppOIDCConfigColumnIDTokenRoleAssertion = Column{ name: projection.AppOIDCConfigColumnIDTokenRoleAssertion, table: appOIDCConfigsTable, } AppOIDCConfigColumnIDTokenUserinfoAssertion = Column{ name: projection.AppOIDCConfigColumnIDTokenUserinfoAssertion, table: appOIDCConfigsTable, } AppOIDCConfigColumnClockSkew = Column{ name: projection.AppOIDCConfigColumnClockSkew, table: appOIDCConfigsTable, } AppOIDCConfigColumnAdditionalOrigins = Column{ name: projection.AppOIDCConfigColumnAdditionalOrigins, table: appOIDCConfigsTable, } ) func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bool, projectID, appID string) (*App, error) { if shouldTriggerBulk { projection.AppProjection.Trigger(ctx) } stmt, scan := prepareAppQuery() query, args, err := stmt.Where( sq.Eq{ AppColumnID.identifier(): appID, AppColumnProjectID.identifier(): projectID, AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }, ).ToSql() if err != nil { return nil, errors.ThrowInternal(err, "QUERY-AFDgg", "Errors.Query.SQLStatement") } row := q.client.QueryRowContext(ctx, query, args...) return scan(row) } func (q *Queries) ProjectIDFromOIDCClientID(ctx context.Context, appID string) (string, error) { stmt, scan := prepareProjectIDByAppQuery() query, args, err := stmt.Where( sq.Eq{ AppOIDCConfigColumnClientID.identifier(): appID, AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }, ).ToSql() if err != nil { return "", errors.ThrowInternal(err, "QUERY-7d92U", "Errors.Query.SQLStatement") } row := q.client.QueryRowContext(ctx, query, args...) return scan(row) } func (q *Queries) ProjectIDFromClientID(ctx context.Context, appID string) (string, error) { stmt, scan := prepareProjectIDByAppQuery() query, args, err := stmt.Where( sq.And{ sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}, sq.Or{ sq.Eq{AppOIDCConfigColumnClientID.identifier(): appID}, sq.Eq{AppAPIConfigColumnClientID.identifier(): appID}, }, }, ).ToSql() if err != nil { return "", errors.ThrowInternal(err, "QUERY-SDfg3", "Errors.Query.SQLStatement") } row := q.client.QueryRowContext(ctx, query, args...) return scan(row) } func (q *Queries) ProjectByOIDCClientID(ctx context.Context, id string) (*Project, error) { stmt, scan := prepareProjectByAppQuery() query, args, err := stmt.Where( sq.Eq{ AppOIDCConfigColumnClientID.identifier(): id, AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }, ).ToSql() if err != nil { return nil, errors.ThrowInternal(err, "QUERY-XhJi4", "Errors.Query.SQLStatement") } row := q.client.QueryRowContext(ctx, query, args...) return scan(row) } func (q *Queries) AppByOIDCClientID(ctx context.Context, clientID string) (*App, error) { stmt, scan := prepareAppQuery() query, args, err := stmt.Where( sq.Eq{ AppOIDCConfigColumnClientID.identifier(): clientID, AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }, ).ToSql() if err != nil { return nil, errors.ThrowInternal(err, "QUERY-JgVop", "Errors.Query.SQLStatement") } row := q.client.QueryRowContext(ctx, query, args...) return scan(row) } func (q *Queries) AppByClientID(ctx context.Context, clientID string) (*App, error) { stmt, scan := prepareAppQuery() query, args, err := stmt.Where( sq.And{ sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}, sq.Or{ sq.Eq{AppOIDCConfigColumnClientID.identifier(): clientID}, sq.Eq{AppAPIConfigColumnClientID.identifier(): clientID}, }, }, ).ToSql() if err != nil { return nil, errors.ThrowInternal(err, "QUERY-Dfge2", "Errors.Query.SQLStatement") } row := q.client.QueryRowContext(ctx, query, args...) return scan(row) } func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries) (*Apps, error) { query, scan := prepareAppsQuery() stmt, args, err := queries.toQuery(query). Where( sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}, ).ToSql() if err != nil { return nil, errors.ThrowInvalidArgument(err, "QUERY-fajp8", "Errors.Query.InvalidRequest") } rows, err := q.client.QueryContext(ctx, stmt, args...) if err != nil { return nil, errors.ThrowInternal(err, "QUERY-aJnZL", "Errors.Internal") } apps, err := scan(rows) if err != nil { return nil, err } apps.LatestSequence, err = q.latestSequence(ctx, appsTable) return apps, err } func (q *Queries) SearchClientIDs(ctx context.Context, queries *AppSearchQueries) ([]string, error) { query, scan := prepareClientIDsQuery() stmt, args, err := queries.toQuery(query). Where( sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}, ).ToSql() if err != nil { return nil, errors.ThrowInvalidArgument(err, "QUERY-fajp8", "Errors.Query.InvalidRequest") } rows, err := q.client.QueryContext(ctx, stmt, args...) if err != nil { return nil, errors.ThrowInternal(err, "QUERY-aJnZL", "Errors.Internal") } return scan(rows) } func NewAppNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { return NewTextQuery(AppColumnName, value, method) } func NewAppProjectIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(AppColumnProjectID, id, TextEquals) } func prepareAppQuery() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { return sq.Select( AppColumnID.identifier(), AppColumnName.identifier(), AppColumnProjectID.identifier(), AppColumnCreationDate.identifier(), AppColumnChangeDate.identifier(), AppColumnResourceOwner.identifier(), AppColumnState.identifier(), AppColumnSequence.identifier(), AppAPIConfigColumnAppID.identifier(), AppAPIConfigColumnClientID.identifier(), AppAPIConfigColumnAuthMethod.identifier(), AppOIDCConfigColumnAppID.identifier(), AppOIDCConfigColumnVersion.identifier(), AppOIDCConfigColumnClientID.identifier(), AppOIDCConfigColumnRedirectUris.identifier(), AppOIDCConfigColumnResponseTypes.identifier(), AppOIDCConfigColumnGrantTypes.identifier(), AppOIDCConfigColumnApplicationType.identifier(), AppOIDCConfigColumnAuthMethodType.identifier(), AppOIDCConfigColumnPostLogoutRedirectUris.identifier(), AppOIDCConfigColumnDevMode.identifier(), AppOIDCConfigColumnAccessTokenType.identifier(), AppOIDCConfigColumnAccessTokenRoleAssertion.identifier(), AppOIDCConfigColumnIDTokenRoleAssertion.identifier(), AppOIDCConfigColumnIDTokenUserinfoAssertion.identifier(), AppOIDCConfigColumnClockSkew.identifier(), AppOIDCConfigColumnAdditionalOrigins.identifier(), ).From(appsTable.identifier()). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*App, error) { app := new(App) var ( apiConfig = sqlAPIConfig{} oidcConfig = sqlOIDCConfig{} ) err := row.Scan( &app.ID, &app.Name, &app.ProjectID, &app.CreationDate, &app.ChangeDate, &app.ResourceOwner, &app.State, &app.Sequence, &apiConfig.appID, &apiConfig.clientID, &apiConfig.authMethod, &oidcConfig.appID, &oidcConfig.version, &oidcConfig.clientID, &oidcConfig.redirectUris, &oidcConfig.responseTypes, &oidcConfig.grantTypes, &oidcConfig.applicationType, &oidcConfig.authMethodType, &oidcConfig.postLogoutRedirectUris, &oidcConfig.devMode, &oidcConfig.accessTokenType, &oidcConfig.accessTokenRoleAssertion, &oidcConfig.iDTokenRoleAssertion, &oidcConfig.iDTokenUserinfoAssertion, &oidcConfig.clockSkew, &oidcConfig.additionalOrigins, ) if err != nil { if errs.Is(err, sql.ErrNoRows) { return nil, errors.ThrowNotFound(err, "QUERY-pCP8P", "Errors.App.NotExisting") } return nil, errors.ThrowInternal(err, "QUERY-0R2Nw", "Errors.Internal") } apiConfig.set(app) oidcConfig.set(app) return app, nil } } func prepareProjectIDByAppQuery() (sq.SelectBuilder, func(*sql.Row) (projectID string, err error)) { return sq.Select( AppColumnProjectID.identifier(), ).From(appsTable.identifier()). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (projectID string, err error) { err = row.Scan( &projectID, ) if err != nil { if errs.Is(err, sql.ErrNoRows) { return "", errors.ThrowNotFound(err, "QUERY-aKcc2", "Errors.Project.NotExisting") } return "", errors.ThrowInternal(err, "QUERY-3A5TG", "Errors.Internal") } return projectID, nil } } func prepareProjectByAppQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { return sq.Select( ProjectColumnID.identifier(), ProjectColumnCreationDate.identifier(), ProjectColumnChangeDate.identifier(), ProjectColumnResourceOwner.identifier(), ProjectColumnState.identifier(), ProjectColumnSequence.identifier(), ProjectColumnName.identifier(), ProjectColumnProjectRoleAssertion.identifier(), ProjectColumnProjectRoleCheck.identifier(), ProjectColumnHasProjectCheck.identifier(), ProjectColumnPrivateLabelingSetting.identifier(), ).From(projectsTable.identifier()). Join(join(AppColumnProjectID, ProjectColumnID)). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Project, error) { p := new(Project) err := row.Scan( &p.ID, &p.CreationDate, &p.ChangeDate, &p.ResourceOwner, &p.State, &p.Sequence, &p.Name, &p.ProjectRoleAssertion, &p.ProjectRoleCheck, &p.HasProjectCheck, &p.PrivateLabelingSetting, ) if err != nil { if errs.Is(err, sql.ErrNoRows) { return nil, errors.ThrowNotFound(err, "QUERY-fk2fs", "Errors.Project.NotFound") } return nil, errors.ThrowInternal(err, "QUERY-dj2FF", "Errors.Internal") } return p, nil } } func prepareAppsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Apps, error)) { return sq.Select( AppColumnID.identifier(), AppColumnName.identifier(), AppColumnProjectID.identifier(), AppColumnCreationDate.identifier(), AppColumnChangeDate.identifier(), AppColumnResourceOwner.identifier(), AppColumnState.identifier(), AppColumnSequence.identifier(), AppAPIConfigColumnAppID.identifier(), AppAPIConfigColumnClientID.identifier(), AppAPIConfigColumnAuthMethod.identifier(), AppOIDCConfigColumnAppID.identifier(), AppOIDCConfigColumnVersion.identifier(), AppOIDCConfigColumnClientID.identifier(), AppOIDCConfigColumnRedirectUris.identifier(), AppOIDCConfigColumnResponseTypes.identifier(), AppOIDCConfigColumnGrantTypes.identifier(), AppOIDCConfigColumnApplicationType.identifier(), AppOIDCConfigColumnAuthMethodType.identifier(), AppOIDCConfigColumnPostLogoutRedirectUris.identifier(), AppOIDCConfigColumnDevMode.identifier(), AppOIDCConfigColumnAccessTokenType.identifier(), AppOIDCConfigColumnAccessTokenRoleAssertion.identifier(), AppOIDCConfigColumnIDTokenRoleAssertion.identifier(), AppOIDCConfigColumnIDTokenUserinfoAssertion.identifier(), AppOIDCConfigColumnClockSkew.identifier(), AppOIDCConfigColumnAdditionalOrigins.identifier(), countColumn.identifier(), ).From(appsTable.identifier()). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Rows) (*Apps, error) { apps := &Apps{Apps: []*App{}} for row.Next() { app := new(App) var ( apiConfig = sqlAPIConfig{} oidcConfig = sqlOIDCConfig{} ) err := row.Scan( &app.ID, &app.Name, &app.ProjectID, &app.CreationDate, &app.ChangeDate, &app.ResourceOwner, &app.State, &app.Sequence, &apiConfig.appID, &apiConfig.clientID, &apiConfig.authMethod, &oidcConfig.appID, &oidcConfig.version, &oidcConfig.clientID, &oidcConfig.redirectUris, &oidcConfig.responseTypes, &oidcConfig.grantTypes, &oidcConfig.applicationType, &oidcConfig.authMethodType, &oidcConfig.postLogoutRedirectUris, &oidcConfig.devMode, &oidcConfig.accessTokenType, &oidcConfig.accessTokenRoleAssertion, &oidcConfig.iDTokenRoleAssertion, &oidcConfig.iDTokenUserinfoAssertion, &oidcConfig.clockSkew, &oidcConfig.additionalOrigins, &apps.Count, ) if err != nil { return nil, errors.ThrowInternal(err, "QUERY-0R2Nw", "Errors.Internal") } apiConfig.set(app) oidcConfig.set(app) apps.Apps = append(apps.Apps, app) } return apps, nil } } func prepareClientIDsQuery() (sq.SelectBuilder, func(*sql.Rows) ([]string, error)) { return sq.Select( AppAPIConfigColumnClientID.identifier(), AppOIDCConfigColumnClientID.identifier(), ).From(appsTable.identifier()). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) ([]string, error) { ids := database.StringArray{} for rows.Next() { var apiID sql.NullString var oidcID sql.NullString if err := rows.Scan( &apiID, &oidcID, ); err != nil { return nil, errors.ThrowInternal(err, "QUERY-0R2Nw", "Errors.Internal") } if apiID.Valid { ids = append(ids, apiID.String) } else if oidcID.Valid { ids = append(ids, oidcID.String) } } return ids, nil } } type sqlOIDCConfig struct { appID sql.NullString version sql.NullInt32 clientID sql.NullString redirectUris database.StringArray applicationType sql.NullInt16 authMethodType sql.NullInt16 postLogoutRedirectUris database.StringArray devMode sql.NullBool accessTokenType sql.NullInt16 accessTokenRoleAssertion sql.NullBool iDTokenRoleAssertion sql.NullBool iDTokenUserinfoAssertion sql.NullBool clockSkew sql.NullInt64 additionalOrigins database.StringArray responseTypes database.EnumArray[domain.OIDCResponseType] grantTypes database.EnumArray[domain.OIDCGrantType] } func (c sqlOIDCConfig) set(app *App) { if !c.appID.Valid { return } app.OIDCConfig = &OIDCApp{ Version: domain.OIDCVersion(c.version.Int32), ClientID: c.clientID.String, RedirectURIs: c.redirectUris, AppType: domain.OIDCApplicationType(c.applicationType.Int16), AuthMethodType: domain.OIDCAuthMethodType(c.authMethodType.Int16), PostLogoutRedirectURIs: c.postLogoutRedirectUris, IsDevMode: c.devMode.Bool, AccessTokenType: domain.OIDCTokenType(c.accessTokenType.Int16), AssertAccessTokenRole: c.accessTokenRoleAssertion.Bool, AssertIDTokenRole: c.iDTokenRoleAssertion.Bool, AssertIDTokenUserinfo: c.iDTokenUserinfoAssertion.Bool, ClockSkew: time.Duration(c.clockSkew.Int64), AdditionalOrigins: c.additionalOrigins, ResponseTypes: c.responseTypes, GrantTypes: c.grantTypes, } compliance := domain.GetOIDCCompliance(app.OIDCConfig.Version, app.OIDCConfig.AppType, app.OIDCConfig.GrantTypes, app.OIDCConfig.ResponseTypes, app.OIDCConfig.AuthMethodType, app.OIDCConfig.RedirectURIs) app.OIDCConfig.ComplianceProblems = compliance.Problems var err error app.OIDCConfig.AllowedOrigins, err = domain.OIDCOriginAllowList(app.OIDCConfig.RedirectURIs, app.OIDCConfig.AdditionalOrigins) logging.LogWithFields("app", app.ID).OnError(err).Warn("unable to set allowed origins") } type sqlAPIConfig struct { appID sql.NullString clientID sql.NullString authMethod sql.NullInt16 } func (c sqlAPIConfig) set(app *App) { if !c.appID.Valid { return } app.APIConfig = &APIApp{ ClientID: c.clientID.String, AuthMethodType: domain.APIAuthMethodType(c.authMethod.Int16), } }