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,6 +5,8 @@ import (
"strings"
"time"
"github.com/muhlemmer/gu"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
@@ -120,6 +122,7 @@ func (c *Commands) AddOIDCAppCommand(app *addOIDCApp) preparation.Validation {
}
}
// TODO: Combine with AddOIDCApplication and addOIDCApplicationWithID
func (c *Commands) AddOIDCApplicationWithID(ctx context.Context, oidcApp *domain.OIDCApp, resourceOwner, appID string) (_ *domain.OIDCApp, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -142,9 +145,15 @@ func (c *Commands) AddOIDCApplication(ctx context.Context, oidcApp *domain.OIDCA
if oidcApp == nil || oidcApp.AggregateID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-34Fm0", "Errors.Project.App.Invalid")
}
if _, err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil {
projectResOwner, err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner)
if err != nil {
return nil, err
}
if resourceOwner == "" {
resourceOwner = projectResOwner
}
if oidcApp.AppName == "" || !oidcApp.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1n8df", "Errors.Project.App.Invalid")
}
@@ -162,6 +171,13 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain
defer func() { span.EndWithError(err) }()
addedApplication := NewOIDCApplicationWriteModel(oidcApp.AggregateID, resourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, addedApplication); err != nil {
return nil, err
}
if err := c.checkPermissionUpdateApplication(ctx, addedApplication.ResourceOwner, addedApplication.AggregateID); err != nil {
return nil, err
}
projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel)
oidcApp.AppID = appID
@@ -183,27 +199,27 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain
}
events = append(events, project_repo.NewOIDCConfigAddedEvent(ctx,
projectAgg,
oidcApp.OIDCVersion,
gu.Value(oidcApp.OIDCVersion),
oidcApp.AppID,
oidcApp.ClientID,
oidcApp.EncodedHash,
trimStringSliceWhiteSpaces(oidcApp.RedirectUris),
oidcApp.ResponseTypes,
oidcApp.GrantTypes,
oidcApp.ApplicationType,
oidcApp.AuthMethodType,
gu.Value(oidcApp.ApplicationType),
gu.Value(oidcApp.AuthMethodType),
trimStringSliceWhiteSpaces(oidcApp.PostLogoutRedirectUris),
oidcApp.DevMode,
oidcApp.AccessTokenType,
oidcApp.AccessTokenRoleAssertion,
oidcApp.IDTokenRoleAssertion,
oidcApp.IDTokenUserinfoAssertion,
oidcApp.ClockSkew,
gu.Value(oidcApp.DevMode),
gu.Value(oidcApp.AccessTokenType),
gu.Value(oidcApp.AccessTokenRoleAssertion),
gu.Value(oidcApp.IDTokenRoleAssertion),
gu.Value(oidcApp.IDTokenUserinfoAssertion),
gu.Value(oidcApp.ClockSkew),
trimStringSliceWhiteSpaces(oidcApp.AdditionalOrigins),
oidcApp.SkipNativeAppSuccessPage,
strings.TrimSpace(oidcApp.BackChannelLogoutURI),
oidcApp.LoginVersion,
strings.TrimSpace(oidcApp.LoginBaseURI),
gu.Value(oidcApp.SkipNativeAppSuccessPage),
strings.TrimSpace(gu.Value(oidcApp.BackChannelLogoutURI)),
gu.Value(oidcApp.LoginVersion),
strings.TrimSpace(gu.Value(oidcApp.LoginBaseURI)),
))
addedApplication.AppID = oidcApp.AppID
@@ -226,7 +242,7 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain
return result, nil
}
func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCApp, resourceOwner string) (*domain.OIDCApp, error) {
func (c *Commands) UpdateOIDCApplication(ctx context.Context, oidc *domain.OIDCApp, resourceOwner string) (*domain.OIDCApp, error) {
if !oidc.IsValid() || oidc.AppID == "" || oidc.AggregateID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-5m9fs", "Errors.Project.App.OIDCConfigInvalid")
}
@@ -241,7 +257,23 @@ func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCA
if !existingOIDC.IsOIDC() {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-GBr34", "Errors.Project.App.IsNotOIDC")
}
if err := c.eventstore.FilterToQueryReducer(ctx, existingOIDC); err != nil {
return nil, err
}
if err := c.checkPermissionUpdateApplication(ctx, existingOIDC.ResourceOwner, existingOIDC.AggregateID); err != nil {
return nil, err
}
projectAgg := ProjectAggregateFromWriteModel(&existingOIDC.WriteModel)
var backChannelLogout, loginBaseURI *string
if oidc.BackChannelLogoutURI != nil {
backChannelLogout = gu.Ptr(strings.TrimSpace(*oidc.BackChannelLogoutURI))
}
if oidc.LoginBaseURI != nil {
loginBaseURI = gu.Ptr(strings.TrimSpace(*oidc.LoginBaseURI))
}
changedEvent, hasChanged, err := existingOIDC.NewChangedEvent(
ctx,
projectAgg,
@@ -261,9 +293,9 @@ func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCA
oidc.ClockSkew,
trimStringSliceWhiteSpaces(oidc.AdditionalOrigins),
oidc.SkipNativeAppSuccessPage,
strings.TrimSpace(oidc.BackChannelLogoutURI),
backChannelLogout,
oidc.LoginVersion,
strings.TrimSpace(oidc.LoginBaseURI),
loginBaseURI,
)
if err != nil {
return nil, err
@@ -301,6 +333,11 @@ func (c *Commands) ChangeOIDCApplicationSecret(ctx context.Context, projectID, a
if !existingOIDC.IsOIDC() {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Ghrh3", "Errors.Project.App.IsNotOIDC")
}
if err := c.checkPermissionUpdateApplication(ctx, existingOIDC.ResourceOwner, existingOIDC.AggregateID); err != nil {
return nil, err
}
encodedHash, plain, err := c.newHashedSecret(ctx, c.eventstore.Filter) //nolint:staticcheck
if err != nil {
return nil, err