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

@@ -7,6 +7,7 @@ import (
"net/http"
"testing"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/api/authz"
@@ -49,6 +50,8 @@ var testMetadataChangedEntityID = []byte(`<?xml version="1.0"?>
`)
func TestCommandSide_AddSAMLApplication(t *testing.T) {
t.Parallel()
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
@@ -117,6 +120,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
domain.PrivateLabelingSettingUnspecified),
),
),
expectFilter(),
),
},
args: args{
@@ -134,6 +138,37 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "empty metas, invalid argument error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingUnspecified),
),
),
expectFilter(),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppName: "app",
EntityID: "https://test.com/saml/metadata",
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "create saml app, metadata not parsable",
fields: fields{
@@ -146,6 +181,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
domain.PrivateLabelingSettingUnspecified),
),
),
expectFilter(),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t),
},
@@ -158,7 +194,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: []byte("test metadata"),
MetadataURL: "",
MetadataURL: gu.Ptr(""),
},
resourceOwner: "org1",
},
@@ -178,6 +214,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
domain.PrivateLabelingSettingUnspecified),
),
),
expectFilter(),
expectPush(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -206,7 +243,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: "",
MetadataURL: gu.Ptr(""),
},
resourceOwner: "org1",
},
@@ -216,12 +253,14 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
AggregateID: "project1",
ResourceOwner: "org1",
},
AppID: "app1",
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: "",
State: domain.AppStateActive,
AppID: "app1",
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: gu.Ptr(""),
State: domain.AppStateActive,
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
LoginBaseURI: gu.Ptr(""),
},
},
},
@@ -237,6 +276,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
domain.PrivateLabelingSettingUnspecified),
),
),
expectFilter(),
expectPush(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -265,9 +305,9 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: "",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: "https://test.com/login",
MetadataURL: gu.Ptr(""),
LoginVersion: gu.Ptr(domain.LoginVersion2),
LoginBaseURI: gu.Ptr("https://test.com/login"),
},
resourceOwner: "org1",
},
@@ -281,10 +321,10 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: "",
MetadataURL: gu.Ptr(""),
State: domain.AppStateActive,
LoginVersion: domain.LoginVersion2,
LoginBaseURI: "https://test.com/login",
LoginVersion: gu.Ptr(domain.LoginVersion2),
LoginBaseURI: gu.Ptr("https://test.com/login"),
},
},
},
@@ -300,6 +340,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
domain.PrivateLabelingSettingUnspecified),
),
),
expectFilter(),
expectPush(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -329,7 +370,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: nil,
MetadataURL: "http://localhost:8080/saml/metadata",
MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"),
},
resourceOwner: "org1",
},
@@ -339,12 +380,14 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
AggregateID: "project1",
ResourceOwner: "org1",
},
AppID: "app1",
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: "http://localhost:8080/saml/metadata",
State: domain.AppStateActive,
AppID: "app1",
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"),
State: domain.AppStateActive,
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
LoginBaseURI: gu.Ptr(""),
},
},
},
@@ -360,6 +403,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
domain.PrivateLabelingSettingUnspecified),
),
),
expectFilter(),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t),
httpClient: newTestClient(http.StatusNotFound, nil),
@@ -373,7 +417,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: nil,
MetadataURL: "http://localhost:8080/saml/metadata",
MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"),
},
resourceOwner: "org1",
},
@@ -385,10 +429,13 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
c := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
httpClient: tt.fields.httpClient,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
httpClient: tt.fields.httpClient,
checkPermission: newMockPermissionCheckAllowed(),
}
c.setMilestonesCompletedForTest("instanceID")
got, err := c.AddSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner)
@@ -406,6 +453,8 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) {
}
func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
t.Parallel()
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
httpClient *http.Client
@@ -544,7 +593,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
AppID: "app1",
EntityID: "https://test.com/saml/metadata",
Metadata: nil,
MetadataURL: "http://localhost:8080/saml/metadata",
MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"),
},
resourceOwner: "org1",
},
@@ -590,7 +639,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
AppID: "app1",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: "",
MetadataURL: gu.Ptr(""),
},
resourceOwner: "org1",
},
@@ -646,7 +695,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: nil,
MetadataURL: "http://localhost:8080/saml/metadata",
MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"),
},
resourceOwner: "org1",
},
@@ -656,17 +705,19 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
AggregateID: "project1",
ResourceOwner: "org1",
},
AppID: "app1",
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: testMetadataChangedEntityID,
MetadataURL: "http://localhost:8080/saml/metadata",
State: domain.AppStateActive,
AppID: "app1",
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: testMetadataChangedEntityID,
MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"),
State: domain.AppStateActive,
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
LoginBaseURI: gu.Ptr(""),
},
},
},
{
name: "change saml app, ok, metadata",
name: "partial change saml app, ok, metadata",
fields: fields{
eventstore: expectEventstore(
expectFilter(
@@ -713,7 +764,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: testMetadataChangedEntityID,
MetadataURL: "",
MetadataURL: gu.Ptr(""),
},
resourceOwner: "org1",
},
@@ -723,15 +774,18 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
AggregateID: "project1",
ResourceOwner: "org1",
},
AppID: "app1",
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: testMetadataChangedEntityID,
MetadataURL: "",
State: domain.AppStateActive,
AppID: "app1",
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: testMetadataChangedEntityID,
MetadataURL: gu.Ptr(""),
State: domain.AppStateActive,
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
LoginBaseURI: gu.Ptr(""),
},
},
}, {
},
{
name: "change saml app, ok, loginversion",
fields: fields{
eventstore: expectEventstore(
@@ -781,9 +835,9 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: testMetadataChangedEntityID,
MetadataURL: "",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: "https://test.com/login",
MetadataURL: gu.Ptr(""),
LoginVersion: gu.Ptr(domain.LoginVersion2),
LoginBaseURI: gu.Ptr("https://test.com/login"),
},
resourceOwner: "org1",
},
@@ -797,10 +851,10 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: testMetadataChangedEntityID,
MetadataURL: "",
MetadataURL: gu.Ptr(""),
State: domain.AppStateActive,
LoginVersion: domain.LoginVersion2,
LoginBaseURI: "https://test.com/login",
LoginVersion: gu.Ptr(domain.LoginVersion2),
LoginBaseURI: gu.Ptr("https://test.com/login"),
},
},
},
@@ -808,11 +862,14 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
r := &Commands{
eventstore: tt.fields.eventstore(t),
httpClient: tt.fields.httpClient,
eventstore: tt.fields.eventstore(t),
httpClient: tt.fields.httpClient,
checkPermission: newMockPermissionCheckAllowed(),
}
got, err := r.ChangeSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner)
got, err := r.UpdateSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}