feat: App API v2 (#10077)

# Which Problems Are Solved

This PR *partially* addresses #9450 . Specifically, it implements the
resource based API for the apps. APIs for app keys ARE not part of this
PR.

# How the Problems Are Solved

- `CreateApplication`, `PatchApplication` (update) and
`RegenerateClientSecret` endpoints are now unique for all app types:
API, SAML and OIDC apps.
  - All new endpoints have integration tests
  - All new endpoints are using permission checks V2

# Additional Changes

- The `ListApplications` endpoint allows to do sorting (see protobuf for
details) and filtering by app type (see protobuf).
- SAML and OIDC update endpoint can now receive requests for partial
updates

# Additional Context

Partially addresses #9450
This commit is contained in:
Marco A.
2025-06-27 17:25:44 +02:00
committed by GitHub
parent 016676e1dc
commit 2691dae2b6
48 changed files with 6845 additions and 603 deletions

View File

@@ -5,9 +5,11 @@ import (
"database/sql"
_ "embed"
"errors"
"slices"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/muhlemmer/gu"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
@@ -307,6 +309,19 @@ func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bo
return app, err
}
func (q *Queries) AppByIDWithPermission(ctx context.Context, appID string, activeOnly bool, permissionCheck domain.PermissionCheck) (*App, error) {
app, err := q.AppByID(ctx, appID, activeOnly)
if err != nil {
return nil, err
}
if err := appCheckPermission(ctx, app.ResourceOwner, app.ProjectID, permissionCheck); err != nil {
return nil, err
}
return app, nil
}
func (q *Queries) AppByID(ctx context.Context, appID string, activeOnly bool) (app *App, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -476,11 +491,54 @@ func (q *Queries) AppByOIDCClientID(ctx context.Context, clientID string) (app *
return app, err
}
func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, withOwnerRemoved bool) (apps *Apps, err error) {
func (q *Queries) AppByClientID(ctx context.Context, clientID string) (app *App, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
stmt, scan := prepareAppQuery(true)
eq := sq.Eq{
AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
AppColumnState.identifier(): domain.AppStateActive,
ProjectColumnState.identifier(): domain.ProjectStateActive,
OrgColumnState.identifier(): domain.OrgStateActive,
}
query, args, err := stmt.Where(sq.And{
eq,
sq.Or{
sq.Eq{AppOIDCConfigColumnClientID.identifier(): clientID},
sq.Eq{AppAPIConfigColumnClientID.identifier(): clientID},
},
}).ToSql()
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Dfge2", "Errors.Query.SQLStatement")
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
app, err = scan(row)
return err
}, query, args...)
return app, err
}
func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, permissionCheck domain.PermissionCheck) (*Apps, error) {
apps, err := q.searchApps(ctx, queries, PermissionV2(ctx, permissionCheck))
if err != nil {
return nil, err
}
if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 {
apps.Apps = appsCheckPermission(ctx, apps.Apps, permissionCheck)
}
return apps, nil
}
func (q *Queries) searchApps(ctx context.Context, queries *AppSearchQueries, isPermissionV2Enabled bool) (apps *Apps, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareAppsQuery()
query = appPermissionCheckV2(ctx, query, isPermissionV2Enabled, queries)
eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil {
@@ -498,6 +556,21 @@ func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, wit
return apps, err
}
func appPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *AppSearchQueries) sq.SelectBuilder {
if !enabled {
return query
}
join, args := PermissionClause(
ctx,
AppColumnResourceOwner,
domain.PermissionProjectAppRead,
SingleOrgPermissionOption(queries.Queries),
WithProjectsPermissionOption(AppColumnProjectID),
)
return query.JoinClause(join, args...)
}
func (q *Queries) SearchClientIDs(ctx context.Context, queries *AppSearchQueries, shouldTriggerBulk bool) (ids []string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -574,10 +647,25 @@ func (q *Queries) SAMLAppLoginVersion(ctx context.Context, appID string) (loginV
return loginVersion, nil
}
func appCheckPermission(ctx context.Context, resourceOwner string, projectID string, permissionCheck domain.PermissionCheck) error {
return permissionCheck(ctx, domain.PermissionProjectAppRead, resourceOwner, projectID)
}
// appsCheckPermission returns only the apps that the user in context has permission to read
func appsCheckPermission(ctx context.Context, apps []*App, permissionCheck domain.PermissionCheck) []*App {
return slices.DeleteFunc(apps, func(app *App) bool {
return permissionCheck(ctx, domain.PermissionProjectAppRead, app.ResourceOwner, app.ProjectID) != nil
})
}
func NewAppNameSearchQuery(method TextComparison, value string) (SearchQuery, error) {
return NewTextQuery(AppColumnName, value, method)
}
func NewAppStateSearchQuery(value domain.AppState) (SearchQuery, error) {
return NewNumberQuery(AppColumnState, int(value), NumberEquals)
}
func NewAppProjectIDSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery(AppColumnProjectID, id, TextEquals)
}
@@ -1089,7 +1177,7 @@ func (c sqlOIDCConfig) set(app *App) {
if c.loginBaseURI.Valid {
app.OIDCConfig.LoginBaseURI = &c.loginBaseURI.String
}
compliance := domain.GetOIDCCompliance(app.OIDCConfig.Version, app.OIDCConfig.AppType, app.OIDCConfig.GrantTypes, app.OIDCConfig.ResponseTypes, app.OIDCConfig.AuthMethodType, app.OIDCConfig.RedirectURIs)
compliance := domain.GetOIDCCompliance(gu.Ptr(app.OIDCConfig.Version), gu.Ptr(app.OIDCConfig.AppType), app.OIDCConfig.GrantTypes, app.OIDCConfig.ResponseTypes, gu.Ptr(app.OIDCConfig.AuthMethodType), app.OIDCConfig.RedirectURIs)
app.OIDCConfig.ComplianceProblems = compliance.Problems
var err error