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

@@ -8,13 +8,16 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestCommandSide_ChangeApplication(t *testing.T) {
t.Parallel()
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -35,9 +38,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) {
{
name: "invalid app missing projectid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}),
},
args: args{
ctx: context.Background(),
@@ -55,9 +56,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) {
{
name: "invalid app missing appid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}),
},
args: args{
ctx: context.Background(),
@@ -74,9 +73,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) {
{
name: "invalid app missing name, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}),
},
args: args{
ctx: context.Background(),
@@ -94,10 +91,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) {
{
name: "app not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
eventstore: expectEventstore(expectFilter()),
},
args: args{
ctx: context.Background(),
@@ -115,8 +109,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) {
{
name: "app name not changed, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -142,8 +135,14 @@ func TestCommandSide_ChangeApplication(t *testing.T) {
{
name: "app changed, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
)),
),
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -179,10 +178,13 @@ func TestCommandSide_ChangeApplication(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: newMockPermissionCheckAllowed(),
}
got, err := r.ChangeApplication(tt.args.ctx, tt.args.projectID, tt.args.app, tt.args.resourceOwner)
got, err := r.UpdateApplicationName(tt.args.ctx, tt.args.projectID, tt.args.app, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -197,8 +199,10 @@ func TestCommandSide_ChangeApplication(t *testing.T) {
}
func TestCommandSide_DeactivateApplication(t *testing.T) {
t.Parallel()
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -219,9 +223,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) {
{
name: "missing projectid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}),
},
args: args{
ctx: context.Background(),
@@ -236,9 +238,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) {
{
name: "missing appid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}),
},
args: args{
ctx: context.Background(),
@@ -253,8 +253,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) {
{
name: "app not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -271,8 +270,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) {
{
name: "app already inactive, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -299,8 +297,14 @@ func TestCommandSide_DeactivateApplication(t *testing.T) {
{
name: "app deactivate, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
)),
),
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -331,8 +335,11 @@ func TestCommandSide_DeactivateApplication(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: newMockPermissionCheckAllowed(),
}
got, err := r.DeactivateApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner)
if tt.res.err == nil {
@@ -349,8 +356,10 @@ func TestCommandSide_DeactivateApplication(t *testing.T) {
}
func TestCommandSide_ReactivateApplication(t *testing.T) {
t.Parallel()
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -371,9 +380,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) {
{
name: "missing projectid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}),
},
args: args{
ctx: context.Background(),
@@ -388,9 +395,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) {
{
name: "missing appid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}),
},
args: args{
ctx: context.Background(),
@@ -405,10 +410,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) {
{
name: "app not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
eventstore: expectEventstore(expectFilter()),
},
args: args{
ctx: context.Background(),
@@ -423,8 +425,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) {
{
name: "app already active, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -447,8 +448,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) {
{
name: "app reactivate, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -483,8 +483,11 @@ func TestCommandSide_ReactivateApplication(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: newMockPermissionCheckAllowed(),
}
got, err := r.ReactivateApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner)
if tt.res.err == nil {
@@ -501,8 +504,10 @@ func TestCommandSide_ReactivateApplication(t *testing.T) {
}
func TestCommandSide_RemoveApplication(t *testing.T) {
t.Parallel()
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -523,9 +528,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
{
name: "missing projectid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}),
},
args: args{
ctx: context.Background(),
@@ -540,9 +543,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
{
name: "missing appid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}),
},
args: args{
ctx: context.Background(),
@@ -557,10 +558,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
{
name: "app not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
eventstore: expectEventstore(expectFilter()),
},
args: args{
ctx: context.Background(),
@@ -575,8 +573,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
{
name: "app remove, entityID, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -584,6 +581,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
"app",
)),
),
expectFilter(),
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -625,8 +623,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
{
name: "app remove, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -636,6 +633,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
),
// app is not saml, or no saml config available
expectFilter(),
expectFilter(),
expectPush(
project.NewApplicationRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -661,8 +659,11 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: newMockPermissionCheckAllowed(),
}
got, err := r.RemoveApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner)
if tt.res.err == nil {