diff --git a/cmd/start/start.go b/cmd/start/start.go
index 3c3b5cb3e0..dbd6289041 100644
--- a/cmd/start/start.go
+++ b/cmd/start/start.go
@@ -36,6 +36,7 @@ import (
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
action_v2_beta "github.com/zitadel/zitadel/internal/api/grpc/action/v2beta"
"github.com/zitadel/zitadel/internal/api/grpc/admin"
+ app "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta"
"github.com/zitadel/zitadel/internal/api/grpc/auth"
feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2"
feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta"
@@ -509,6 +510,10 @@ func startAPIs(
if err := apis.RegisterService(ctx, debug_events.CreateServer(commands, queries)); err != nil {
return nil, err
}
+ if err := apis.RegisterService(ctx, app.CreateServer(commands, queries, permissionCheck)); err != nil {
+ return nil, err
+ }
+
instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...)
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.SystemAuthZ, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))
diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go
index b5d36272d4..8024cd9d6e 100644
--- a/internal/api/grpc/admin/export.go
+++ b/internal/api/grpc/admin/export.go
@@ -783,7 +783,7 @@ func (s *Server) getProjectsAndApps(ctx context.Context, org string) ([]*v1_pb.D
if err != nil {
return nil, nil, nil, nil, nil, err
}
- apps, err := s.query.SearchApps(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{appSearch}}, false)
+ apps, err := s.query.SearchApps(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{appSearch}}, nil)
if err != nil {
return nil, nil, nil, nil, nil, err
}
diff --git a/internal/api/grpc/app/v2beta/app.go b/internal/api/grpc/app/v2beta/app.go
new file mode 100644
index 0000000000..48c602f454
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/app.go
@@ -0,0 +1,208 @@
+package app
+
+import (
+ "context"
+ "strings"
+ "time"
+
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta/convert"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/zerrors"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+)
+
+func (s *Server) CreateApplication(ctx context.Context, req *app.CreateApplicationRequest) (*app.CreateApplicationResponse, error) {
+ switch t := req.GetCreationRequestType().(type) {
+ case *app.CreateApplicationRequest_ApiRequest:
+ apiApp, err := s.command.AddAPIApplication(ctx, convert.CreateAPIApplicationRequestToDomain(req.GetName(), req.GetProjectId(), req.GetId(), t.ApiRequest), "")
+ if err != nil {
+ return nil, err
+ }
+
+ return &app.CreateApplicationResponse{
+ AppId: apiApp.AppID,
+ CreationDate: timestamppb.New(apiApp.ChangeDate),
+ CreationResponseType: &app.CreateApplicationResponse_ApiResponse{
+ ApiResponse: &app.CreateAPIApplicationResponse{
+ ClientId: apiApp.ClientID,
+ ClientSecret: apiApp.ClientSecretString,
+ },
+ },
+ }, nil
+
+ case *app.CreateApplicationRequest_OidcRequest:
+ oidcAppRequest, err := convert.CreateOIDCAppRequestToDomain(req.GetName(), req.GetProjectId(), req.GetOidcRequest())
+ if err != nil {
+ return nil, err
+ }
+
+ oidcApp, err := s.command.AddOIDCApplication(ctx, oidcAppRequest, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return &app.CreateApplicationResponse{
+ AppId: oidcApp.AppID,
+ CreationDate: timestamppb.New(oidcApp.ChangeDate),
+ CreationResponseType: &app.CreateApplicationResponse_OidcResponse{
+ OidcResponse: &app.CreateOIDCApplicationResponse{
+ ClientId: oidcApp.ClientID,
+ ClientSecret: oidcApp.ClientSecretString,
+ NoneCompliant: oidcApp.Compliance.NoneCompliant,
+ ComplianceProblems: convert.ComplianceProblemsToLocalizedMessages(oidcApp.Compliance.Problems),
+ },
+ },
+ }, nil
+
+ case *app.CreateApplicationRequest_SamlRequest:
+ samlAppRequest, err := convert.CreateSAMLAppRequestToDomain(req.GetName(), req.GetProjectId(), req.GetSamlRequest())
+ if err != nil {
+ return nil, err
+ }
+
+ samlApp, err := s.command.AddSAMLApplication(ctx, samlAppRequest, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return &app.CreateApplicationResponse{
+ AppId: samlApp.AppID,
+ CreationDate: timestamppb.New(samlApp.ChangeDate),
+ CreationResponseType: &app.CreateApplicationResponse_SamlResponse{
+ SamlResponse: &app.CreateSAMLApplicationResponse{},
+ },
+ }, nil
+ default:
+ return nil, zerrors.ThrowInvalidArgument(nil, "APP-0iiN46", "unknown app type")
+ }
+}
+
+func (s *Server) UpdateApplication(ctx context.Context, req *app.UpdateApplicationRequest) (*app.UpdateApplicationResponse, error) {
+ var changedTime time.Time
+
+ if name := strings.TrimSpace(req.GetName()); name != "" {
+ updatedDetails, err := s.command.UpdateApplicationName(
+ ctx,
+ req.GetProjectId(),
+ &domain.ChangeApp{
+ AppID: req.GetId(),
+ AppName: name,
+ },
+ "",
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ changedTime = updatedDetails.EventDate
+ }
+
+ switch t := req.GetUpdateRequestType().(type) {
+ case *app.UpdateApplicationRequest_ApiConfigurationRequest:
+ updatedAPIApp, err := s.command.UpdateAPIApplication(ctx, convert.UpdateAPIApplicationConfigurationRequestToDomain(req.GetId(), req.GetProjectId(), t.ApiConfigurationRequest), "")
+ if err != nil {
+ return nil, err
+ }
+
+ changedTime = updatedAPIApp.ChangeDate
+
+ case *app.UpdateApplicationRequest_OidcConfigurationRequest:
+ oidcApp, err := convert.UpdateOIDCAppConfigRequestToDomain(req.GetId(), req.GetProjectId(), t.OidcConfigurationRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ updatedOIDCApp, err := s.command.UpdateOIDCApplication(ctx, oidcApp, "")
+ if err != nil {
+ return nil, err
+ }
+
+ changedTime = updatedOIDCApp.ChangeDate
+
+ case *app.UpdateApplicationRequest_SamlConfigurationRequest:
+ samlApp, err := convert.UpdateSAMLAppConfigRequestToDomain(req.GetId(), req.GetProjectId(), t.SamlConfigurationRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ updatedSAMLApp, err := s.command.UpdateSAMLApplication(ctx, samlApp, "")
+ if err != nil {
+ return nil, err
+ }
+
+ changedTime = updatedSAMLApp.ChangeDate
+ }
+
+ return &app.UpdateApplicationResponse{
+ ChangeDate: timestamppb.New(changedTime),
+ }, nil
+}
+
+func (s *Server) DeleteApplication(ctx context.Context, req *app.DeleteApplicationRequest) (*app.DeleteApplicationResponse, error) {
+ details, err := s.command.RemoveApplication(ctx, req.GetProjectId(), req.GetId(), "")
+ if err != nil {
+ return nil, err
+ }
+
+ return &app.DeleteApplicationResponse{
+ DeletionDate: timestamppb.New(details.EventDate),
+ }, nil
+}
+
+func (s *Server) DeactivateApplication(ctx context.Context, req *app.DeactivateApplicationRequest) (*app.DeactivateApplicationResponse, error) {
+ details, err := s.command.DeactivateApplication(ctx, req.GetProjectId(), req.GetId(), "")
+ if err != nil {
+ return nil, err
+ }
+
+ return &app.DeactivateApplicationResponse{
+ DeactivationDate: timestamppb.New(details.EventDate),
+ }, nil
+
+}
+
+func (s *Server) ReactivateApplication(ctx context.Context, req *app.ReactivateApplicationRequest) (*app.ReactivateApplicationResponse, error) {
+ details, err := s.command.ReactivateApplication(ctx, req.GetProjectId(), req.GetId(), "")
+ if err != nil {
+ return nil, err
+ }
+
+ return &app.ReactivateApplicationResponse{
+ ReactivationDate: timestamppb.New(details.EventDate),
+ }, nil
+
+}
+
+func (s *Server) RegenerateClientSecret(ctx context.Context, req *app.RegenerateClientSecretRequest) (*app.RegenerateClientSecretResponse, error) {
+ var secret string
+ var changeDate time.Time
+
+ switch req.GetAppType().(type) {
+ case *app.RegenerateClientSecretRequest_IsApi:
+ config, err := s.command.ChangeAPIApplicationSecret(ctx, req.GetProjectId(), req.GetApplicationId(), "")
+ if err != nil {
+ return nil, err
+ }
+ secret = config.ClientSecretString
+ changeDate = config.ChangeDate
+
+ case *app.RegenerateClientSecretRequest_IsOidc:
+ config, err := s.command.ChangeOIDCApplicationSecret(ctx, req.GetProjectId(), req.GetApplicationId(), "")
+ if err != nil {
+ return nil, err
+ }
+
+ secret = config.ClientSecretString
+ changeDate = config.ChangeDate
+
+ default:
+ return nil, zerrors.ThrowInvalidArgument(nil, "APP-aLWIzw", "unknown app type")
+ }
+
+ return &app.RegenerateClientSecretResponse{
+ ClientSecret: secret,
+ CreationDate: timestamppb.New(changeDate),
+ }, nil
+}
diff --git a/internal/api/grpc/app/v2beta/convert/api_app.go b/internal/api/grpc/app/v2beta/convert/api_app.go
new file mode 100644
index 0000000000..bad76ab0d5
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/convert/api_app.go
@@ -0,0 +1,60 @@
+package convert
+
+import (
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore/v1/models"
+ "github.com/zitadel/zitadel/internal/query"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+)
+
+func CreateAPIApplicationRequestToDomain(name, projectID, appID string, app *app.CreateAPIApplicationRequest) *domain.APIApp {
+ return &domain.APIApp{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: projectID,
+ },
+ AppName: name,
+ AppID: appID,
+ AuthMethodType: apiAuthMethodTypeToDomain(app.GetAuthMethodType()),
+ }
+}
+
+func UpdateAPIApplicationConfigurationRequestToDomain(appID, projectID string, app *app.UpdateAPIApplicationConfigurationRequest) *domain.APIApp {
+ return &domain.APIApp{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: projectID,
+ },
+ AppID: appID,
+ AuthMethodType: apiAuthMethodTypeToDomain(app.GetAuthMethodType()),
+ }
+}
+
+func appAPIConfigToPb(apiApp *query.APIApp) app.ApplicationConfig {
+ return &app.Application_ApiConfig{
+ ApiConfig: &app.APIConfig{
+ ClientId: apiApp.ClientID,
+ AuthMethodType: apiAuthMethodTypeToPb(apiApp.AuthMethodType),
+ },
+ }
+}
+
+func apiAuthMethodTypeToDomain(authType app.APIAuthMethodType) domain.APIAuthMethodType {
+ switch authType {
+ case app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC:
+ return domain.APIAuthMethodTypeBasic
+ case app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT:
+ return domain.APIAuthMethodTypePrivateKeyJWT
+ default:
+ return domain.APIAuthMethodTypeBasic
+ }
+}
+
+func apiAuthMethodTypeToPb(methodType domain.APIAuthMethodType) app.APIAuthMethodType {
+ switch methodType {
+ case domain.APIAuthMethodTypeBasic:
+ return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC
+ case domain.APIAuthMethodTypePrivateKeyJWT:
+ return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT
+ default:
+ return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC
+ }
+}
diff --git a/internal/api/grpc/app/v2beta/convert/api_app_test.go b/internal/api/grpc/app/v2beta/convert/api_app_test.go
new file mode 100644
index 0000000000..9f15c3df76
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/convert/api_app_test.go
@@ -0,0 +1,149 @@
+package convert
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore/v1/models"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+)
+
+func TestCreateAPIApplicationRequestToDomain(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ appName string
+ projectID string
+ appID string
+ req *app.CreateAPIApplicationRequest
+ want *domain.APIApp
+ }{
+ {
+ name: "basic auth method",
+ appName: "my-app",
+ projectID: "proj-1",
+ appID: "someID",
+ req: &app.CreateAPIApplicationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
+ },
+ want: &domain.APIApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
+ AppName: "my-app",
+ AuthMethodType: domain.APIAuthMethodTypeBasic,
+ AppID: "someID",
+ },
+ },
+ {
+ name: "private key jwt",
+ appName: "jwt-app",
+ projectID: "proj-2",
+ req: &app.CreateAPIApplicationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ },
+ want: &domain.APIApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"},
+ AppName: "jwt-app",
+ AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ got := CreateAPIApplicationRequestToDomain(tt.appName, tt.projectID, tt.appID, tt.req)
+
+ // Then
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func TestUpdateAPIApplicationConfigurationRequestToDomain(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ name string
+ appID string
+ projectID string
+ req *app.UpdateAPIApplicationConfigurationRequest
+ want *domain.APIApp
+ }{
+ {
+ name: "basic auth method",
+ appID: "app-1",
+ projectID: "proj-1",
+ req: &app.UpdateAPIApplicationConfigurationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
+ },
+ want: &domain.APIApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
+ AppID: "app-1",
+ AuthMethodType: domain.APIAuthMethodTypeBasic,
+ },
+ },
+ {
+ name: "private key jwt",
+ appID: "app-2",
+ projectID: "proj-2",
+ req: &app.UpdateAPIApplicationConfigurationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ },
+ want: &domain.APIApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"},
+ AppID: "app-2",
+ AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ got := UpdateAPIApplicationConfigurationRequestToDomain(tt.appID, tt.projectID, tt.req)
+
+ // Then
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func Test_apiAuthMethodTypeToPb(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ methodType domain.APIAuthMethodType
+ expectedResult app.APIAuthMethodType
+ }{
+ {
+ name: "basic auth method",
+ methodType: domain.APIAuthMethodTypeBasic,
+ expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
+ },
+ {
+ name: "private key jwt",
+ methodType: domain.APIAuthMethodTypePrivateKeyJWT,
+ expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ },
+ {
+ name: "unknown auth method defaults to basic",
+ expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
+ },
+ }
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res := apiAuthMethodTypeToPb(tc.methodType)
+
+ // Then
+ assert.Equal(t, tc.expectedResult, res)
+ })
+ }
+}
diff --git a/internal/api/grpc/app/v2beta/convert/convert.go b/internal/api/grpc/app/v2beta/convert/convert.go
new file mode 100644
index 0000000000..c732b3a0c5
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/convert/convert.go
@@ -0,0 +1,165 @@
+package convert
+
+import (
+ "net/url"
+
+ "github.com/muhlemmer/gu"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
+ "github.com/zitadel/zitadel/internal/config/systemdefaults"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/query"
+ "github.com/zitadel/zitadel/internal/zerrors"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+)
+
+func AppToPb(query_app *query.App) *app.Application {
+ if query_app == nil {
+ return &app.Application{}
+ }
+
+ return &app.Application{
+ Id: query_app.ID,
+ CreationDate: timestamppb.New(query_app.CreationDate),
+ ChangeDate: timestamppb.New(query_app.ChangeDate),
+ State: appStateToPb(query_app.State),
+ Name: query_app.Name,
+ Config: appConfigToPb(query_app),
+ }
+}
+
+func AppsToPb(queryApps []*query.App) []*app.Application {
+ pbApps := make([]*app.Application, len(queryApps))
+
+ for i, queryApp := range queryApps {
+ pbApps[i] = AppToPb(queryApp)
+ }
+
+ return pbApps
+}
+
+func ListApplicationsRequestToModel(sysDefaults systemdefaults.SystemDefaults, req *app.ListApplicationsRequest) (*query.AppSearchQueries, error) {
+ offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination())
+ if err != nil {
+ return nil, err
+ }
+
+ queries, err := appQueriesToModel(req.GetFilters())
+ if err != nil {
+ return nil, err
+ }
+ projectQuery, err := query.NewAppProjectIDSearchQuery(req.GetProjectId())
+ if err != nil {
+ return nil, err
+ }
+
+ queries = append(queries, projectQuery)
+ return &query.AppSearchQueries{
+ SearchRequest: query.SearchRequest{
+ Offset: offset,
+ Limit: limit,
+ Asc: asc,
+ SortingColumn: appSortingToColumn(req.GetSortingColumn()),
+ },
+
+ Queries: queries,
+ }, nil
+}
+
+func appSortingToColumn(sortingCriteria app.AppSorting) query.Column {
+ switch sortingCriteria {
+ case app.AppSorting_APP_SORT_BY_CHANGE_DATE:
+ return query.AppColumnChangeDate
+ case app.AppSorting_APP_SORT_BY_CREATION_DATE:
+ return query.AppColumnCreationDate
+ case app.AppSorting_APP_SORT_BY_NAME:
+ return query.AppColumnName
+ case app.AppSorting_APP_SORT_BY_STATE:
+ return query.AppColumnState
+ case app.AppSorting_APP_SORT_BY_ID:
+ fallthrough
+ default:
+ return query.AppColumnID
+ }
+}
+
+func appStateToPb(state domain.AppState) app.AppState {
+ switch state {
+ case domain.AppStateActive:
+ return app.AppState_APP_STATE_ACTIVE
+ case domain.AppStateInactive:
+ return app.AppState_APP_STATE_INACTIVE
+ case domain.AppStateRemoved:
+ return app.AppState_APP_STATE_REMOVED
+ case domain.AppStateUnspecified:
+ fallthrough
+ default:
+ return app.AppState_APP_STATE_UNSPECIFIED
+ }
+}
+
+func appConfigToPb(app *query.App) app.ApplicationConfig {
+ if app.OIDCConfig != nil {
+ return appOIDCConfigToPb(app.OIDCConfig)
+ }
+ if app.SAMLConfig != nil {
+ return appSAMLConfigToPb(app.SAMLConfig)
+ }
+ return appAPIConfigToPb(app.APIConfig)
+}
+
+func loginVersionToDomain(version *app.LoginVersion) (*domain.LoginVersion, *string, error) {
+ switch v := version.GetVersion().(type) {
+ case nil:
+ return gu.Ptr(domain.LoginVersionUnspecified), gu.Ptr(""), nil
+ case *app.LoginVersion_LoginV1:
+ return gu.Ptr(domain.LoginVersion1), gu.Ptr(""), nil
+ case *app.LoginVersion_LoginV2:
+ _, err := url.Parse(v.LoginV2.GetBaseUri())
+ return gu.Ptr(domain.LoginVersion2), gu.Ptr(v.LoginV2.GetBaseUri()), err
+ default:
+ return gu.Ptr(domain.LoginVersionUnspecified), gu.Ptr(""), nil
+ }
+}
+
+func loginVersionToPb(version domain.LoginVersion, baseURI *string) *app.LoginVersion {
+ switch version {
+ case domain.LoginVersionUnspecified:
+ return nil
+ case domain.LoginVersion1:
+ return &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}
+ case domain.LoginVersion2:
+ return &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: baseURI}}}
+ default:
+ return nil
+ }
+}
+
+func appQueriesToModel(queries []*app.ApplicationSearchFilter) (toReturn []query.SearchQuery, err error) {
+ toReturn = make([]query.SearchQuery, len(queries))
+ for i, query := range queries {
+ toReturn[i], err = appQueryToModel(query)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return toReturn, nil
+}
+
+func appQueryToModel(appQuery *app.ApplicationSearchFilter) (query.SearchQuery, error) {
+ switch q := appQuery.GetFilter().(type) {
+ case *app.ApplicationSearchFilter_NameFilter:
+ return query.NewAppNameSearchQuery(filter.TextMethodPbToQuery(q.NameFilter.GetMethod()), q.NameFilter.Name)
+ case *app.ApplicationSearchFilter_StateFilter:
+ return query.NewAppStateSearchQuery(domain.AppState(q.StateFilter))
+ case *app.ApplicationSearchFilter_ApiAppOnly:
+ return query.NewNotNullQuery(query.AppAPIConfigColumnAppID)
+ case *app.ApplicationSearchFilter_OidcAppOnly:
+ return query.NewNotNullQuery(query.AppOIDCConfigColumnAppID)
+ case *app.ApplicationSearchFilter_SamlAppOnly:
+ return query.NewNotNullQuery(query.AppSAMLConfigColumnAppID)
+ default:
+ return nil, zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid")
+ }
+}
diff --git a/internal/api/grpc/app/v2beta/convert/convert_test.go b/internal/api/grpc/app/v2beta/convert/convert_test.go
new file mode 100644
index 0000000000..5835691d43
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/convert/convert_test.go
@@ -0,0 +1,520 @@
+package convert
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "testing"
+ "time"
+
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/types/known/durationpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta"
+ "github.com/zitadel/zitadel/internal/config/systemdefaults"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/query"
+ "github.com/zitadel/zitadel/internal/zerrors"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+ filter_pb_v2 "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
+ filter_pb_v2_beta "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
+)
+
+func TestAppToPb(t *testing.T) {
+ t.Parallel()
+
+ now := time.Now()
+
+ tt := []struct {
+ testName string
+ inputQueryApp *query.App
+ expectedPbApp *app.Application
+ }{
+ {
+ testName: "full app conversion",
+ inputQueryApp: &query.App{
+ ID: "id",
+ CreationDate: now,
+ ChangeDate: now,
+ State: domain.AppStateActive,
+ Name: "test-app",
+ APIConfig: &query.APIApp{},
+ },
+ expectedPbApp: &app.Application{
+ Id: "id",
+ CreationDate: timestamppb.New(now),
+ ChangeDate: timestamppb.New(now),
+ State: app.AppState_APP_STATE_ACTIVE,
+ Name: "test-app",
+ Config: &app.Application_ApiConfig{
+ ApiConfig: &app.APIConfig{},
+ },
+ },
+ },
+ {
+ testName: "nil app",
+ inputQueryApp: nil,
+ expectedPbApp: &app.Application{},
+ },
+ }
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res := AppToPb(tc.inputQueryApp)
+
+ // Then
+ assert.Equal(t, tc.expectedPbApp, res)
+ })
+ }
+}
+
+func TestListApplicationsRequestToModel(t *testing.T) {
+ t.Parallel()
+
+ validSearchByNameQuery, err := query.NewAppNameSearchQuery(filter.TextMethodPbToQuery(filter_pb_v2_beta.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS), "test")
+ require.NoError(t, err)
+
+ validSearchByProjectQuery, err := query.NewAppProjectIDSearchQuery("project1")
+ require.NoError(t, err)
+
+ sysDefaults := systemdefaults.SystemDefaults{DefaultQueryLimit: 100, MaxQueryLimit: 150}
+
+ tt := []struct {
+ testName string
+ req *app.ListApplicationsRequest
+
+ expectedResponse *query.AppSearchQueries
+ expectedError error
+ }{
+ {
+ testName: "invalid pagination limit",
+ req: &app.ListApplicationsRequest{
+ Pagination: &filter_pb_v2.PaginationRequest{Asc: true, Limit: uint32(sysDefaults.MaxQueryLimit + 1)},
+ },
+ expectedResponse: nil,
+ expectedError: zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", sysDefaults.MaxQueryLimit+1, sysDefaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded"),
+ },
+ {
+ testName: "empty request",
+ req: &app.ListApplicationsRequest{
+ ProjectId: "project1",
+ Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
+ },
+ expectedResponse: &query.AppSearchQueries{
+ SearchRequest: query.SearchRequest{
+ Offset: 0,
+ Limit: 100,
+ Asc: true,
+ SortingColumn: query.AppColumnID,
+ },
+ Queries: []query.SearchQuery{
+ validSearchByProjectQuery,
+ },
+ },
+ },
+ {
+ testName: "valid request",
+ req: &app.ListApplicationsRequest{
+ ProjectId: "project1",
+ Filters: []*app.ApplicationSearchFilter{
+ {
+ Filter: &app.ApplicationSearchFilter_NameFilter{NameFilter: &app.ApplicationNameQuery{Name: "test"}},
+ },
+ },
+ SortingColumn: app.AppSorting_APP_SORT_BY_NAME,
+ Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
+ },
+
+ expectedResponse: &query.AppSearchQueries{
+ SearchRequest: query.SearchRequest{
+ Offset: 0,
+ Limit: 100,
+ Asc: true,
+ SortingColumn: query.AppColumnName,
+ },
+ Queries: []query.SearchQuery{
+ validSearchByNameQuery,
+ validSearchByProjectQuery,
+ },
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ got, err := ListApplicationsRequestToModel(sysDefaults, tc.req)
+
+ // Then
+ assert.Equal(t, tc.expectedError, err)
+ assert.Equal(t, tc.expectedResponse, got)
+ })
+ }
+}
+
+func TestAppSortingToColumn(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ sorting app.AppSorting
+ expected query.Column
+ }{
+ {
+ name: "sort by change date",
+ sorting: app.AppSorting_APP_SORT_BY_CHANGE_DATE,
+ expected: query.AppColumnChangeDate,
+ },
+ {
+ name: "sort by creation date",
+ sorting: app.AppSorting_APP_SORT_BY_CREATION_DATE,
+ expected: query.AppColumnCreationDate,
+ },
+ {
+ name: "sort by name",
+ sorting: app.AppSorting_APP_SORT_BY_NAME,
+ expected: query.AppColumnName,
+ },
+ {
+ name: "sort by state",
+ sorting: app.AppSorting_APP_SORT_BY_STATE,
+ expected: query.AppColumnState,
+ },
+ {
+ name: "sort by ID",
+ sorting: app.AppSorting_APP_SORT_BY_ID,
+ expected: query.AppColumnID,
+ },
+ {
+ name: "unknown sorting defaults to ID",
+ sorting: app.AppSorting(99),
+ expected: query.AppColumnID,
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := appSortingToColumn(tc.sorting)
+
+ // Then
+ assert.Equal(t, tc.expected, result)
+ })
+ }
+}
+
+func TestAppStateToPb(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ state domain.AppState
+ expected app.AppState
+ }{
+ {
+ name: "active state",
+ state: domain.AppStateActive,
+ expected: app.AppState_APP_STATE_ACTIVE,
+ },
+ {
+ name: "inactive state",
+ state: domain.AppStateInactive,
+ expected: app.AppState_APP_STATE_INACTIVE,
+ },
+ {
+ name: "removed state",
+ state: domain.AppStateRemoved,
+ expected: app.AppState_APP_STATE_REMOVED,
+ },
+ {
+ name: "unspecified state",
+ state: domain.AppStateUnspecified,
+ expected: app.AppState_APP_STATE_UNSPECIFIED,
+ },
+ {
+ name: "unknown state defaults to unspecified",
+ state: domain.AppState(99),
+ expected: app.AppState_APP_STATE_UNSPECIFIED,
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := appStateToPb(tc.state)
+
+ // Then
+ assert.Equal(t, tc.expected, result)
+ })
+ }
+}
+
+func TestAppConfigToPb(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ app *query.App
+ expected app.ApplicationConfig
+ }{
+ {
+ name: "OIDC config",
+ app: &query.App{
+ OIDCConfig: &query.OIDCApp{},
+ },
+ expected: &app.Application_OidcConfig{
+ OidcConfig: &app.OIDCConfig{
+ ResponseTypes: []app.OIDCResponseType{},
+ GrantTypes: []app.OIDCGrantType{},
+ ComplianceProblems: []*app.OIDCLocalizedMessage{},
+ ClockSkew: &durationpb.Duration{},
+ },
+ },
+ },
+ {
+ name: "SAML config",
+ app: &query.App{
+ SAMLConfig: &query.SAMLApp{},
+ },
+ expected: &app.Application_SamlConfig{
+ SamlConfig: &app.SAMLConfig{
+ Metadata: &app.SAMLConfig_MetadataXml{},
+ },
+ },
+ },
+ {
+ name: "API config",
+ app: &query.App{
+ APIConfig: &query.APIApp{},
+ },
+ expected: &app.Application_ApiConfig{
+ ApiConfig: &app.APIConfig{},
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := appConfigToPb(tc.app)
+
+ // Then
+ assert.Equal(t, tc.expected, result)
+ })
+ }
+}
+
+func TestLoginVersionToDomain(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ version *app.LoginVersion
+ expectedVer *domain.LoginVersion
+ expectedURI *string
+ expectedError error
+ }{
+ {
+ name: "nil version",
+ version: nil,
+ expectedVer: gu.Ptr(domain.LoginVersionUnspecified),
+ expectedURI: gu.Ptr(""),
+ },
+ {
+ name: "login v1",
+ version: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}},
+ expectedVer: gu.Ptr(domain.LoginVersion1),
+ expectedURI: gu.Ptr(""),
+ },
+ {
+ name: "login v2 valid URI",
+ version: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://valid.url")}}},
+ expectedVer: gu.Ptr(domain.LoginVersion2),
+ expectedURI: gu.Ptr("https://valid.url"),
+ },
+ {
+ name: "login v2 invalid URI",
+ version: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: gu.Ptr("://invalid")}}},
+ expectedVer: gu.Ptr(domain.LoginVersion2),
+ expectedURI: gu.Ptr("://invalid"),
+ expectedError: &url.Error{Op: "parse", URL: "://invalid", Err: errors.New("missing protocol scheme")},
+ },
+ {
+ name: "unknown version type",
+ version: &app.LoginVersion{},
+ expectedVer: gu.Ptr(domain.LoginVersionUnspecified),
+ expectedURI: gu.Ptr(""),
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ version, uri, err := loginVersionToDomain(tc.version)
+
+ // Then
+ assert.Equal(t, tc.expectedVer, version)
+ assert.Equal(t, tc.expectedURI, uri)
+ assert.Equal(t, tc.expectedError, err)
+ })
+ }
+}
+
+func TestLoginVersionToPb(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ version domain.LoginVersion
+ baseURI *string
+ expected *app.LoginVersion
+ }{
+ {
+ name: "unspecified version",
+ version: domain.LoginVersionUnspecified,
+ baseURI: nil,
+ expected: nil,
+ },
+ {
+ name: "login v1",
+ version: domain.LoginVersion1,
+ baseURI: nil,
+ expected: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV1{
+ LoginV1: &app.LoginV1{},
+ },
+ },
+ },
+ {
+ name: "login v2",
+ version: domain.LoginVersion2,
+ baseURI: gu.Ptr("https://example.com"),
+ expected: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: gu.Ptr("https://example.com"),
+ },
+ },
+ },
+ },
+ {
+ name: "unknown version",
+ version: domain.LoginVersion(99),
+ baseURI: nil,
+ expected: nil,
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := loginVersionToPb(tc.version, tc.baseURI)
+
+ // Then
+ assert.Equal(t, tc.expected, result)
+ })
+ }
+}
+
+func TestAppQueryToModel(t *testing.T) {
+ t.Parallel()
+
+ validAppNameSearchQuery, err := query.NewAppNameSearchQuery(query.TextEquals, "test")
+ require.NoError(t, err)
+
+ validAppStateSearchQuery, err := query.NewAppStateSearchQuery(domain.AppStateActive)
+ require.NoError(t, err)
+
+ tt := []struct {
+ name string
+ query *app.ApplicationSearchFilter
+
+ expectedQuery query.SearchQuery
+ expectedError error
+ }{
+ {
+ name: "name query",
+ query: &app.ApplicationSearchFilter{
+ Filter: &app.ApplicationSearchFilter_NameFilter{
+ NameFilter: &app.ApplicationNameQuery{
+ Name: "test",
+ Method: filter_pb_v2.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
+ },
+ },
+ },
+ expectedQuery: validAppNameSearchQuery,
+ },
+ {
+ name: "state query",
+ query: &app.ApplicationSearchFilter{
+ Filter: &app.ApplicationSearchFilter_StateFilter{
+ StateFilter: app.AppState_APP_STATE_ACTIVE,
+ },
+ },
+ expectedQuery: validAppStateSearchQuery,
+ },
+ {
+ name: "api app only query",
+ query: &app.ApplicationSearchFilter{
+ Filter: &app.ApplicationSearchFilter_ApiAppOnly{},
+ },
+ expectedQuery: &query.NotNullQuery{
+ Column: query.AppAPIConfigColumnAppID,
+ },
+ },
+ {
+ name: "oidc app only query",
+ query: &app.ApplicationSearchFilter{
+ Filter: &app.ApplicationSearchFilter_OidcAppOnly{},
+ },
+ expectedQuery: &query.NotNullQuery{
+ Column: query.AppOIDCConfigColumnAppID,
+ },
+ },
+ {
+ name: "saml app only query",
+ query: &app.ApplicationSearchFilter{
+ Filter: &app.ApplicationSearchFilter_SamlAppOnly{},
+ },
+ expectedQuery: &query.NotNullQuery{
+ Column: query.AppSAMLConfigColumnAppID,
+ },
+ },
+ {
+ name: "invalid query type",
+ query: &app.ApplicationSearchFilter{},
+ expectedQuery: nil,
+ expectedError: zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid"),
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result, err := appQueryToModel(tc.query)
+
+ // Then
+ assert.Equal(t, tc.expectedError, err)
+ assert.Equal(t, tc.expectedQuery, result)
+ })
+ }
+}
diff --git a/internal/api/grpc/app/v2beta/convert/oidc_app.go b/internal/api/grpc/app/v2beta/convert/oidc_app.go
new file mode 100644
index 0000000000..223e43d166
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/convert/oidc_app.go
@@ -0,0 +1,291 @@
+package convert
+
+import (
+ "github.com/muhlemmer/gu"
+ "google.golang.org/protobuf/types/known/durationpb"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore/v1/models"
+ "github.com/zitadel/zitadel/internal/query"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+)
+
+func CreateOIDCAppRequestToDomain(name, projectID string, req *app.CreateOIDCApplicationRequest) (*domain.OIDCApp, error) {
+ loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion())
+ if err != nil {
+ return nil, err
+ }
+ return &domain.OIDCApp{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: projectID,
+ },
+ AppName: name,
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
+ RedirectUris: req.GetRedirectUris(),
+ ResponseTypes: oidcResponseTypesToDomain(req.GetResponseTypes()),
+ GrantTypes: oidcGrantTypesToDomain(req.GetGrantTypes()),
+ ApplicationType: gu.Ptr(oidcApplicationTypeToDomain(req.GetAppType())),
+ AuthMethodType: gu.Ptr(oidcAuthMethodTypeToDomain(req.GetAuthMethodType())),
+ PostLogoutRedirectUris: req.GetPostLogoutRedirectUris(),
+ DevMode: &req.DevMode,
+ AccessTokenType: gu.Ptr(oidcTokenTypeToDomain(req.GetAccessTokenType())),
+ AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()),
+ IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()),
+ IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()),
+ ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()),
+ AdditionalOrigins: req.GetAdditionalOrigins(),
+ SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()),
+ BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()),
+ LoginVersion: loginVersion,
+ LoginBaseURI: loginBaseURI,
+ }, nil
+}
+
+func UpdateOIDCAppConfigRequestToDomain(appID, projectID string, app *app.UpdateOIDCApplicationConfigurationRequest) (*domain.OIDCApp, error) {
+ loginVersion, loginBaseURI, err := loginVersionToDomain(app.GetLoginVersion())
+ if err != nil {
+ return nil, err
+ }
+ return &domain.OIDCApp{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: projectID,
+ },
+ AppID: appID,
+ RedirectUris: app.RedirectUris,
+ ResponseTypes: oidcResponseTypesToDomain(app.ResponseTypes),
+ GrantTypes: oidcGrantTypesToDomain(app.GrantTypes),
+ ApplicationType: oidcApplicationTypeToDomainPtr(app.AppType),
+ AuthMethodType: oidcAuthMethodTypeToDomainPtr(app.AuthMethodType),
+ PostLogoutRedirectUris: app.PostLogoutRedirectUris,
+ DevMode: app.DevMode,
+ AccessTokenType: oidcTokenTypeToDomainPtr(app.AccessTokenType),
+ AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
+ IDTokenRoleAssertion: app.IdTokenRoleAssertion,
+ IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion,
+ ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()),
+ AdditionalOrigins: app.AdditionalOrigins,
+ SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage,
+ BackChannelLogoutURI: app.BackChannelLogoutUri,
+ LoginVersion: loginVersion,
+ LoginBaseURI: loginBaseURI,
+ }, nil
+}
+
+func oidcResponseTypesToDomain(responseTypes []app.OIDCResponseType) []domain.OIDCResponseType {
+ if len(responseTypes) == 0 {
+ return []domain.OIDCResponseType{domain.OIDCResponseTypeCode}
+ }
+ oidcResponseTypes := make([]domain.OIDCResponseType, len(responseTypes))
+ for i, responseType := range responseTypes {
+ switch responseType {
+ case app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED:
+ oidcResponseTypes[i] = domain.OIDCResponseTypeUnspecified
+ case app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE:
+ oidcResponseTypes[i] = domain.OIDCResponseTypeCode
+ case app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN:
+ oidcResponseTypes[i] = domain.OIDCResponseTypeIDToken
+ case app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN:
+ oidcResponseTypes[i] = domain.OIDCResponseTypeIDTokenToken
+ }
+ }
+ return oidcResponseTypes
+}
+
+func oidcGrantTypesToDomain(grantTypes []app.OIDCGrantType) []domain.OIDCGrantType {
+ if len(grantTypes) == 0 {
+ return []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}
+ }
+ oidcGrantTypes := make([]domain.OIDCGrantType, len(grantTypes))
+ for i, grantType := range grantTypes {
+ switch grantType {
+ case app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE:
+ oidcGrantTypes[i] = domain.OIDCGrantTypeAuthorizationCode
+ case app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT:
+ oidcGrantTypes[i] = domain.OIDCGrantTypeImplicit
+ case app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN:
+ oidcGrantTypes[i] = domain.OIDCGrantTypeRefreshToken
+ case app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE:
+ oidcGrantTypes[i] = domain.OIDCGrantTypeDeviceCode
+ case app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE:
+ oidcGrantTypes[i] = domain.OIDCGrantTypeTokenExchange
+ }
+ }
+ return oidcGrantTypes
+}
+
+func oidcApplicationTypeToDomainPtr(appType *app.OIDCAppType) *domain.OIDCApplicationType {
+ if appType == nil {
+ return nil
+ }
+
+ res := oidcApplicationTypeToDomain(*appType)
+ return &res
+}
+
+func oidcApplicationTypeToDomain(appType app.OIDCAppType) domain.OIDCApplicationType {
+ switch appType {
+ case app.OIDCAppType_OIDC_APP_TYPE_WEB:
+ return domain.OIDCApplicationTypeWeb
+ case app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT:
+ return domain.OIDCApplicationTypeUserAgent
+ case app.OIDCAppType_OIDC_APP_TYPE_NATIVE:
+ return domain.OIDCApplicationTypeNative
+ }
+ return domain.OIDCApplicationTypeWeb
+}
+
+func oidcAuthMethodTypeToDomainPtr(authType *app.OIDCAuthMethodType) *domain.OIDCAuthMethodType {
+ if authType == nil {
+ return nil
+ }
+
+ res := oidcAuthMethodTypeToDomain(*authType)
+ return &res
+}
+
+func oidcAuthMethodTypeToDomain(authType app.OIDCAuthMethodType) domain.OIDCAuthMethodType {
+ switch authType {
+ case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC:
+ return domain.OIDCAuthMethodTypeBasic
+ case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST:
+ return domain.OIDCAuthMethodTypePost
+ case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE:
+ return domain.OIDCAuthMethodTypeNone
+ case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT:
+ return domain.OIDCAuthMethodTypePrivateKeyJWT
+ default:
+ return domain.OIDCAuthMethodTypeBasic
+ }
+}
+
+func oidcTokenTypeToDomainPtr(tokenType *app.OIDCTokenType) *domain.OIDCTokenType {
+ if tokenType == nil {
+ return nil
+ }
+
+ res := oidcTokenTypeToDomain(*tokenType)
+ return &res
+}
+
+func oidcTokenTypeToDomain(tokenType app.OIDCTokenType) domain.OIDCTokenType {
+ switch tokenType {
+ case app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER:
+ return domain.OIDCTokenTypeBearer
+ case app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT:
+ return domain.OIDCTokenTypeJWT
+ default:
+ return domain.OIDCTokenTypeBearer
+ }
+}
+
+func ComplianceProblemsToLocalizedMessages(complianceProblems []string) []*app.OIDCLocalizedMessage {
+ converted := make([]*app.OIDCLocalizedMessage, len(complianceProblems))
+ for i, p := range complianceProblems {
+ converted[i] = &app.OIDCLocalizedMessage{Key: p}
+ }
+
+ return converted
+}
+
+func appOIDCConfigToPb(oidcApp *query.OIDCApp) *app.Application_OidcConfig {
+ return &app.Application_OidcConfig{
+ OidcConfig: &app.OIDCConfig{
+ RedirectUris: oidcApp.RedirectURIs,
+ ResponseTypes: oidcResponseTypesFromModel(oidcApp.ResponseTypes),
+ GrantTypes: oidcGrantTypesFromModel(oidcApp.GrantTypes),
+ AppType: oidcApplicationTypeToPb(oidcApp.AppType),
+ ClientId: oidcApp.ClientID,
+ AuthMethodType: oidcAuthMethodTypeToPb(oidcApp.AuthMethodType),
+ PostLogoutRedirectUris: oidcApp.PostLogoutRedirectURIs,
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ NoneCompliant: len(oidcApp.ComplianceProblems) != 0,
+ ComplianceProblems: ComplianceProblemsToLocalizedMessages(oidcApp.ComplianceProblems),
+ DevMode: oidcApp.IsDevMode,
+ AccessTokenType: oidcTokenTypeToPb(oidcApp.AccessTokenType),
+ AccessTokenRoleAssertion: oidcApp.AssertAccessTokenRole,
+ IdTokenRoleAssertion: oidcApp.AssertIDTokenRole,
+ IdTokenUserinfoAssertion: oidcApp.AssertIDTokenUserinfo,
+ ClockSkew: durationpb.New(oidcApp.ClockSkew),
+ AdditionalOrigins: oidcApp.AdditionalOrigins,
+ AllowedOrigins: oidcApp.AllowedOrigins,
+ SkipNativeAppSuccessPage: oidcApp.SkipNativeAppSuccessPage,
+ BackChannelLogoutUri: oidcApp.BackChannelLogoutURI,
+ LoginVersion: loginVersionToPb(oidcApp.LoginVersion, oidcApp.LoginBaseURI),
+ },
+ }
+}
+
+func oidcResponseTypesFromModel(responseTypes []domain.OIDCResponseType) []app.OIDCResponseType {
+ oidcResponseTypes := make([]app.OIDCResponseType, len(responseTypes))
+ for i, responseType := range responseTypes {
+ switch responseType {
+ case domain.OIDCResponseTypeUnspecified:
+ oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED
+ case domain.OIDCResponseTypeCode:
+ oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE
+ case domain.OIDCResponseTypeIDToken:
+ oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN
+ case domain.OIDCResponseTypeIDTokenToken:
+ oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN
+ }
+ }
+ return oidcResponseTypes
+}
+
+func oidcGrantTypesFromModel(grantTypes []domain.OIDCGrantType) []app.OIDCGrantType {
+ oidcGrantTypes := make([]app.OIDCGrantType, len(grantTypes))
+ for i, grantType := range grantTypes {
+ switch grantType {
+ case domain.OIDCGrantTypeAuthorizationCode:
+ oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE
+ case domain.OIDCGrantTypeImplicit:
+ oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT
+ case domain.OIDCGrantTypeRefreshToken:
+ oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN
+ case domain.OIDCGrantTypeDeviceCode:
+ oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE
+ case domain.OIDCGrantTypeTokenExchange:
+ oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE
+ }
+ }
+ return oidcGrantTypes
+}
+
+func oidcApplicationTypeToPb(appType domain.OIDCApplicationType) app.OIDCAppType {
+ switch appType {
+ case domain.OIDCApplicationTypeWeb:
+ return app.OIDCAppType_OIDC_APP_TYPE_WEB
+ case domain.OIDCApplicationTypeUserAgent:
+ return app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT
+ case domain.OIDCApplicationTypeNative:
+ return app.OIDCAppType_OIDC_APP_TYPE_NATIVE
+ default:
+ return app.OIDCAppType_OIDC_APP_TYPE_WEB
+ }
+}
+
+func oidcAuthMethodTypeToPb(authType domain.OIDCAuthMethodType) app.OIDCAuthMethodType {
+ switch authType {
+ case domain.OIDCAuthMethodTypeBasic:
+ return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC
+ case domain.OIDCAuthMethodTypePost:
+ return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST
+ case domain.OIDCAuthMethodTypeNone:
+ return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE
+ case domain.OIDCAuthMethodTypePrivateKeyJWT:
+ return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT
+ default:
+ return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC
+ }
+}
+
+func oidcTokenTypeToPb(tokenType domain.OIDCTokenType) app.OIDCTokenType {
+ switch tokenType {
+ case domain.OIDCTokenTypeBearer:
+ return app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER
+ case domain.OIDCTokenTypeJWT:
+ return app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT
+ default:
+ return app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER
+ }
+}
diff --git a/internal/api/grpc/app/v2beta/convert/oidc_app_test.go b/internal/api/grpc/app/v2beta/convert/oidc_app_test.go
new file mode 100644
index 0000000000..a6b3f0b709
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/convert/oidc_app_test.go
@@ -0,0 +1,755 @@
+package convert
+
+import (
+ "net/url"
+ "testing"
+ "time"
+
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+ "google.golang.org/protobuf/types/known/durationpb"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore/v1/models"
+ "github.com/zitadel/zitadel/internal/query"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+)
+
+func TestCreateOIDCAppRequestToDomain(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ projectID string
+ req *app.CreateOIDCApplicationRequest
+
+ expectedModel *domain.OIDCApp
+ expectedError error
+ }{
+ {
+ testName: "unparsable login version 2 URL",
+ projectID: "pid",
+ req: &app.CreateOIDCApplicationRequest{
+ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}},
+ },
+ },
+ expectedModel: nil,
+ expectedError: &url.Error{
+ URL: "%+o",
+ Op: "parse",
+ Err: url.EscapeError("%+o"),
+ },
+ },
+ {
+ testName: "all fields set",
+ projectID: "project1",
+ req: &app.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"https://redirect"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ PostLogoutRedirectUris: []string{"https://logout"},
+ DevMode: true,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
+ AccessTokenRoleAssertion: true,
+ IdTokenRoleAssertion: true,
+ IdTokenUserinfoAssertion: true,
+ ClockSkew: durationpb.New(5 * time.Second),
+ AdditionalOrigins: []string{"https://origin"},
+ SkipNativeAppSuccessPage: true,
+ BackChannelLogoutUri: "https://backchannel",
+ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{
+ BaseUri: gu.Ptr("https://login"),
+ }}},
+ },
+ expectedModel: &domain.OIDCApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "project1"},
+ AppName: "all fields set",
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
+ RedirectUris: []string{"https://redirect"},
+ ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
+ GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic),
+ PostLogoutRedirectUris: []string{"https://logout"},
+ DevMode: gu.Ptr(true),
+ AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IDTokenRoleAssertion: gu.Ptr(true),
+ IDTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: gu.Ptr(5 * time.Second),
+ AdditionalOrigins: []string{"https://origin"},
+ SkipNativeAppSuccessPage: gu.Ptr(true),
+ BackChannelLogoutURI: gu.Ptr("https://backchannel"),
+ LoginVersion: gu.Ptr(domain.LoginVersion2),
+ LoginBaseURI: gu.Ptr("https://login"),
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res, err := CreateOIDCAppRequestToDomain(tc.testName, tc.projectID, tc.req)
+
+ // Then
+ assert.Equal(t, tc.expectedError, err)
+ assert.Equal(t, tc.expectedModel, res)
+ })
+ }
+}
+
+func TestUpdateOIDCAppConfigRequestToDomain(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+
+ appID string
+ projectID string
+ req *app.UpdateOIDCApplicationConfigurationRequest
+
+ expectedModel *domain.OIDCApp
+ expectedError error
+ }{
+ {
+ testName: "unparsable login version 2 URL",
+ appID: "app1",
+ projectID: "pid",
+ req: &app.UpdateOIDCApplicationConfigurationRequest{
+ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")},
+ }},
+ },
+ expectedModel: nil,
+ expectedError: &url.Error{
+ URL: "%+o",
+ Op: "parse",
+ Err: url.EscapeError("%+o"),
+ },
+ },
+ {
+ testName: "successful Update",
+ appID: "app1",
+ projectID: "proj1",
+ req: &app.UpdateOIDCApplicationConfigurationRequest{
+ RedirectUris: []string{"https://redirect"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: gu.Ptr(app.OIDCAppType_OIDC_APP_TYPE_WEB),
+ AuthMethodType: gu.Ptr(app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC),
+ PostLogoutRedirectUris: []string{"https://logout"},
+ DevMode: gu.Ptr(true),
+ AccessTokenType: gu.Ptr(app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IdTokenRoleAssertion: gu.Ptr(true),
+ IdTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: durationpb.New(5 * time.Second),
+ AdditionalOrigins: []string{"https://origin"},
+ SkipNativeAppSuccessPage: gu.Ptr(true),
+ BackChannelLogoutUri: gu.Ptr("https://backchannel"),
+ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://login")},
+ }},
+ },
+ expectedModel: &domain.OIDCApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj1"},
+ AppID: "app1",
+ RedirectUris: []string{"https://redirect"},
+ ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
+ GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic),
+ PostLogoutRedirectUris: []string{"https://logout"},
+ DevMode: gu.Ptr(true),
+ AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IDTokenRoleAssertion: gu.Ptr(true),
+ IDTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: gu.Ptr(5 * time.Second),
+ AdditionalOrigins: []string{"https://origin"},
+ SkipNativeAppSuccessPage: gu.Ptr(true),
+ BackChannelLogoutURI: gu.Ptr("https://backchannel"),
+ LoginVersion: gu.Ptr(domain.LoginVersion2),
+ LoginBaseURI: gu.Ptr("https://login"),
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ got, err := UpdateOIDCAppConfigRequestToDomain(tc.appID, tc.projectID, tc.req)
+
+ // Then
+ assert.Equal(t, tc.expectedError, err)
+ assert.Equal(t, tc.expectedModel, got)
+ })
+ }
+}
+
+func TestOIDCResponseTypesToDomain(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputResponseType []app.OIDCResponseType
+ expectedResponse []domain.OIDCResponseType
+ }{
+ {
+ testName: "empty response types",
+ inputResponseType: []app.OIDCResponseType{},
+ expectedResponse: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
+ },
+ {
+ testName: "all response types",
+ inputResponseType: []app.OIDCResponseType{
+ app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED,
+ app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
+ app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN,
+ app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN,
+ },
+ expectedResponse: []domain.OIDCResponseType{
+ domain.OIDCResponseTypeUnspecified,
+ domain.OIDCResponseTypeCode,
+ domain.OIDCResponseTypeIDToken,
+ domain.OIDCResponseTypeIDTokenToken,
+ },
+ },
+ {
+ testName: "single response type",
+ inputResponseType: []app.OIDCResponseType{
+ app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
+ },
+ expectedResponse: []domain.OIDCResponseType{
+ domain.OIDCResponseTypeCode,
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res := oidcResponseTypesToDomain(tc.inputResponseType)
+
+ // Then
+ assert.Equal(t, tc.expectedResponse, res)
+ })
+ }
+}
+
+func TestOIDCGrantTypesToDomain(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputGrantType []app.OIDCGrantType
+ expectedGrants []domain.OIDCGrantType
+ }{
+ {
+ testName: "empty grant types",
+ inputGrantType: []app.OIDCGrantType{},
+ expectedGrants: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ },
+ {
+ testName: "all grant types",
+ inputGrantType: []app.OIDCGrantType{
+ app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
+ app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT,
+ app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN,
+ app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE,
+ app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
+ },
+ expectedGrants: []domain.OIDCGrantType{
+ domain.OIDCGrantTypeAuthorizationCode,
+ domain.OIDCGrantTypeImplicit,
+ domain.OIDCGrantTypeRefreshToken,
+ domain.OIDCGrantTypeDeviceCode,
+ domain.OIDCGrantTypeTokenExchange,
+ },
+ },
+ {
+ testName: "single grant type",
+ inputGrantType: []app.OIDCGrantType{
+ app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
+ },
+ expectedGrants: []domain.OIDCGrantType{
+ domain.OIDCGrantTypeAuthorizationCode,
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res := oidcGrantTypesToDomain(tc.inputGrantType)
+
+ // Then
+ assert.Equal(t, tc.expectedGrants, res)
+ })
+ }
+}
+
+func TestOIDCApplicationTypeToDomain(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ appType app.OIDCAppType
+ expected domain.OIDCApplicationType
+ }{
+ {
+ name: "web type",
+ appType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ expected: domain.OIDCApplicationTypeWeb,
+ },
+ {
+ name: "user agent type",
+ appType: app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT,
+ expected: domain.OIDCApplicationTypeUserAgent,
+ },
+ {
+ name: "native type",
+ appType: app.OIDCAppType_OIDC_APP_TYPE_NATIVE,
+ expected: domain.OIDCApplicationTypeNative,
+ },
+ {
+ name: "unspecified type defaults to web",
+ expected: domain.OIDCApplicationTypeWeb,
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := oidcApplicationTypeToDomain(tc.appType)
+
+ // Then
+ assert.Equal(t, tc.expected, result)
+ })
+ }
+}
+
+func TestOIDCAuthMethodTypeToDomain(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ authType app.OIDCAuthMethodType
+ expectedResponse domain.OIDCAuthMethodType
+ }{
+ {
+ name: "basic auth type",
+ authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ expectedResponse: domain.OIDCAuthMethodTypeBasic,
+ },
+ {
+ name: "post auth type",
+ authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST,
+ expectedResponse: domain.OIDCAuthMethodTypePost,
+ },
+ {
+ name: "none auth type",
+ authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
+ expectedResponse: domain.OIDCAuthMethodTypeNone,
+ },
+ {
+ name: "private key jwt auth type",
+ authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ expectedResponse: domain.OIDCAuthMethodTypePrivateKeyJWT,
+ },
+ {
+ name: "unspecified auth type defaults to basic",
+ expectedResponse: domain.OIDCAuthMethodTypeBasic,
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res := oidcAuthMethodTypeToDomain(tc.authType)
+
+ // Then
+ assert.Equal(t, tc.expectedResponse, res)
+ })
+ }
+}
+
+func TestOIDCTokenTypeToDomain(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ tokenType app.OIDCTokenType
+ expectedType domain.OIDCTokenType
+ }{
+ {
+ name: "bearer token type",
+ tokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
+ expectedType: domain.OIDCTokenTypeBearer,
+ },
+ {
+ name: "jwt token type",
+ tokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ expectedType: domain.OIDCTokenTypeJWT,
+ },
+ {
+ name: "unspecified defaults to bearer",
+ expectedType: domain.OIDCTokenTypeBearer,
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := oidcTokenTypeToDomain(tc.tokenType)
+
+ // Then
+ assert.Equal(t, tc.expectedType, result)
+ })
+ }
+}
+func TestAppOIDCConfigToPb(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ input *query.OIDCApp
+ expected *app.Application_OidcConfig
+ }{
+ {
+ name: "empty config",
+ input: &query.OIDCApp{},
+ expected: &app.Application_OidcConfig{
+ OidcConfig: &app.OIDCConfig{
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ ComplianceProblems: []*app.OIDCLocalizedMessage{},
+ ClockSkew: durationpb.New(0),
+ ResponseTypes: []app.OIDCResponseType{},
+ GrantTypes: []app.OIDCGrantType{},
+ },
+ },
+ },
+ {
+ name: "full config",
+ input: &query.OIDCApp{
+ RedirectURIs: []string{"https://example.com/callback"},
+ ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
+ GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ AppType: domain.OIDCApplicationTypeWeb,
+ ClientID: "client123",
+ AuthMethodType: domain.OIDCAuthMethodTypeBasic,
+ PostLogoutRedirectURIs: []string{"https://example.com/logout"},
+ ComplianceProblems: []string{"problem1", "problem2"},
+ IsDevMode: true,
+ AccessTokenType: domain.OIDCTokenTypeBearer,
+ AssertAccessTokenRole: true,
+ AssertIDTokenRole: true,
+ AssertIDTokenUserinfo: true,
+ ClockSkew: 5 * time.Second,
+ AdditionalOrigins: []string{"https://app.example.com"},
+ AllowedOrigins: []string{"https://allowed.example.com"},
+ SkipNativeAppSuccessPage: true,
+ BackChannelLogoutURI: "https://example.com/backchannel",
+ LoginVersion: domain.LoginVersion2,
+ LoginBaseURI: gu.Ptr("https://login.example.com"),
+ },
+ expected: &app.Application_OidcConfig{
+ OidcConfig: &app.OIDCConfig{
+ RedirectUris: []string{"https://example.com/callback"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ ClientId: "client123",
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ PostLogoutRedirectUris: []string{"https://example.com/logout"},
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ NoneCompliant: true,
+ ComplianceProblems: []*app.OIDCLocalizedMessage{
+ {Key: "problem1"},
+ {Key: "problem2"},
+ },
+ DevMode: true,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
+ AccessTokenRoleAssertion: true,
+ IdTokenRoleAssertion: true,
+ IdTokenUserinfoAssertion: true,
+ ClockSkew: durationpb.New(5 * time.Second),
+ AdditionalOrigins: []string{"https://app.example.com"},
+ AllowedOrigins: []string{"https://allowed.example.com"},
+ SkipNativeAppSuccessPage: true,
+ BackChannelLogoutUri: "https://example.com/backchannel",
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: gu.Ptr("https://login.example.com"),
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tt {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := appOIDCConfigToPb(tt.input)
+
+ // Then
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+func TestOIDCResponseTypesFromModel(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ responseTypes []domain.OIDCResponseType
+ expected []app.OIDCResponseType
+ }{
+ {
+ name: "empty response types",
+ responseTypes: []domain.OIDCResponseType{},
+ expected: []app.OIDCResponseType{},
+ },
+ {
+ name: "all response types",
+ responseTypes: []domain.OIDCResponseType{
+ domain.OIDCResponseTypeUnspecified,
+ domain.OIDCResponseTypeCode,
+ domain.OIDCResponseTypeIDToken,
+ domain.OIDCResponseTypeIDTokenToken,
+ },
+ expected: []app.OIDCResponseType{
+ app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED,
+ app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
+ app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN,
+ app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN,
+ },
+ },
+ {
+ name: "single response type",
+ responseTypes: []domain.OIDCResponseType{
+ domain.OIDCResponseTypeCode,
+ },
+ expected: []app.OIDCResponseType{
+ app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := oidcResponseTypesFromModel(tc.responseTypes)
+
+ // Then
+ assert.Equal(t, tc.expected, result)
+ })
+ }
+}
+func TestOIDCGrantTypesFromModel(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ grantTypes []domain.OIDCGrantType
+ expected []app.OIDCGrantType
+ }{
+ {
+ name: "empty grant types",
+ grantTypes: []domain.OIDCGrantType{},
+ expected: []app.OIDCGrantType{},
+ },
+ {
+ name: "all grant types",
+ grantTypes: []domain.OIDCGrantType{
+ domain.OIDCGrantTypeAuthorizationCode,
+ domain.OIDCGrantTypeImplicit,
+ domain.OIDCGrantTypeRefreshToken,
+ domain.OIDCGrantTypeDeviceCode,
+ domain.OIDCGrantTypeTokenExchange,
+ },
+ expected: []app.OIDCGrantType{
+ app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
+ app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT,
+ app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN,
+ app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE,
+ app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
+ },
+ },
+ {
+ name: "single grant type",
+ grantTypes: []domain.OIDCGrantType{
+ domain.OIDCGrantTypeAuthorizationCode,
+ },
+ expected: []app.OIDCGrantType{
+ app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := oidcGrantTypesFromModel(tc.grantTypes)
+
+ // Then
+ assert.Equal(t, tc.expected, result)
+ })
+ }
+}
+
+func TestOIDCApplicationTypeToPb(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ appType domain.OIDCApplicationType
+ expected app.OIDCAppType
+ }{
+ {
+ name: "web type",
+ appType: domain.OIDCApplicationTypeWeb,
+ expected: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ },
+ {
+ name: "user agent type",
+ appType: domain.OIDCApplicationTypeUserAgent,
+ expected: app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT,
+ },
+ {
+ name: "native type",
+ appType: domain.OIDCApplicationTypeNative,
+ expected: app.OIDCAppType_OIDC_APP_TYPE_NATIVE,
+ },
+ {
+ name: "unspecified type defaults to web",
+ appType: domain.OIDCApplicationType(999), // Invalid value to trigger default case
+ expected: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := oidcApplicationTypeToPb(tc.appType)
+
+ // Then
+ assert.Equal(t, tc.expected, result)
+ })
+ }
+}
+
+func TestOIDCAuthMethodTypeToPb(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ authType domain.OIDCAuthMethodType
+ expected app.OIDCAuthMethodType
+ }{
+ {
+ name: "basic auth type",
+ authType: domain.OIDCAuthMethodTypeBasic,
+ expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ },
+ {
+ name: "post auth type",
+ authType: domain.OIDCAuthMethodTypePost,
+ expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST,
+ },
+ {
+ name: "none auth type",
+ authType: domain.OIDCAuthMethodTypeNone,
+ expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
+ },
+ {
+ name: "private key jwt auth type",
+ authType: domain.OIDCAuthMethodTypePrivateKeyJWT,
+ expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ },
+ {
+ name: "unknown auth type defaults to basic",
+ authType: domain.OIDCAuthMethodType(999),
+ expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := oidcAuthMethodTypeToPb(tc.authType)
+
+ // Then
+ assert.Equal(t, tc.expected, result)
+ })
+ }
+}
+
+func TestOIDCTokenTypeToPb(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ tokenType domain.OIDCTokenType
+ expected app.OIDCTokenType
+ }{
+ {
+ name: "bearer token type",
+ tokenType: domain.OIDCTokenTypeBearer,
+ expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
+ },
+ {
+ name: "jwt token type",
+ tokenType: domain.OIDCTokenTypeJWT,
+ expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ },
+ {
+ name: "unknown token type defaults to bearer",
+ tokenType: domain.OIDCTokenType(999), // Invalid value to trigger default case
+ expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ result := oidcTokenTypeToPb(tc.tokenType)
+
+ // Then
+ assert.Equal(t, tc.expected, result)
+ })
+ }
+}
diff --git a/internal/api/grpc/app/v2beta/convert/saml_app.go b/internal/api/grpc/app/v2beta/convert/saml_app.go
new file mode 100644
index 0000000000..7f1bef082b
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/convert/saml_app.go
@@ -0,0 +1,77 @@
+package convert
+
+import (
+ "github.com/muhlemmer/gu"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore/v1/models"
+ "github.com/zitadel/zitadel/internal/query"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+)
+
+func CreateSAMLAppRequestToDomain(name, projectID string, req *app.CreateSAMLApplicationRequest) (*domain.SAMLApp, error) {
+ loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion())
+ if err != nil {
+ return nil, err
+ }
+ return &domain.SAMLApp{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: projectID,
+ },
+ AppName: name,
+ Metadata: req.GetMetadataXml(),
+ MetadataURL: gu.Ptr(req.GetMetadataUrl()),
+ LoginVersion: loginVersion,
+ LoginBaseURI: loginBaseURI,
+ }, nil
+}
+
+func UpdateSAMLAppConfigRequestToDomain(appID, projectID string, app *app.UpdateSAMLApplicationConfigurationRequest) (*domain.SAMLApp, error) {
+ loginVersion, loginBaseURI, err := loginVersionToDomain(app.GetLoginVersion())
+ if err != nil {
+ return nil, err
+ }
+
+ metasXML, metasURL := metasToDomain(app.GetMetadata())
+ return &domain.SAMLApp{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: projectID,
+ },
+ AppID: appID,
+ Metadata: metasXML,
+ MetadataURL: metasURL,
+ LoginVersion: loginVersion,
+ LoginBaseURI: loginBaseURI,
+ }, nil
+}
+
+func metasToDomain(metas app.MetaType) ([]byte, *string) {
+ switch t := metas.(type) {
+ case *app.UpdateSAMLApplicationConfigurationRequest_MetadataXml:
+ return t.MetadataXml, nil
+ case *app.UpdateSAMLApplicationConfigurationRequest_MetadataUrl:
+ return nil, &t.MetadataUrl
+ case nil:
+ return nil, nil
+ default:
+ return nil, nil
+ }
+}
+
+func appSAMLConfigToPb(samlApp *query.SAMLApp) app.ApplicationConfig {
+ if samlApp == nil {
+ return &app.Application_SamlConfig{
+ SamlConfig: &app.SAMLConfig{
+ Metadata: &app.SAMLConfig_MetadataXml{},
+ LoginVersion: &app.LoginVersion{},
+ },
+ }
+ }
+
+ return &app.Application_SamlConfig{
+ SamlConfig: &app.SAMLConfig{
+ Metadata: &app.SAMLConfig_MetadataXml{MetadataXml: samlApp.Metadata},
+ LoginVersion: loginVersionToPb(samlApp.LoginVersion, samlApp.LoginBaseURI),
+ },
+ }
+}
diff --git a/internal/api/grpc/app/v2beta/convert/saml_app_test.go b/internal/api/grpc/app/v2beta/convert/saml_app_test.go
new file mode 100644
index 0000000000..b41ec432b6
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/convert/saml_app_test.go
@@ -0,0 +1,256 @@
+package convert
+
+import (
+ "fmt"
+ "net/url"
+ "testing"
+
+ "github.com/brianvoe/gofakeit/v6"
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore/v1/models"
+ "github.com/zitadel/zitadel/internal/query"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+)
+
+func samlMetadataGen(entityID string) []byte {
+ str := fmt.Sprintf(`
+
+
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
+
+
+
+
+`,
+ entityID)
+
+ return []byte(str)
+}
+
+func TestCreateSAMLAppRequestToDomain(t *testing.T) {
+ t.Parallel()
+
+ genMetaForValidRequest := samlMetadataGen(gofakeit.URL())
+
+ tt := []struct {
+ testName string
+ appName string
+ projectID string
+ req *app.CreateSAMLApplicationRequest
+
+ expectedResponse *domain.SAMLApp
+ expectedError error
+ }{
+ {
+ testName: "login version error",
+ appName: "test-app",
+ projectID: "proj-1",
+ req: &app.CreateSAMLApplicationRequest{
+ Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
+ MetadataXml: samlMetadataGen(gofakeit.URL()),
+ },
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")},
+ },
+ },
+ },
+ expectedError: &url.Error{
+ URL: "%+o",
+ Op: "parse",
+ Err: url.EscapeError("%+o"),
+ },
+ },
+ {
+ testName: "valid request",
+ appName: "test-app",
+ projectID: "proj-1",
+ req: &app.CreateSAMLApplicationRequest{
+ Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
+ MetadataXml: genMetaForValidRequest,
+ },
+ LoginVersion: nil,
+ },
+
+ expectedResponse: &domain.SAMLApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
+ AppName: "test-app",
+ Metadata: genMetaForValidRequest,
+ MetadataURL: gu.Ptr(""),
+ LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
+ LoginBaseURI: gu.Ptr(""),
+ State: 0,
+ },
+ },
+ {
+ testName: "nil request",
+ appName: "test-app",
+ projectID: "proj-1",
+ req: nil,
+
+ expectedResponse: &domain.SAMLApp{
+ AppName: "test-app",
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
+ MetadataURL: gu.Ptr(""),
+ LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
+ LoginBaseURI: gu.Ptr(""),
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res, err := CreateSAMLAppRequestToDomain(tc.appName, tc.projectID, tc.req)
+
+ // Then
+ assert.Equal(t, tc.expectedError, err)
+ assert.Equal(t, tc.expectedResponse, res)
+ })
+ }
+}
+func TestUpdateSAMLAppConfigRequestToDomain(t *testing.T) {
+ t.Parallel()
+
+ genMetaForValidRequest := samlMetadataGen(gofakeit.URL())
+
+ tt := []struct {
+ testName string
+ appID string
+ projectID string
+ req *app.UpdateSAMLApplicationConfigurationRequest
+
+ expectedResponse *domain.SAMLApp
+ expectedError error
+ }{
+ {
+ testName: "login version error",
+ appID: "app-1",
+ projectID: "proj-1",
+ req: &app.UpdateSAMLApplicationConfigurationRequest{
+ Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
+ MetadataXml: samlMetadataGen(gofakeit.URL()),
+ },
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")},
+ },
+ },
+ },
+ expectedError: &url.Error{
+ URL: "%+o",
+ Op: "parse",
+ Err: url.EscapeError("%+o"),
+ },
+ },
+ {
+ testName: "valid request",
+ appID: "app-1",
+ projectID: "proj-1",
+ req: &app.UpdateSAMLApplicationConfigurationRequest{
+ Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
+ MetadataXml: genMetaForValidRequest,
+ },
+ LoginVersion: nil,
+ },
+ expectedResponse: &domain.SAMLApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
+ AppID: "app-1",
+ Metadata: genMetaForValidRequest,
+ LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
+ LoginBaseURI: gu.Ptr(""),
+ },
+ },
+ {
+ testName: "nil request",
+ appID: "app-1",
+ projectID: "proj-1",
+ req: nil,
+ expectedResponse: &domain.SAMLApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
+ AppID: "app-1",
+ LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
+ LoginBaseURI: gu.Ptr(""),
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res, err := UpdateSAMLAppConfigRequestToDomain(tc.appID, tc.projectID, tc.req)
+
+ // Then
+ assert.Equal(t, tc.expectedError, err)
+ assert.Equal(t, tc.expectedResponse, res)
+ })
+ }
+}
+
+func TestAppSAMLConfigToPb(t *testing.T) {
+ t.Parallel()
+
+ metadata := samlMetadataGen(gofakeit.URL())
+
+ tt := []struct {
+ name string
+ inputSAMLApp *query.SAMLApp
+
+ expectedPbApp app.ApplicationConfig
+ }{
+ {
+ name: "valid conversion",
+ inputSAMLApp: &query.SAMLApp{
+ Metadata: metadata,
+ LoginVersion: domain.LoginVersion2,
+ LoginBaseURI: gu.Ptr("https://example.com"),
+ },
+ expectedPbApp: &app.Application_SamlConfig{
+ SamlConfig: &app.SAMLConfig{
+ Metadata: &app.SAMLConfig_MetadataXml{
+ MetadataXml: metadata,
+ },
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://example.com")},
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "nil saml app",
+ inputSAMLApp: nil,
+ expectedPbApp: &app.Application_SamlConfig{
+ SamlConfig: &app.SAMLConfig{
+ Metadata: &app.SAMLConfig_MetadataXml{},
+ LoginVersion: &app.LoginVersion{},
+ },
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ got := appSAMLConfigToPb(tc.inputSAMLApp)
+
+ // Then
+ assert.Equal(t, tc.expectedPbApp, got)
+ })
+ }
+}
diff --git a/internal/api/grpc/app/v2beta/integration_test/app_test.go b/internal/api/grpc/app/v2beta/integration_test/app_test.go
new file mode 100644
index 0000000000..1ba46987cf
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/integration_test/app_test.go
@@ -0,0 +1,1446 @@
+//go:build integration
+
+package instance_test
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/brianvoe/gofakeit/v6"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+ org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
+)
+
+func TestCreateApplication(t *testing.T) {
+ p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false)
+
+ t.Parallel()
+
+ notExistingProjectID := gofakeit.UUID()
+
+ tt := []struct {
+ testName string
+ creationRequest *app.CreateApplicationRequest
+ inputCtx context.Context
+
+ expectedResponseType string
+ expectedErrorType codes.Code
+ }{
+ {
+ testName: "when project for API app creation is not found should return failed precondition error",
+ inputCtx: IAMOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: notExistingProjectID,
+ Name: "App Name",
+ CreationRequestType: &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ },
+ },
+ },
+ expectedErrorType: codes.FailedPrecondition,
+ },
+ {
+ testName: "when CreateAPIApp request is valid should create app and return no error",
+ inputCtx: IAMOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: "App Name",
+ CreationRequestType: &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ },
+ },
+ },
+ expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_ApiResponse{}),
+ },
+ {
+ testName: "when project for OIDC app creation is not found should return failed precondition error",
+ inputCtx: IAMOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: notExistingProjectID,
+ Name: "App Name",
+ CreationRequestType: &app.CreateApplicationRequest_OidcRequest{
+ OidcRequest: &app.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"http://example.com"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ PostLogoutRedirectUris: []string{"http://example.com/home"},
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ BackChannelLogoutUri: "http://example.com/logout",
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ },
+ expectedErrorType: codes.FailedPrecondition,
+ },
+ {
+ testName: "when CreateOIDCApp request is valid should create app and return no error",
+ inputCtx: IAMOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: &app.CreateApplicationRequest_OidcRequest{
+ OidcRequest: &app.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"http://example.com"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ PostLogoutRedirectUris: []string{"http://example.com/home"},
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ BackChannelLogoutUri: "http://example.com/logout",
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ },
+
+ expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_OidcResponse{}),
+ },
+ {
+ testName: "when project for SAML app creation is not found should return failed precondition error",
+ inputCtx: IAMOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: notExistingProjectID,
+ Name: gofakeit.AppName(),
+ CreationRequestType: &app.CreateApplicationRequest_SamlRequest{
+ SamlRequest: &app.CreateSAMLApplicationRequest{
+ Metadata: &app.CreateSAMLApplicationRequest_MetadataUrl{
+ MetadataUrl: "http://example.com/metas",
+ },
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ },
+ expectedErrorType: codes.FailedPrecondition,
+ },
+ {
+ testName: "when CreateSAMLApp request is valid should create app and return no error",
+ inputCtx: IAMOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: &app.CreateApplicationRequest_SamlRequest{
+ SamlRequest: &app.CreateSAMLApplicationRequest{
+ Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
+ MetadataXml: samlMetadataGen(gofakeit.URL()),
+ },
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ },
+ expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_SamlResponse{}),
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+ res, err := instance.Client.AppV2Beta.CreateApplication(tc.inputCtx, tc.creationRequest)
+
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ resType := fmt.Sprintf("%T", res.GetCreationResponseType())
+ assert.Equal(t, tc.expectedResponseType, resType)
+ assert.NotZero(t, res.GetAppId())
+ assert.NotZero(t, res.GetCreationDate())
+ }
+ })
+ }
+}
+
+func TestCreateApplication_WithDifferentPermissions(t *testing.T) {
+ p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
+
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ creationRequest *app.CreateApplicationRequest
+ inputCtx context.Context
+
+ expectedResponseType string
+ expectedErrorType codes.Code
+ }{
+ // Login User with no project.app.write
+ {
+ testName: "when user has no project.app.write permission for API request should return permission error",
+ inputCtx: LoginUserCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.Name(),
+ CreationRequestType: &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ },
+ },
+ },
+ expectedErrorType: codes.PermissionDenied,
+ },
+ {
+ testName: "when user has no project.app.write permission for OIDC request should return permission error",
+ inputCtx: LoginUserCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: &app.CreateApplicationRequest_OidcRequest{
+ OidcRequest: &app.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"http://example.com"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ PostLogoutRedirectUris: []string{"http://example.com/home"},
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ BackChannelLogoutUri: "http://example.com/logout",
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ },
+
+ expectedErrorType: codes.PermissionDenied,
+ },
+ {
+ testName: "when user has no project.app.write permission for SAML request should return permission error",
+ inputCtx: LoginUserCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: &app.CreateApplicationRequest_SamlRequest{
+ SamlRequest: &app.CreateSAMLApplicationRequest{
+ Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
+ MetadataXml: samlMetadataGen(gofakeit.URL()),
+ },
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ },
+ expectedErrorType: codes.PermissionDenied,
+ },
+
+ // OrgOwner with project.app.write permission
+ {
+ testName: "when user is OrgOwner API request should succeed",
+ inputCtx: OrgOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.Name(),
+ CreationRequestType: &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ },
+ },
+ },
+ expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_ApiResponse{}),
+ },
+ {
+ testName: "when user is OrgOwner OIDC request should succeed",
+ inputCtx: OrgOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: &app.CreateApplicationRequest_OidcRequest{
+ OidcRequest: &app.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"http://example.com"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ PostLogoutRedirectUris: []string{"http://example.com/home"},
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ BackChannelLogoutUri: "http://example.com/logout",
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ },
+
+ expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_OidcResponse{}),
+ },
+ {
+ testName: "when user is OrgOwner SAML request should succeed",
+ inputCtx: OrgOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: &app.CreateApplicationRequest_SamlRequest{
+ SamlRequest: &app.CreateSAMLApplicationRequest{
+ Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
+ MetadataXml: samlMetadataGen(gofakeit.URL()),
+ },
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ },
+ expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_SamlResponse{}),
+ },
+
+ // Project owner with project.app.write permission
+ {
+ testName: "when user is ProjectOwner API request should succeed",
+ inputCtx: projectOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.Name(),
+ CreationRequestType: &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ },
+ },
+ },
+ expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_ApiResponse{}),
+ },
+ {
+ testName: "when user is ProjectOwner OIDC request should succeed",
+ inputCtx: projectOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: &app.CreateApplicationRequest_OidcRequest{
+ OidcRequest: &app.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"http://example.com"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ PostLogoutRedirectUris: []string{"http://example.com/home"},
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ BackChannelLogoutUri: "http://example.com/logout",
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ },
+
+ expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_OidcResponse{}),
+ },
+ {
+ testName: "when user is ProjectOwner SAML request should succeed",
+ inputCtx: projectOwnerCtx,
+ creationRequest: &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: &app.CreateApplicationRequest_SamlRequest{
+ SamlRequest: &app.CreateSAMLApplicationRequest{
+ Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
+ MetadataXml: samlMetadataGen(gofakeit.URL()),
+ },
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ },
+ expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_SamlResponse{}),
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+ res, err := instance.Client.AppV2Beta.CreateApplication(tc.inputCtx, tc.creationRequest)
+
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ resType := fmt.Sprintf("%T", res.GetCreationResponseType())
+ assert.Equal(t, tc.expectedResponseType, resType)
+ assert.NotZero(t, res.GetAppId())
+ assert.NotZero(t, res.GetCreationDate())
+ }
+ })
+ }
+}
+
+func TestUpdateApplication(t *testing.T) {
+ orgNotInCtx := instance.CreateOrganization(IAMOwnerCtx, gofakeit.Name(), gofakeit.Email())
+ pNotInCtx := instance.CreateProject(IAMOwnerCtx, t, orgNotInCtx.GetOrganizationId(), gofakeit.AppName(), false, false)
+
+ p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false)
+
+ baseURI := "http://example.com"
+
+ t.Cleanup(func() {
+ instance.Client.OrgV2beta.DeleteOrganization(IAMOwnerCtx, &org.DeleteOrganizationRequest{
+ Id: orgNotInCtx.GetOrganizationId(),
+ })
+ })
+
+ reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
+ }
+ reqForAPIAppCreation := reqForAppNameCreation
+
+ reqForOIDCAppCreation := &app.CreateApplicationRequest_OidcRequest{
+ OidcRequest: &app.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"http://example.com"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ PostLogoutRedirectUris: []string{"http://example.com/home"},
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ BackChannelLogoutUri: "http://example.com/logout",
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ }
+
+ samlMetas := samlMetadataGen(gofakeit.URL())
+ reqForSAMLAppCreation := &app.CreateApplicationRequest_SamlRequest{
+ SamlRequest: &app.CreateSAMLApplicationRequest{
+ Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
+ MetadataXml: samlMetas,
+ },
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ }
+
+ appForNameChange, appNameChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: reqForAppNameCreation,
+ })
+ require.Nil(t, appNameChangeErr)
+
+ appForAPIConfigChange, appAPIConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: reqForAPIAppCreation,
+ })
+ require.Nil(t, appAPIConfigChangeErr)
+
+ appForOIDCConfigChange, appOIDCConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: reqForOIDCAppCreation,
+ })
+ require.Nil(t, appOIDCConfigChangeErr)
+
+ appForSAMLConfigChange, appSAMLConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: reqForSAMLAppCreation,
+ })
+ require.Nil(t, appSAMLConfigChangeErr)
+
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputCtx context.Context
+ updateRequest *app.UpdateApplicationRequest
+
+ expectedErrorType codes.Code
+ }{
+ {
+ testName: "when app for app name change request is not found should return not found error",
+ inputCtx: IAMOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: pNotInCtx.GetId(),
+ Id: appForNameChange.GetAppId(),
+ Name: "New name",
+ },
+ expectedErrorType: codes.NotFound,
+ },
+ {
+ testName: "when request for app name change is valid should return updated timestamp",
+ inputCtx: IAMOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForNameChange.GetAppId(),
+
+ Name: "New name",
+ },
+ },
+
+ {
+ testName: "when app for API config change request is not found should return not found error",
+ inputCtx: IAMOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: pNotInCtx.GetId(),
+ Id: appForAPIConfigChange.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{
+ ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ },
+ },
+ },
+ expectedErrorType: codes.NotFound,
+ },
+ {
+ testName: "when request for API config change is valid should return updated timestamp",
+ inputCtx: IAMOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForAPIConfigChange.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{
+ ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
+ },
+ },
+ },
+ },
+ {
+ testName: "when app for OIDC config change request is not found should return not found error",
+ inputCtx: IAMOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: pNotInCtx.GetId(),
+ Id: appForOIDCConfigChange.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{
+ OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{
+ PostLogoutRedirectUris: []string{"http://example.com/home2"},
+ },
+ },
+ },
+ expectedErrorType: codes.NotFound,
+ },
+ {
+ testName: "when request for OIDC config change is valid should return updated timestamp",
+ inputCtx: IAMOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForOIDCConfigChange.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{
+ OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{
+ PostLogoutRedirectUris: []string{"http://example.com/home2"},
+ },
+ },
+ },
+ },
+
+ {
+ testName: "when app for SAML config change request is not found should return not found error",
+ inputCtx: IAMOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: pNotInCtx.GetId(),
+ Id: appForSAMLConfigChange.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{
+ SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{
+ Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
+ MetadataXml: samlMetas,
+ },
+ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}},
+ },
+ },
+ },
+ expectedErrorType: codes.NotFound,
+ },
+ {
+ testName: "when request for SAML config change is valid should return updated timestamp",
+ inputCtx: IAMOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForSAMLConfigChange.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{
+ SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{
+ Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
+ MetadataXml: samlMetas,
+ },
+ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}},
+ },
+ },
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+ res, err := instance.Client.AppV2Beta.UpdateApplication(tc.inputCtx, tc.updateRequest)
+
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ assert.NotZero(t, res.GetChangeDate())
+ }
+ })
+ }
+}
+
+func TestUpdateApplication_WithDifferentPermissions(t *testing.T) {
+ baseURI := "http://example.com"
+
+ p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
+
+ reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
+ }
+
+ appForNameChange, appNameChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: reqForAppNameCreation,
+ })
+ require.Nil(t, appNameChangeErr)
+
+ appForAPIConfigChangeForProjectOwner := createAPIApp(t, p.GetId())
+ appForAPIConfigChangeForOrgOwner := createAPIApp(t, p.GetId())
+ appForAPIConfigChangeForLoginUser := createAPIApp(t, p.GetId())
+
+ appForOIDCConfigChangeForProjectOwner := createOIDCApp(t, baseURI, p.GetId())
+ appForOIDCConfigChangeForOrgOwner := createOIDCApp(t, baseURI, p.GetId())
+ appForOIDCConfigChangeForLoginUser := createOIDCApp(t, baseURI, p.GetId())
+
+ samlMetasForProjectOwner, appForSAMLConfigChangeForProjectOwner := createSAMLApp(t, baseURI, p.GetId())
+ samlMetasForOrgOwner, appForSAMLConfigChangeForOrgOwner := createSAMLApp(t, baseURI, p.GetId())
+ samlMetasForLoginUser, appForSAMLConfigChangeForLoginUser := createSAMLApp(t, baseURI, p.GetId())
+
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputCtx context.Context
+ updateRequest *app.UpdateApplicationRequest
+
+ expectedErrorType codes.Code
+ }{
+ // ProjectOwner
+ {
+ testName: "when user is ProjectOwner app name request should succeed",
+ inputCtx: projectOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForNameChange.GetAppId(),
+
+ Name: gofakeit.AppName(),
+ },
+ },
+ {
+ testName: "when user is ProjectOwner API app request should succeed",
+ inputCtx: projectOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForAPIConfigChangeForProjectOwner.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{
+ ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
+ },
+ },
+ },
+ },
+ {
+ testName: "when user is ProjectOwner OIDC app request should succeed",
+ inputCtx: projectOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForOIDCConfigChangeForProjectOwner.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{
+ OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{
+ PostLogoutRedirectUris: []string{"http://example.com/home2"},
+ },
+ },
+ },
+ },
+ {
+ testName: "when user is ProjectOwner SAML request should succeed",
+ inputCtx: projectOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForSAMLConfigChangeForProjectOwner.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{
+ SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{
+ Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
+ MetadataXml: samlMetasForProjectOwner,
+ },
+ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}},
+ },
+ },
+ },
+ },
+
+ // OrgOwner context
+ {
+ testName: "when user is OrgOwner app name request should succeed",
+ inputCtx: OrgOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForNameChange.GetAppId(),
+
+ Name: gofakeit.AppName(),
+ },
+ },
+ {
+ testName: "when user is OrgOwner API app request should succeed",
+ inputCtx: OrgOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForAPIConfigChangeForOrgOwner.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{
+ ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
+ },
+ },
+ },
+ },
+ {
+ testName: "when user is OrgOwner OIDC app request should succeed",
+ inputCtx: OrgOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForOIDCConfigChangeForOrgOwner.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{
+ OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{
+ PostLogoutRedirectUris: []string{"http://example.com/home2"},
+ },
+ },
+ },
+ },
+ {
+ testName: "when user is OrgOwner SAML request should succeed",
+ inputCtx: OrgOwnerCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForSAMLConfigChangeForOrgOwner.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{
+ SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{
+ Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
+ MetadataXml: samlMetasForOrgOwner,
+ },
+ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}},
+ },
+ },
+ },
+ },
+
+ // LoginUser
+ {
+ testName: "when user has no project.app.write permission for app name change request should return permission error",
+ inputCtx: LoginUserCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForNameChange.GetAppId(),
+
+ Name: gofakeit.AppName(),
+ },
+ expectedErrorType: codes.PermissionDenied,
+ },
+ {
+ testName: "when user has no project.app.write permission for API request should return permission error",
+ inputCtx: LoginUserCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForAPIConfigChangeForLoginUser.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{
+ ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
+ },
+ },
+ },
+ expectedErrorType: codes.PermissionDenied,
+ },
+ {
+ testName: "when user has no project.app.write permission for OIDC request should return permission error",
+ inputCtx: LoginUserCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForOIDCConfigChangeForLoginUser.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{
+ OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{
+ PostLogoutRedirectUris: []string{"http://example.com/home2"},
+ },
+ },
+ },
+ expectedErrorType: codes.PermissionDenied,
+ },
+ {
+ testName: "when user has no project.app.write permission for SAML request should return permission error",
+ inputCtx: LoginUserCtx,
+ updateRequest: &app.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appForSAMLConfigChangeForLoginUser.GetAppId(),
+ UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{
+ SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{
+ Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
+ MetadataXml: samlMetasForLoginUser,
+ },
+ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}},
+ },
+ },
+ },
+ expectedErrorType: codes.PermissionDenied,
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+ res, err := instance.Client.AppV2Beta.UpdateApplication(tc.inputCtx, tc.updateRequest)
+
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ assert.NotZero(t, res.GetChangeDate())
+ }
+ })
+ }
+}
+
+func TestDeleteApplication(t *testing.T) {
+ p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false)
+
+ reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
+ }
+
+ appToDelete, appNameChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: reqForAppNameCreation,
+ })
+ require.Nil(t, appNameChangeErr)
+
+ t.Parallel()
+ tt := []struct {
+ testName string
+ deleteRequest *app.DeleteApplicationRequest
+ inputCtx context.Context
+
+ expectedErrorType codes.Code
+ }{
+ {
+ testName: "when app to delete is not found should return not found error",
+ inputCtx: IAMOwnerCtx,
+ deleteRequest: &app.DeleteApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: gofakeit.Sentence(2),
+ },
+ expectedErrorType: codes.NotFound,
+ },
+ {
+ testName: "when app to delete is found should return deletion time",
+ inputCtx: IAMOwnerCtx,
+ deleteRequest: &app.DeleteApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToDelete.GetAppId(),
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res, err := instance.Client.AppV2Beta.DeleteApplication(tc.inputCtx, tc.deleteRequest)
+
+ // Then
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ assert.NotZero(t, res.GetDeletionDate())
+ }
+ })
+ }
+}
+
+func TestDeleteApplication_WithDifferentPermissions(t *testing.T) {
+ p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
+
+ appToDeleteForLoginUser := createAPIApp(t, p.GetId())
+ appToDeleteForProjectOwner := createAPIApp(t, p.GetId())
+ appToDeleteForOrgOwner := createAPIApp(t, p.GetId())
+
+ t.Parallel()
+ tt := []struct {
+ testName string
+ deleteRequest *app.DeleteApplicationRequest
+ inputCtx context.Context
+
+ expectedErrorType codes.Code
+ }{
+ // Login User
+ {
+ testName: "when user has no project.app.delete permission for app delete request should return permission error",
+ inputCtx: LoginUserCtx,
+ deleteRequest: &app.DeleteApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToDeleteForLoginUser.GetAppId(),
+ },
+ expectedErrorType: codes.PermissionDenied,
+ },
+
+ // Project Owner
+ {
+ testName: "when user is ProjectOwner delete app request should succeed",
+ inputCtx: projectOwnerCtx,
+ deleteRequest: &app.DeleteApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToDeleteForProjectOwner.GetAppId(),
+ },
+ },
+
+ // Org Owner
+ {
+ testName: "when user is OrgOwner delete app request should succeed",
+ inputCtx: projectOwnerCtx,
+ deleteRequest: &app.DeleteApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToDeleteForOrgOwner.GetAppId(),
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res, err := instance.Client.AppV2Beta.DeleteApplication(tc.inputCtx, tc.deleteRequest)
+
+ // Then
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ assert.NotZero(t, res.GetDeletionDate())
+ }
+ })
+ }
+}
+
+func TestDeactivateApplication(t *testing.T) {
+ p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false)
+
+ reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
+ }
+
+ appToDeactivate, appCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: reqForAppNameCreation,
+ })
+ require.NoError(t, appCreateErr)
+
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputCtx context.Context
+ deleteRequest *app.DeactivateApplicationRequest
+
+ expectedErrorType codes.Code
+ }{
+ {
+ testName: "when app to deactivate is not found should return not found error",
+ inputCtx: IAMOwnerCtx,
+ deleteRequest: &app.DeactivateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: gofakeit.Sentence(2),
+ },
+ expectedErrorType: codes.NotFound,
+ },
+ {
+ testName: "when app to deactivate is found should return deactivation time",
+ inputCtx: IAMOwnerCtx,
+ deleteRequest: &app.DeactivateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToDeactivate.GetAppId(),
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res, err := instance.Client.AppV2Beta.DeactivateApplication(tc.inputCtx, tc.deleteRequest)
+
+ // Then
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ assert.NotZero(t, res.GetDeactivationDate())
+ }
+ })
+ }
+}
+
+func TestDeactivateApplication_WithDifferentPermissions(t *testing.T) {
+ p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
+
+ appToDeactivateForLoginUser := createAPIApp(t, p.GetId())
+ appToDeactivateForPrjectOwner := createAPIApp(t, p.GetId())
+ appToDeactivateForOrgOwner := createAPIApp(t, p.GetId())
+
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputCtx context.Context
+ deleteRequest *app.DeactivateApplicationRequest
+
+ expectedErrorType codes.Code
+ }{
+ // Login User
+ {
+ testName: "when user has no project.app.write permission for app deactivate request should return permission error",
+ inputCtx: IAMOwnerCtx,
+ deleteRequest: &app.DeactivateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToDeactivateForLoginUser.GetAppId(),
+ },
+ },
+
+ // Project Owner
+ {
+ testName: "when user is ProjectOwner deactivate app request should succeed",
+ inputCtx: projectOwnerCtx,
+ deleteRequest: &app.DeactivateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToDeactivateForPrjectOwner.GetAppId(),
+ },
+ },
+
+ // Org Owner
+ {
+ testName: "when user is OrgOwner deactivate app request should succeed",
+ inputCtx: OrgOwnerCtx,
+ deleteRequest: &app.DeactivateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToDeactivateForOrgOwner.GetAppId(),
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res, err := instance.Client.AppV2Beta.DeactivateApplication(tc.inputCtx, tc.deleteRequest)
+
+ // Then
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ assert.NotZero(t, res.GetDeactivationDate())
+ }
+ })
+ }
+}
+
+func TestReactivateApplication(t *testing.T) {
+ p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false)
+
+ reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
+ }
+
+ appToReactivate, appCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: reqForAppNameCreation,
+ })
+ require.Nil(t, appCreateErr)
+
+ _, appDeactivateErr := instance.Client.AppV2Beta.DeactivateApplication(IAMOwnerCtx, &app.DeactivateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToReactivate.GetAppId(),
+ })
+ require.Nil(t, appDeactivateErr)
+
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputCtx context.Context
+ reactivateRequest *app.ReactivateApplicationRequest
+
+ expectedErrorType codes.Code
+ }{
+ {
+ testName: "when app to reactivate is not found should return not found error",
+ inputCtx: IAMOwnerCtx,
+ reactivateRequest: &app.ReactivateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: gofakeit.Sentence(2),
+ },
+ expectedErrorType: codes.NotFound,
+ },
+ {
+ testName: "when app to reactivate is found should return deactivation time",
+ inputCtx: IAMOwnerCtx,
+ reactivateRequest: &app.ReactivateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToReactivate.GetAppId(),
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res, err := instance.Client.AppV2Beta.ReactivateApplication(tc.inputCtx, tc.reactivateRequest)
+
+ // Then
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ assert.NotZero(t, res.GetReactivationDate())
+ }
+ })
+ }
+}
+
+func TestReactivateApplication_WithDifferentPermissions(t *testing.T) {
+ p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
+
+ appToReactivateForLoginUser := createAPIApp(t, p.GetId())
+ deactivateApp(t, appToReactivateForLoginUser, p.GetId())
+
+ appToReactivateForProjectOwner := createAPIApp(t, p.GetId())
+ deactivateApp(t, appToReactivateForProjectOwner, p.GetId())
+
+ appToReactivateForOrgOwner := createAPIApp(t, p.GetId())
+ deactivateApp(t, appToReactivateForOrgOwner, p.GetId())
+
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputCtx context.Context
+ reactivateRequest *app.ReactivateApplicationRequest
+
+ expectedErrorType codes.Code
+ }{
+ // Login User
+ {
+ testName: "when user has no project.app.write permission for app reactivate request should return permission error",
+ inputCtx: LoginUserCtx,
+ reactivateRequest: &app.ReactivateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToReactivateForLoginUser.GetAppId(),
+ },
+ expectedErrorType: codes.PermissionDenied,
+ },
+
+ // Project Owner
+ {
+ testName: "when user is ProjectOwner reactivate app request should succeed",
+ inputCtx: projectOwnerCtx,
+ reactivateRequest: &app.ReactivateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToReactivateForProjectOwner.GetAppId(),
+ },
+ },
+
+ // Org Owner
+ {
+ testName: "when user is OrgOwner reactivate app request should succeed",
+ inputCtx: OrgOwnerCtx,
+ reactivateRequest: &app.ReactivateApplicationRequest{
+ ProjectId: p.GetId(),
+ Id: appToReactivateForOrgOwner.GetAppId(),
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res, err := instance.Client.AppV2Beta.ReactivateApplication(tc.inputCtx, tc.reactivateRequest)
+
+ // Then
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ assert.NotZero(t, res.GetReactivationDate())
+ }
+ })
+ }
+}
+
+func TestRegenerateClientSecret(t *testing.T) {
+ p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false)
+
+ reqForApiAppCreation := &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
+ }
+
+ apiAppToRegen, apiAppCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: reqForApiAppCreation,
+ })
+ require.Nil(t, apiAppCreateErr)
+
+ reqForOIDCAppCreation := &app.CreateApplicationRequest_OidcRequest{
+ OidcRequest: &app.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"http://example.com"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ PostLogoutRedirectUris: []string{"http://example.com/home"},
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ BackChannelLogoutUri: "http://example.com/logout",
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ }
+
+ oidcAppToRegen, oidcAppCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: gofakeit.AppName(),
+ CreationRequestType: reqForOIDCAppCreation,
+ })
+ require.Nil(t, oidcAppCreateErr)
+
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputCtx context.Context
+ regenRequest *app.RegenerateClientSecretRequest
+
+ expectedErrorType codes.Code
+ oldSecret string
+ }{
+ {
+ testName: "when app to regen is not expected type should return invalid argument error",
+ inputCtx: IAMOwnerCtx,
+ regenRequest: &app.RegenerateClientSecretRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: gofakeit.Sentence(2),
+ },
+ expectedErrorType: codes.InvalidArgument,
+ },
+ {
+ testName: "when app to regen is not found should return not found error",
+ inputCtx: IAMOwnerCtx,
+ regenRequest: &app.RegenerateClientSecretRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: gofakeit.Sentence(2),
+ AppType: &app.RegenerateClientSecretRequest_IsApi{},
+ },
+ expectedErrorType: codes.NotFound,
+ },
+ {
+ testName: "when API app to regen is found should return different secret",
+ inputCtx: IAMOwnerCtx,
+ regenRequest: &app.RegenerateClientSecretRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: apiAppToRegen.GetAppId(),
+ AppType: &app.RegenerateClientSecretRequest_IsApi{},
+ },
+ oldSecret: apiAppToRegen.GetApiResponse().GetClientSecret(),
+ },
+ {
+ testName: "when OIDC app to regen is found should return different secret",
+ inputCtx: IAMOwnerCtx,
+ regenRequest: &app.RegenerateClientSecretRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: oidcAppToRegen.GetAppId(),
+ AppType: &app.RegenerateClientSecretRequest_IsOidc{},
+ },
+ oldSecret: oidcAppToRegen.GetOidcResponse().GetClientSecret(),
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res, err := instance.Client.AppV2Beta.RegenerateClientSecret(tc.inputCtx, tc.regenRequest)
+
+ // Then
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ assert.NotZero(t, res.GetCreationDate())
+ assert.NotEqual(t, tc.oldSecret, res.GetClientSecret())
+ }
+ })
+ }
+
+}
+
+func TestRegenerateClientSecret_WithDifferentPermissions(t *testing.T) {
+ p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
+
+ apiAppToRegenForLoginUser := createAPIApp(t, p.GetId())
+ apiAppToRegenForProjectOwner := createAPIApp(t, p.GetId())
+ apiAppToRegenForOrgOwner := createAPIApp(t, p.GetId())
+
+ oidcAppToRegenForLoginUser := createOIDCApp(t, baseURI, p.GetId())
+ oidcAppToRegenForProjectOwner := createOIDCApp(t, baseURI, p.GetId())
+ oidcAppToRegenForOrgOwner := createOIDCApp(t, baseURI, p.GetId())
+
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputCtx context.Context
+ regenRequest *app.RegenerateClientSecretRequest
+
+ expectedErrorType codes.Code
+ oldSecret string
+ }{
+ // Login user
+ {
+ testName: "when user has no project.app.write permission for API app secret regen request should return permission error",
+ inputCtx: LoginUserCtx,
+ regenRequest: &app.RegenerateClientSecretRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: apiAppToRegenForLoginUser.GetAppId(),
+ AppType: &app.RegenerateClientSecretRequest_IsApi{},
+ },
+ expectedErrorType: codes.PermissionDenied,
+ },
+ {
+ testName: "when user has no project.app.write permission for OIDC app secret regen request should return permission error",
+ inputCtx: LoginUserCtx,
+ regenRequest: &app.RegenerateClientSecretRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: oidcAppToRegenForLoginUser.GetAppId(),
+ AppType: &app.RegenerateClientSecretRequest_IsOidc{},
+ },
+ expectedErrorType: codes.PermissionDenied,
+ },
+
+ // Project Owner
+ {
+ testName: "when user is ProjectOwner regen API app secret request should succeed",
+ inputCtx: projectOwnerCtx,
+ regenRequest: &app.RegenerateClientSecretRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: apiAppToRegenForProjectOwner.GetAppId(),
+ AppType: &app.RegenerateClientSecretRequest_IsApi{},
+ },
+ oldSecret: apiAppToRegenForProjectOwner.GetApiResponse().GetClientSecret(),
+ },
+ {
+ testName: "when user is ProjectOwner regen OIDC app secret request should succeed",
+ inputCtx: projectOwnerCtx,
+ regenRequest: &app.RegenerateClientSecretRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: oidcAppToRegenForProjectOwner.GetAppId(),
+ AppType: &app.RegenerateClientSecretRequest_IsOidc{},
+ },
+ oldSecret: oidcAppToRegenForProjectOwner.GetOidcResponse().GetClientSecret(),
+ },
+
+ // Org Owner
+ {
+ testName: "when user is OrgOwner regen API app secret request should succeed",
+ inputCtx: OrgOwnerCtx,
+ regenRequest: &app.RegenerateClientSecretRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: apiAppToRegenForOrgOwner.GetAppId(),
+ AppType: &app.RegenerateClientSecretRequest_IsApi{},
+ },
+ oldSecret: apiAppToRegenForOrgOwner.GetApiResponse().GetClientSecret(),
+ },
+ {
+ testName: "when user is OrgOwner regen OIDC app secret request should succeed",
+ inputCtx: OrgOwnerCtx,
+ regenRequest: &app.RegenerateClientSecretRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: oidcAppToRegenForOrgOwner.GetAppId(),
+ AppType: &app.RegenerateClientSecretRequest_IsOidc{},
+ },
+ oldSecret: oidcAppToRegenForOrgOwner.GetOidcResponse().GetClientSecret(),
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ // When
+ res, err := instance.Client.AppV2Beta.RegenerateClientSecret(tc.inputCtx, tc.regenRequest)
+
+ // Then
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+ assert.NotZero(t, res.GetCreationDate())
+ assert.NotEqual(t, tc.oldSecret, res.GetClientSecret())
+ }
+ })
+ }
+
+}
diff --git a/internal/api/grpc/app/v2beta/integration_test/query_test.go b/internal/api/grpc/app/v2beta/integration_test/query_test.go
new file mode 100644
index 0000000000..578fcec138
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/integration_test/query_test.go
@@ -0,0 +1,575 @@
+//go:build integration
+
+package instance_test
+
+import (
+ "context"
+ "fmt"
+ "slices"
+ "testing"
+ "time"
+
+ "github.com/brianvoe/gofakeit/v6"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+
+ "github.com/zitadel/zitadel/internal/integration"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+ "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
+)
+
+func TestGetApplication(t *testing.T) {
+ p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
+
+ apiAppName := gofakeit.AppName()
+ createdApiApp, errAPIAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: apiAppName,
+ CreationRequestType: &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
+ },
+ },
+ })
+ require.Nil(t, errAPIAppCreation)
+
+ samlAppName := gofakeit.AppName()
+ createdSAMLApp, errSAMLAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: samlAppName,
+ CreationRequestType: &app.CreateApplicationRequest_SamlRequest{
+ SamlRequest: &app.CreateSAMLApplicationRequest{
+ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}},
+ Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{MetadataXml: samlMetadataGen(gofakeit.URL())},
+ },
+ },
+ })
+ require.Nil(t, errSAMLAppCreation)
+
+ oidcAppName := gofakeit.AppName()
+ createdOIDCApp, errOIDCAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: oidcAppName,
+ CreationRequestType: &app.CreateApplicationRequest_OidcRequest{
+ OidcRequest: &app.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"http://example.com"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ PostLogoutRedirectUris: []string{"http://example.com/home"},
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ BackChannelLogoutUri: "http://example.com/logout",
+ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: &baseURI}}},
+ },
+ },
+ })
+ require.Nil(t, errOIDCAppCreation)
+
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputRequest *app.GetApplicationRequest
+ inputCtx context.Context
+
+ expectedErrorType codes.Code
+ expectedAppName string
+ expectedAppID string
+ expectedApplicationType string
+ }{
+ {
+ testName: "when unknown app ID should return not found error",
+ inputCtx: IAMOwnerCtx,
+ inputRequest: &app.GetApplicationRequest{
+ Id: gofakeit.Sentence(2),
+ },
+
+ expectedErrorType: codes.NotFound,
+ },
+ {
+ testName: "when user has no permission should return membership not found error",
+ inputCtx: NoPermissionCtx,
+ inputRequest: &app.GetApplicationRequest{
+ Id: createdApiApp.GetAppId(),
+ },
+
+ expectedErrorType: codes.NotFound,
+ },
+ {
+ testName: "when providing API app ID should return valid API app result",
+ inputCtx: projectOwnerCtx,
+ inputRequest: &app.GetApplicationRequest{
+ Id: createdApiApp.GetAppId(),
+ },
+
+ expectedAppName: apiAppName,
+ expectedAppID: createdApiApp.GetAppId(),
+ expectedApplicationType: fmt.Sprintf("%T", &app.Application_ApiConfig{}),
+ },
+ {
+ testName: "when providing SAML app ID should return valid SAML app result",
+ inputCtx: IAMOwnerCtx,
+ inputRequest: &app.GetApplicationRequest{
+ Id: createdSAMLApp.GetAppId(),
+ },
+
+ expectedAppName: samlAppName,
+ expectedAppID: createdSAMLApp.GetAppId(),
+ expectedApplicationType: fmt.Sprintf("%T", &app.Application_SamlConfig{}),
+ },
+ {
+ testName: "when providing OIDC app ID should return valid OIDC app result",
+ inputCtx: IAMOwnerCtx,
+ inputRequest: &app.GetApplicationRequest{
+ Id: createdOIDCApp.GetAppId(),
+ },
+
+ expectedAppName: oidcAppName,
+ expectedAppID: createdOIDCApp.GetAppId(),
+ expectedApplicationType: fmt.Sprintf("%T", &app.Application_OidcConfig{}),
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+
+ retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 30*time.Second)
+ require.EventuallyWithT(t, func(ttt *assert.CollectT) {
+ // When
+ res, err := instance.Client.AppV2Beta.GetApplication(tc.inputCtx, tc.inputRequest)
+
+ // Then
+ require.Equal(t, tc.expectedErrorType, status.Code(err))
+ if tc.expectedErrorType == codes.OK {
+
+ assert.Equal(t, tc.expectedAppID, res.GetApp().GetId())
+ assert.Equal(t, tc.expectedAppName, res.GetApp().GetName())
+ assert.NotZero(t, res.GetApp().GetCreationDate())
+ assert.NotZero(t, res.GetApp().GetChangeDate())
+
+ appType := fmt.Sprintf("%T", res.GetApp().GetConfig())
+ assert.Equal(t, tc.expectedApplicationType, appType)
+ }
+ }, retryDuration, tick)
+ })
+ }
+}
+
+func TestListApplications(t *testing.T) {
+ p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
+
+ t.Parallel()
+
+ createdApiApp, apiAppName := createAPIAppWithName(t, p.GetId())
+
+ createdDeactivatedApiApp, deactivatedApiAppName := createAPIAppWithName(t, p.GetId())
+ deactivateApp(t, createdDeactivatedApiApp, p.GetId())
+
+ _, createdSAMLApp, samlAppName := createSAMLAppWithName(t, gofakeit.URL(), p.GetId())
+
+ createdOIDCApp, oidcAppName := createOIDCAppWithName(t, gofakeit.URL(), p.GetId())
+
+ type appWithName struct {
+ app *app.CreateApplicationResponse
+ name string
+ }
+
+ // Sorting
+ appsSortedByName := []appWithName{
+ {name: apiAppName, app: createdApiApp},
+ {name: deactivatedApiAppName, app: createdDeactivatedApiApp},
+ {name: samlAppName, app: createdSAMLApp},
+ {name: oidcAppName, app: createdOIDCApp},
+ }
+ slices.SortFunc(appsSortedByName, func(a, b appWithName) int {
+ if a.name < b.name {
+ return -1
+ }
+ if a.name > b.name {
+ return 1
+ }
+
+ return 0
+ })
+
+ appsSortedByID := []appWithName{
+ {name: apiAppName, app: createdApiApp},
+ {name: deactivatedApiAppName, app: createdDeactivatedApiApp},
+ {name: samlAppName, app: createdSAMLApp},
+ {name: oidcAppName, app: createdOIDCApp},
+ }
+ slices.SortFunc(appsSortedByID, func(a, b appWithName) int {
+ if a.app.GetAppId() < b.app.GetAppId() {
+ return -1
+ }
+ if a.app.GetAppId() > b.app.GetAppId() {
+ return 1
+ }
+ return 0
+ })
+
+ appsSortedByCreationDate := []appWithName{
+ {name: apiAppName, app: createdApiApp},
+ {name: deactivatedApiAppName, app: createdDeactivatedApiApp},
+ {name: samlAppName, app: createdSAMLApp},
+ {name: oidcAppName, app: createdOIDCApp},
+ }
+ slices.SortFunc(appsSortedByCreationDate, func(a, b appWithName) int {
+ aCreationDate := a.app.GetCreationDate().AsTime()
+ bCreationDate := b.app.GetCreationDate().AsTime()
+
+ if aCreationDate.Before(bCreationDate) {
+ return -1
+ }
+ if bCreationDate.Before(aCreationDate) {
+ return 1
+ }
+
+ return 0
+ })
+
+ tt := []struct {
+ testName string
+ inputRequest *app.ListApplicationsRequest
+ inputCtx context.Context
+
+ expectedOrderedList []appWithName
+ expectedOrderedKeys func(keys []appWithName) any
+ actualOrderedKeys func(keys []*app.Application) any
+ }{
+ {
+ testName: "when no apps found should return empty list",
+ inputCtx: IAMOwnerCtx,
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: "another-id",
+ },
+
+ expectedOrderedList: []appWithName{},
+ expectedOrderedKeys: func(keys []appWithName) any { return keys },
+ actualOrderedKeys: func(keys []*app.Application) any { return keys },
+ },
+ {
+ testName: "when user has no read permission should return empty set",
+ inputCtx: NoPermissionCtx,
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ },
+
+ expectedOrderedList: []appWithName{},
+ expectedOrderedKeys: func(keys []appWithName) any { return keys },
+ actualOrderedKeys: func(keys []*app.Application) any { return keys },
+ },
+ {
+ testName: "when sorting by name should return apps sorted by name in descending order",
+ inputCtx: IAMOwnerCtx,
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ SortingColumn: app.AppSorting_APP_SORT_BY_NAME,
+ Pagination: &filter.PaginationRequest{Asc: true},
+ },
+
+ expectedOrderedList: appsSortedByName,
+ expectedOrderedKeys: func(apps []appWithName) any {
+ names := make([]string, len(apps))
+ for i, a := range apps {
+ names[i] = a.name
+ }
+
+ return names
+ },
+ actualOrderedKeys: func(apps []*app.Application) any {
+ names := make([]string, len(apps))
+ for i, a := range apps {
+ names[i] = a.GetName()
+ }
+
+ return names
+ },
+ },
+
+ {
+ testName: "when user is project owner should return apps sorted by name in ascending order",
+ inputCtx: projectOwnerCtx,
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ SortingColumn: app.AppSorting_APP_SORT_BY_NAME,
+ Pagination: &filter.PaginationRequest{Asc: true},
+ },
+
+ expectedOrderedList: appsSortedByName,
+ expectedOrderedKeys: func(apps []appWithName) any {
+ names := make([]string, len(apps))
+ for i, a := range apps {
+ names[i] = a.name
+ }
+
+ return names
+ },
+ actualOrderedKeys: func(apps []*app.Application) any {
+ names := make([]string, len(apps))
+ for i, a := range apps {
+ names[i] = a.GetName()
+ }
+
+ return names
+ },
+ },
+
+ {
+ testName: "when sorting by id should return apps sorted by id in descending order",
+ inputCtx: IAMOwnerCtx,
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ SortingColumn: app.AppSorting_APP_SORT_BY_ID,
+ Pagination: &filter.PaginationRequest{Asc: true},
+ },
+ expectedOrderedList: appsSortedByID,
+ expectedOrderedKeys: func(apps []appWithName) any {
+ ids := make([]string, len(apps))
+ for i, a := range apps {
+ ids[i] = a.app.GetAppId()
+ }
+
+ return ids
+ },
+ actualOrderedKeys: func(apps []*app.Application) any {
+ ids := make([]string, len(apps))
+ for i, a := range apps {
+ ids[i] = a.GetId()
+ }
+
+ return ids
+ },
+ },
+ {
+ testName: "when sorting by creation date should return apps sorted by creation date in descending order",
+ inputCtx: IAMOwnerCtx,
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ SortingColumn: app.AppSorting_APP_SORT_BY_CREATION_DATE,
+ Pagination: &filter.PaginationRequest{Asc: true},
+ },
+ expectedOrderedList: appsSortedByCreationDate,
+ expectedOrderedKeys: func(apps []appWithName) any {
+ creationDates := make([]time.Time, len(apps))
+ for i, a := range apps {
+ creationDates[i] = a.app.GetCreationDate().AsTime()
+ }
+
+ return creationDates
+ },
+ actualOrderedKeys: func(apps []*app.Application) any {
+ creationDates := make([]time.Time, len(apps))
+ for i, a := range apps {
+ creationDates[i] = a.GetCreationDate().AsTime()
+ }
+
+ return creationDates
+ },
+ },
+ {
+ testName: "when filtering by active apps should return active apps only",
+ inputCtx: IAMOwnerCtx,
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ Pagination: &filter.PaginationRequest{Asc: true},
+ Filters: []*app.ApplicationSearchFilter{
+ {Filter: &app.ApplicationSearchFilter_StateFilter{StateFilter: app.AppState_APP_STATE_ACTIVE}},
+ },
+ },
+ expectedOrderedList: slices.DeleteFunc(
+ slices.Clone(appsSortedByID),
+ func(a appWithName) bool { return a.name == deactivatedApiAppName },
+ ),
+ expectedOrderedKeys: func(apps []appWithName) any {
+ creationDates := make([]time.Time, len(apps))
+ for i, a := range apps {
+ creationDates[i] = a.app.GetCreationDate().AsTime()
+ }
+
+ return creationDates
+ },
+ actualOrderedKeys: func(apps []*app.Application) any {
+ creationDates := make([]time.Time, len(apps))
+ for i, a := range apps {
+ creationDates[i] = a.GetCreationDate().AsTime()
+ }
+
+ return creationDates
+ },
+ },
+ {
+ testName: "when filtering by app type should return apps of matching type only",
+ inputCtx: IAMOwnerCtx,
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ Pagination: &filter.PaginationRequest{Asc: true},
+ Filters: []*app.ApplicationSearchFilter{
+ {Filter: &app.ApplicationSearchFilter_OidcAppOnly{}},
+ },
+ },
+ expectedOrderedList: slices.DeleteFunc(
+ slices.Clone(appsSortedByID),
+ func(a appWithName) bool { return a.name != oidcAppName },
+ ),
+ expectedOrderedKeys: func(apps []appWithName) any {
+ creationDates := make([]time.Time, len(apps))
+ for i, a := range apps {
+ creationDates[i] = a.app.GetCreationDate().AsTime()
+ }
+
+ return creationDates
+ },
+ actualOrderedKeys: func(apps []*app.Application) any {
+ creationDates := make([]time.Time, len(apps))
+ for i, a := range apps {
+ creationDates[i] = a.GetCreationDate().AsTime()
+ }
+
+ return creationDates
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+ retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 30*time.Second)
+ require.EventuallyWithT(t, func(ttt *assert.CollectT) {
+ // When
+ res, err := instance.Client.AppV2Beta.ListApplications(tc.inputCtx, tc.inputRequest)
+
+ // Then
+ require.Equal(ttt, codes.OK, status.Code(err))
+
+ if err == nil {
+ assert.Len(ttt, res.GetApplications(), len(tc.expectedOrderedList))
+ actualOrderedKeys := tc.actualOrderedKeys(res.GetApplications())
+ expectedOrderedKeys := tc.expectedOrderedKeys(tc.expectedOrderedList)
+ assert.ElementsMatch(ttt, expectedOrderedKeys, actualOrderedKeys)
+ }
+ }, retryDuration, tick)
+ })
+ }
+}
+
+func TestListApplications_WithPermissionV2(t *testing.T) {
+ ensureFeaturePermissionV2Enabled(t, instancePermissionV2)
+ iamOwnerCtx := instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeIAMOwner)
+ p, projectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx)
+ _, otherProjectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx)
+
+ appName1, appName2, appName3 := gofakeit.AppName(), gofakeit.AppName(), gofakeit.AppName()
+ reqForAPIAppCreation := &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
+ }
+
+ app1, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: appName1,
+ CreationRequestType: reqForAPIAppCreation,
+ })
+ require.Nil(t, appAPIConfigChangeErr)
+
+ app2, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: appName2,
+ CreationRequestType: reqForAPIAppCreation,
+ })
+ require.Nil(t, appAPIConfigChangeErr)
+
+ app3, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: appName3,
+ CreationRequestType: reqForAPIAppCreation,
+ })
+ require.Nil(t, appAPIConfigChangeErr)
+
+ t.Parallel()
+
+ tt := []struct {
+ testName string
+ inputRequest *app.ListApplicationsRequest
+ inputCtx context.Context
+
+ expectedCode codes.Code
+ expectedAppIDs []string
+ }{
+ {
+ testName: "when user has no read permission should return empty set",
+ inputCtx: instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeNoPermission),
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ },
+
+ expectedAppIDs: []string{},
+ },
+ {
+ testName: "when projectOwner should return full app list",
+ inputCtx: projectOwnerCtx,
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ },
+
+ expectedCode: codes.OK,
+ expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()},
+ },
+ {
+ testName: "when orgOwner should return full app list",
+ inputCtx: instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ },
+
+ expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()},
+ },
+ {
+ testName: "when iamOwner user should return full app list",
+ inputCtx: iamOwnerCtx,
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ },
+
+ expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()},
+ },
+ {
+ testName: "when other projectOwner user should return empty list",
+ inputCtx: otherProjectOwnerCtx,
+ inputRequest: &app.ListApplicationsRequest{
+ ProjectId: p.GetId(),
+ },
+
+ expectedAppIDs: []string{},
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.testName, func(t *testing.T) {
+ t.Parallel()
+ retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 5*time.Second)
+ require.EventuallyWithT(t, func(ttt *assert.CollectT) {
+ // When
+ res, err := instancePermissionV2.Client.AppV2Beta.ListApplications(tc.inputCtx, tc.inputRequest)
+
+ // Then
+ require.Equal(ttt, tc.expectedCode, status.Code(err))
+
+ if err == nil {
+ require.Len(ttt, res.GetApplications(), len(tc.expectedAppIDs))
+
+ resAppIDs := []string{}
+ for _, a := range res.GetApplications() {
+ resAppIDs = append(resAppIDs, a.GetId())
+ }
+
+ assert.ElementsMatch(ttt, tc.expectedAppIDs, resAppIDs)
+ }
+ }, retryDuration, tick)
+ })
+ }
+}
diff --git a/internal/api/grpc/app/v2beta/integration_test/server_test.go b/internal/api/grpc/app/v2beta/integration_test/server_test.go
new file mode 100644
index 0000000000..6618ab0616
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/integration_test/server_test.go
@@ -0,0 +1,205 @@
+//go:build integration
+
+package instance_test
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/brianvoe/gofakeit/v6"
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/zitadel/zitadel/internal/integration"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+ "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
+ project_v2beta "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
+)
+
+var (
+ NoPermissionCtx context.Context
+ LoginUserCtx context.Context
+ OrgOwnerCtx context.Context
+ IAMOwnerCtx context.Context
+
+ instance *integration.Instance
+ instancePermissionV2 *integration.Instance
+
+ baseURI = "http://example.com"
+)
+
+func TestMain(m *testing.M) {
+ os.Exit(func() int {
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
+ defer cancel()
+
+ instance = integration.NewInstance(ctx)
+ instancePermissionV2 = integration.NewInstance(ctx)
+
+ IAMOwnerCtx = instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
+
+ LoginUserCtx = instance.WithAuthorization(ctx, integration.UserTypeLogin)
+ OrgOwnerCtx = instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
+ NoPermissionCtx = instance.WithAuthorization(ctx, integration.UserTypeNoPermission)
+
+ return m.Run()
+ }())
+}
+
+func getProjectAndProjectContext(t *testing.T, inst *integration.Instance, ctx context.Context) (*project_v2beta.CreateProjectResponse, context.Context) {
+ project := inst.CreateProject(ctx, t, inst.DefaultOrg.GetId(), gofakeit.Name(), false, false)
+ userResp := inst.CreateMachineUser(ctx)
+ patResp := inst.CreatePersonalAccessToken(ctx, userResp.GetUserId())
+ inst.CreateProjectMembership(t, ctx, project.GetId(), userResp.GetUserId())
+ projectOwnerCtx := integration.WithAuthorizationToken(context.Background(), patResp.Token)
+
+ return project, projectOwnerCtx
+}
+
+func samlMetadataGen(entityID string) []byte {
+ str := fmt.Sprintf(`
+
+
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
+
+
+
+
+`,
+ entityID)
+
+ return []byte(str)
+}
+
+func createSAMLAppWithName(t *testing.T, baseURI, projectID string) ([]byte, *app.CreateApplicationResponse, string) {
+ samlMetas := samlMetadataGen(gofakeit.URL())
+ appName := gofakeit.AppName()
+
+ appForSAMLConfigChange, appSAMLConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: projectID,
+ Name: appName,
+ CreationRequestType: &app.CreateApplicationRequest_SamlRequest{
+ SamlRequest: &app.CreateSAMLApplicationRequest{
+ Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
+ MetadataXml: samlMetas,
+ },
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ })
+ require.Nil(t, appSAMLConfigChangeErr)
+
+ return samlMetas, appForSAMLConfigChange, appName
+}
+
+func createSAMLApp(t *testing.T, baseURI, projectID string) ([]byte, *app.CreateApplicationResponse) {
+ metas, app, _ := createSAMLAppWithName(t, baseURI, projectID)
+ return metas, app
+}
+
+func createOIDCAppWithName(t *testing.T, baseURI, projectID string) (*app.CreateApplicationResponse, string) {
+ appName := gofakeit.AppName()
+
+ appForOIDCConfigChange, appOIDCConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: projectID,
+ Name: appName,
+ CreationRequestType: &app.CreateApplicationRequest_OidcRequest{
+ OidcRequest: &app.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"http://example.com"},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
+ PostLogoutRedirectUris: []string{"http://example.com/home"},
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ BackChannelLogoutUri: "http://example.com/logout",
+ LoginVersion: &app.LoginVersion{
+ Version: &app.LoginVersion_LoginV2{
+ LoginV2: &app.LoginV2{
+ BaseUri: &baseURI,
+ },
+ },
+ },
+ },
+ },
+ })
+ require.Nil(t, appOIDCConfigChangeErr)
+
+ return appForOIDCConfigChange, appName
+}
+
+func createOIDCApp(t *testing.T, baseURI, projctID string) *app.CreateApplicationResponse {
+ app, _ := createOIDCAppWithName(t, baseURI, projctID)
+
+ return app
+}
+
+func createAPIAppWithName(t *testing.T, projectID string) (*app.CreateApplicationResponse, string) {
+ appName := gofakeit.AppName()
+
+ reqForAPIAppCreation := &app.CreateApplicationRequest_ApiRequest{
+ ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
+ }
+
+ appForAPIConfigChange, appAPIConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
+ ProjectId: projectID,
+ Name: appName,
+ CreationRequestType: reqForAPIAppCreation,
+ })
+ require.Nil(t, appAPIConfigChangeErr)
+
+ return appForAPIConfigChange, appName
+}
+
+func createAPIApp(t *testing.T, projectID string) *app.CreateApplicationResponse {
+ res, _ := createAPIAppWithName(t, projectID)
+ return res
+}
+
+func deactivateApp(t *testing.T, appToDeactivate *app.CreateApplicationResponse, projectID string) {
+ _, appDeactivateErr := instance.Client.AppV2Beta.DeactivateApplication(IAMOwnerCtx, &app.DeactivateApplicationRequest{
+ ProjectId: projectID,
+ Id: appToDeactivate.GetAppId(),
+ })
+ require.Nil(t, appDeactivateErr)
+}
+
+func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) {
+ ctx := instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner)
+ f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{
+ Inheritance: true,
+ })
+ require.NoError(t, err)
+
+ if f.PermissionCheckV2.GetEnabled() {
+ return
+ }
+
+ _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{
+ PermissionCheckV2: gu.Ptr(true),
+ })
+ require.NoError(t, err)
+
+ retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute)
+ require.EventuallyWithT(t, func(tt *assert.CollectT) {
+ f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{Inheritance: true})
+ require.NoError(tt, err)
+ assert.True(tt, f.PermissionCheckV2.GetEnabled())
+ }, retryDuration, tick, "timed out waiting for ensuring instance feature")
+}
diff --git a/internal/api/grpc/app/v2beta/query.go b/internal/api/grpc/app/v2beta/query.go
new file mode 100644
index 0000000000..add8af83e6
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/query.go
@@ -0,0 +1,37 @@
+package app
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta/convert"
+ filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+)
+
+func (s *Server) GetApplication(ctx context.Context, req *app.GetApplicationRequest) (*app.GetApplicationResponse, error) {
+ res, err := s.query.AppByIDWithPermission(ctx, req.GetId(), false, s.checkPermission)
+ if err != nil {
+ return nil, err
+ }
+
+ return &app.GetApplicationResponse{
+ App: convert.AppToPb(res),
+ }, nil
+}
+
+func (s *Server) ListApplications(ctx context.Context, req *app.ListApplicationsRequest) (*app.ListApplicationsResponse, error) {
+ queries, err := convert.ListApplicationsRequestToModel(s.systemDefaults, req)
+ if err != nil {
+ return nil, err
+ }
+
+ res, err := s.query.SearchApps(ctx, queries, s.checkPermission)
+ if err != nil {
+ return nil, err
+ }
+
+ return &app.ListApplicationsResponse{
+ Applications: convert.AppsToPb(res.Apps),
+ Pagination: filter.QueryToPaginationPb(queries.SearchRequest, res.SearchResponse),
+ }, nil
+}
diff --git a/internal/api/grpc/app/v2beta/server.go b/internal/api/grpc/app/v2beta/server.go
new file mode 100644
index 0000000000..8343cbe404
--- /dev/null
+++ b/internal/api/grpc/app/v2beta/server.go
@@ -0,0 +1,57 @@
+package app
+
+import (
+ "google.golang.org/grpc"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/api/grpc/server"
+ "github.com/zitadel/zitadel/internal/command"
+ "github.com/zitadel/zitadel/internal/config/systemdefaults"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/query"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
+)
+
+var _ app.AppServiceServer = (*Server)(nil)
+
+type Server struct {
+ app.UnimplementedAppServiceServer
+ command *command.Commands
+ query *query.Queries
+ systemDefaults systemdefaults.SystemDefaults
+ checkPermission domain.PermissionCheck
+}
+
+type Config struct{}
+
+func CreateServer(
+ command *command.Commands,
+ query *query.Queries,
+ checkPermission domain.PermissionCheck,
+) *Server {
+ return &Server{
+ command: command,
+ query: query,
+ checkPermission: checkPermission,
+ }
+}
+
+func (s *Server) RegisterServer(grpcServer *grpc.Server) {
+ app.RegisterAppServiceServer(grpcServer, s)
+}
+
+func (s *Server) AppName() string {
+ return app.AppService_ServiceDesc.ServiceName
+}
+
+func (s *Server) MethodPrefix() string {
+ return app.AppService_ServiceDesc.ServiceName
+}
+
+func (s *Server) AuthMethods() authz.MethodMapping {
+ return app.AppService_AuthMethods
+}
+
+func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
+ return app.RegisterAppServiceHandler
+}
diff --git a/internal/api/grpc/filter/v2/converter.go b/internal/api/grpc/filter/v2/converter.go
index 7a7d7cd8d7..f797ad4bba 100644
--- a/internal/api/grpc/filter/v2/converter.go
+++ b/internal/api/grpc/filter/v2/converter.go
@@ -48,3 +48,26 @@ func QueryToPaginationPb(request query.SearchRequest, response query.SearchRespo
TotalResult: response.Count,
}
}
+
+func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison {
+ switch method {
+ case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS:
+ return query.TextEquals
+ case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE:
+ return query.TextEqualsIgnoreCase
+ case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH:
+ return query.TextStartsWith
+ case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE:
+ return query.TextStartsWithIgnoreCase
+ case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS:
+ return query.TextContains
+ case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE:
+ return query.TextContainsIgnoreCase
+ case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH:
+ return query.TextEndsWith
+ case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE:
+ return query.TextEndsWithIgnoreCase
+ default:
+ return -1
+ }
+}
diff --git a/internal/api/grpc/management/project_application.go b/internal/api/grpc/management/project_application.go
index ab49905409..a5526d3cb7 100644
--- a/internal/api/grpc/management/project_application.go
+++ b/internal/api/grpc/management/project_application.go
@@ -29,7 +29,7 @@ func (s *Server) ListApps(ctx context.Context, req *mgmt_pb.ListAppsRequest) (*m
if err != nil {
return nil, err
}
- apps, err := s.query.SearchApps(ctx, queries, false)
+ apps, err := s.query.SearchApps(ctx, queries, nil)
if err != nil {
return nil, err
}
@@ -125,7 +125,7 @@ func (s *Server) AddAPIApp(ctx context.Context, req *mgmt_pb.AddAPIAppRequest) (
}
func (s *Server) UpdateApp(ctx context.Context, req *mgmt_pb.UpdateAppRequest) (*mgmt_pb.UpdateAppResponse, error) {
- details, err := s.command.ChangeApplication(ctx, req.ProjectId, UpdateAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
+ details, err := s.command.UpdateApplicationName(ctx, req.ProjectId, UpdateAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
@@ -139,7 +139,7 @@ func (s *Server) UpdateOIDCAppConfig(ctx context.Context, req *mgmt_pb.UpdateOID
if err != nil {
return nil, err
}
- config, err := s.command.ChangeOIDCApplication(ctx, oidcApp, authz.GetCtxData(ctx).OrgID)
+ config, err := s.command.UpdateOIDCApplication(ctx, oidcApp, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
@@ -157,7 +157,7 @@ func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAM
if err != nil {
return nil, err
}
- config, err := s.command.ChangeSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID)
+ config, err := s.command.UpdateSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
@@ -171,7 +171,7 @@ func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAM
}
func (s *Server) UpdateAPIAppConfig(ctx context.Context, req *mgmt_pb.UpdateAPIAppConfigRequest) (*mgmt_pb.UpdateAPIAppConfigResponse, error) {
- config, err := s.command.ChangeAPIApplication(ctx, UpdateAPIAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
+ config, err := s.command.UpdateAPIApplication(ctx, UpdateAPIAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
diff --git a/internal/api/grpc/management/project_application_converter.go b/internal/api/grpc/management/project_application_converter.go
index 13a0048a5b..186cedc933 100644
--- a/internal/api/grpc/management/project_application_converter.go
+++ b/internal/api/grpc/management/project_application_converter.go
@@ -4,6 +4,8 @@ import (
"context"
"time"
+ "github.com/muhlemmer/gu"
+
"github.com/zitadel/zitadel/internal/api/authz"
authn_grpc "github.com/zitadel/zitadel/internal/api/grpc/authn"
"github.com/zitadel/zitadel/internal/api/grpc/object"
@@ -46,24 +48,24 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) (*domain.OIDCApp,
AggregateID: req.ProjectId,
},
AppName: req.Name,
- OIDCVersion: app_grpc.OIDCVersionToDomain(req.Version),
+ OIDCVersion: gu.Ptr(app_grpc.OIDCVersionToDomain(req.Version)),
RedirectUris: req.RedirectUris,
ResponseTypes: app_grpc.OIDCResponseTypesToDomain(req.ResponseTypes),
GrantTypes: app_grpc.OIDCGrantTypesToDomain(req.GrantTypes),
- ApplicationType: app_grpc.OIDCApplicationTypeToDomain(req.AppType),
- AuthMethodType: app_grpc.OIDCAuthMethodTypeToDomain(req.AuthMethodType),
+ ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(req.AppType)),
+ AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(req.AuthMethodType)),
PostLogoutRedirectUris: req.PostLogoutRedirectUris,
- DevMode: req.DevMode,
- AccessTokenType: app_grpc.OIDCTokenTypeToDomain(req.AccessTokenType),
- AccessTokenRoleAssertion: req.AccessTokenRoleAssertion,
- IDTokenRoleAssertion: req.IdTokenRoleAssertion,
- IDTokenUserinfoAssertion: req.IdTokenUserinfoAssertion,
- ClockSkew: req.ClockSkew.AsDuration(),
+ DevMode: gu.Ptr(req.GetDevMode()),
+ AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(req.AccessTokenType)),
+ AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()),
+ IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()),
+ IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()),
+ ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()),
AdditionalOrigins: req.AdditionalOrigins,
- SkipNativeAppSuccessPage: req.SkipNativeAppSuccessPage,
- BackChannelLogoutURI: req.GetBackChannelLogoutUri(),
- LoginVersion: loginVersion,
- LoginBaseURI: loginBaseURI,
+ SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()),
+ BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()),
+ LoginVersion: gu.Ptr(loginVersion),
+ LoginBaseURI: gu.Ptr(loginBaseURI),
}, nil
}
@@ -78,9 +80,9 @@ func AddSAMLAppRequestToDomain(req *mgmt_pb.AddSAMLAppRequest) (*domain.SAMLApp,
},
AppName: req.Name,
Metadata: req.GetMetadataXml(),
- MetadataURL: req.GetMetadataUrl(),
- LoginVersion: loginVersion,
- LoginBaseURI: loginBaseURI,
+ MetadataURL: gu.Ptr(req.GetMetadataUrl()),
+ LoginVersion: gu.Ptr(loginVersion),
+ LoginBaseURI: gu.Ptr(loginBaseURI),
}, nil
}
@@ -114,20 +116,20 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest)
RedirectUris: app.RedirectUris,
ResponseTypes: app_grpc.OIDCResponseTypesToDomain(app.ResponseTypes),
GrantTypes: app_grpc.OIDCGrantTypesToDomain(app.GrantTypes),
- ApplicationType: app_grpc.OIDCApplicationTypeToDomain(app.AppType),
- AuthMethodType: app_grpc.OIDCAuthMethodTypeToDomain(app.AuthMethodType),
+ ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(app.AppType)),
+ AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(app.AuthMethodType)),
PostLogoutRedirectUris: app.PostLogoutRedirectUris,
- DevMode: app.DevMode,
- AccessTokenType: app_grpc.OIDCTokenTypeToDomain(app.AccessTokenType),
- AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
- IDTokenRoleAssertion: app.IdTokenRoleAssertion,
- IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion,
- ClockSkew: app.ClockSkew.AsDuration(),
+ DevMode: gu.Ptr(app.GetDevMode()),
+ AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(app.AccessTokenType)),
+ AccessTokenRoleAssertion: gu.Ptr(app.GetAccessTokenRoleAssertion()),
+ IDTokenRoleAssertion: gu.Ptr(app.GetIdTokenRoleAssertion()),
+ IDTokenUserinfoAssertion: gu.Ptr(app.GetIdTokenUserinfoAssertion()),
+ ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()),
AdditionalOrigins: app.AdditionalOrigins,
- SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage,
- BackChannelLogoutURI: app.BackChannelLogoutUri,
- LoginVersion: loginVersion,
- LoginBaseURI: loginBaseURI,
+ SkipNativeAppSuccessPage: gu.Ptr(app.GetSkipNativeAppSuccessPage()),
+ BackChannelLogoutURI: gu.Ptr(app.GetBackChannelLogoutUri()),
+ LoginVersion: gu.Ptr(loginVersion),
+ LoginBaseURI: gu.Ptr(loginBaseURI),
}, nil
}
@@ -142,9 +144,9 @@ func UpdateSAMLAppConfigRequestToDomain(app *mgmt_pb.UpdateSAMLAppConfigRequest)
},
AppID: app.AppId,
Metadata: app.GetMetadataXml(),
- MetadataURL: app.GetMetadataUrl(),
- LoginVersion: loginVersion,
- LoginBaseURI: loginBaseURI,
+ MetadataURL: gu.Ptr(app.GetMetadataUrl()),
+ LoginVersion: gu.Ptr(loginVersion),
+ LoginBaseURI: gu.Ptr(loginBaseURI),
}, nil
}
diff --git a/internal/authz/repository/eventsourcing/view/application.go b/internal/authz/repository/eventsourcing/view/application.go
index 8db8ec8e39..7fa920bcfe 100644
--- a/internal/authz/repository/eventsourcing/view/application.go
+++ b/internal/authz/repository/eventsourcing/view/application.go
@@ -32,7 +32,7 @@ func (v *View) ApplicationByProjecIDAndAppName(ctx context.Context, projectID, a
},
}
- apps, err := v.Query.SearchApps(ctx, queries, false)
+ apps, err := v.Query.SearchApps(ctx, queries, nil)
if err != nil {
return nil, err
}
diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go
index 6bfeaae219..3f978b6618 100644
--- a/internal/command/permission_checks.go
+++ b/internal/command/permission_checks.go
@@ -85,3 +85,11 @@ func (c *Commands) checkPermissionDeleteProjectGrant(ctx context.Context, resour
}
return nil
}
+
+func (c *Commands) checkPermissionUpdateApplication(ctx context.Context, resourceOwner, appID string) error {
+ return c.newPermissionCheck(ctx, domain.PermissionProjectAppWrite, project.AggregateType)(resourceOwner, appID)
+}
+
+func (c *Commands) checkPermissionDeleteApp(ctx context.Context, resourceOwner, appID string) error {
+ return c.newPermissionCheck(ctx, domain.PermissionProjectAppDelete, project.AggregateType)(resourceOwner, appID)
+}
diff --git a/internal/command/project_application.go b/internal/command/project_application.go
index 0ccf5dc852..465b12e1e1 100644
--- a/internal/command/project_application.go
+++ b/internal/command/project_application.go
@@ -15,7 +15,7 @@ type AddApp struct {
Name string
}
-func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appChange domain.Application, resourceOwner string) (*domain.ObjectDetails, error) {
+func (c *Commands) UpdateApplicationName(ctx context.Context, projectID string, appChange domain.Application, resourceOwner string) (*domain.ObjectDetails, error) {
if projectID == "" || appChange.GetAppID() == "" || appChange.GetApplicationName() == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.App.Invalid")
}
@@ -30,6 +30,13 @@ func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appC
if existingApp.Name == appChange.GetApplicationName() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2m8vx", "Errors.NoChangesFound")
}
+ if err := c.eventstore.FilterToQueryReducer(ctx, existingApp); err != nil {
+ return nil, err
+ }
+ if err := c.checkPermissionUpdateApplication(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil {
+ return nil, err
+ }
+
projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel)
pushedEvents, err := c.eventstore.Push(
ctx,
@@ -59,6 +66,13 @@ func (c *Commands) DeactivateApplication(ctx context.Context, projectID, appID,
if existingApp.State != domain.AppStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-dsh35", "Errors.Project.App.NotActive")
}
+ if err := c.eventstore.FilterToQueryReducer(ctx, existingApp); err != nil {
+ return nil, err
+ }
+ if err := c.checkPermissionUpdateApplication(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil {
+ return nil, err
+ }
+
projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationDeactivatedEvent(ctx, projectAgg, appID))
if err != nil {
@@ -86,6 +100,11 @@ func (c *Commands) ReactivateApplication(ctx context.Context, projectID, appID,
if existingApp.State != domain.AppStateInactive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-1n8cM", "Errors.Project.App.NotInactive")
}
+
+ if err := c.checkPermissionUpdateApplication(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil {
+ return nil, err
+ }
+
projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationReactivatedEvent(ctx, projectAgg, appID))
@@ -111,6 +130,13 @@ func (c *Commands) RemoveApplication(ctx context.Context, projectID, appID, reso
if existingApp.State == domain.AppStateUnspecified || existingApp.State == domain.AppStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-0po9s", "Errors.Project.App.NotExisting")
}
+ if err := c.eventstore.FilterToQueryReducer(ctx, existingApp); err != nil {
+ return nil, err
+ }
+ if err := c.checkPermissionDeleteApp(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil {
+ return nil, err
+ }
+
projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel)
entityID := ""
diff --git a/internal/command/project_application_api.go b/internal/command/project_application_api.go
index 2832dcf873..82e7d0bde8 100644
--- a/internal/command/project_application_api.go
+++ b/internal/command/project_application_api.go
@@ -90,16 +90,24 @@ func (c *Commands) AddAPIApplication(ctx context.Context, apiApp *domain.APIApp,
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Project.App.Invalid")
}
- if _, err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil {
+ projectResOwner, err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner)
+ if err != nil {
return nil, err
}
+ if resourceOwner == "" {
+ resourceOwner = projectResOwner
+ }
+
if !apiApp.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Project.App.Invalid")
}
- appID, err := c.idGenerator.Next()
- if err != nil {
- return nil, err
+ appID := apiApp.AppID
+ if appID == "" {
+ appID, err = c.idGenerator.Next()
+ if err != nil {
+ return nil, err
+ }
}
return c.addAPIApplicationWithID(ctx, apiApp, resourceOwner, appID)
@@ -112,6 +120,13 @@ func (c *Commands) addAPIApplicationWithID(ctx context.Context, apiApp *domain.A
apiApp.AppID = appID
addedApplication := NewAPIApplicationWriteModel(apiApp.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)
events := []eventstore.Command{
@@ -150,7 +165,7 @@ func (c *Commands) addAPIApplicationWithID(ctx context.Context, apiApp *domain.A
return result, nil
}
-func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIApp, resourceOwner string) (*domain.APIApp, error) {
+func (c *Commands) UpdateAPIApplication(ctx context.Context, apiApp *domain.APIApp, resourceOwner string) (*domain.APIApp, error) {
if apiApp.AppID == "" || apiApp.AggregateID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-1m900", "Errors.Project.App.APIConfigInvalid")
}
@@ -165,6 +180,13 @@ func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIA
if !existingAPI.IsAPI() {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Gnwt3", "Errors.Project.App.IsNotAPI")
}
+ if err := c.eventstore.FilterToQueryReducer(ctx, existingAPI); err != nil {
+ return nil, err
+ }
+ if err := c.checkPermissionUpdateApplication(ctx, existingAPI.ResourceOwner, existingAPI.AggregateID); err != nil {
+ return nil, err
+ }
+
projectAgg := ProjectAggregateFromWriteModel(&existingAPI.WriteModel)
changedEvent, hasChanged, err := existingAPI.NewChangedEvent(
ctx,
@@ -205,6 +227,11 @@ func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, ap
if !existingAPI.IsAPI() {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-aeH4", "Errors.Project.App.IsNotAPI")
}
+
+ if err := c.checkPermissionUpdateApplication(ctx, existingAPI.ResourceOwner, existingAPI.AggregateID); err != nil {
+ return nil, err
+ }
+
encodedHash, plain, err := c.newHashedSecret(ctx, c.eventstore.Filter) //nolint:staticcheck
if err != nil {
return nil, err
diff --git a/internal/command/project_application_api_test.go b/internal/command/project_application_api_test.go
index a6d4349254..53448e1c5e 100644
--- a/internal/command/project_application_api_test.go
+++ b/internal/command/project_application_api_test.go
@@ -142,6 +142,7 @@ func TestAddAPIConfig(t *testing.T) {
}
func TestCommandSide_AddAPIApplication(t *testing.T) {
+ t.Parallel()
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
@@ -238,6 +239,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) {
domain.PrivateLabelingSettingUnspecified),
),
),
+ expectFilter(),
expectPush(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -292,6 +294,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) {
domain.PrivateLabelingSettingUnspecified),
),
),
+ expectFilter(),
expectPush(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -346,6 +349,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) {
domain.PrivateLabelingSettingUnspecified),
),
),
+ expectFilter(),
expectPush(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -390,6 +394,8 @@ func TestCommandSide_AddAPIApplication(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
r := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
@@ -397,6 +403,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) {
defaultSecretGenerators: &SecretGenerators{
ClientSecret: emptyConfig,
},
+ checkPermission: newMockPermissionCheckAllowed(),
}
got, err := r.AddAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner)
if tt.res.err == nil {
@@ -413,6 +420,8 @@ func TestCommandSide_AddAPIApplication(t *testing.T) {
}
func TestCommandSide_ChangeAPIApplication(t *testing.T) {
+ t.Parallel()
+
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
@@ -516,6 +525,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) {
domain.APIAuthMethodTypePrivateKeyJWT),
),
),
+ expectFilter(),
),
},
args: args{
@@ -555,6 +565,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) {
domain.APIAuthMethodTypeBasic),
),
),
+ expectFilter(),
expectPush(
newAPIAppChangedEvent(context.Background(),
"app1",
@@ -593,14 +604,17 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
r := &Commands{
eventstore: tt.fields.eventstore(t),
newHashedSecret: mockHashedSecret("secret"),
defaultSecretGenerators: &SecretGenerators{
ClientSecret: emptyConfig,
},
+ checkPermission: newMockPermissionCheckAllowed(),
}
- got, err := r.ChangeAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner)
+ got, err := r.UpdateAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -615,6 +629,8 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) {
}
func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) {
+ t.Parallel()
+
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
}
@@ -734,12 +750,15 @@ func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
r := &Commands{
eventstore: tt.fields.eventstore(t),
newHashedSecret: mockHashedSecret("secret"),
defaultSecretGenerators: &SecretGenerators{
ClientSecret: emptyConfig,
},
+ checkPermission: newMockPermissionCheckAllowed(),
}
got, err := r.ChangeAPIApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner)
if tt.res.err == nil {
diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go
index 77ef7ff0c7..7f33b6a3cf 100644
--- a/internal/command/project_application_oidc.go
+++ b/internal/command/project_application_oidc.go
@@ -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
diff --git a/internal/command/project_application_oidc_model.go b/internal/command/project_application_oidc_model.go
index 603ebdcda2..375bb26f5e 100644
--- a/internal/command/project_application_oidc_model.go
+++ b/internal/command/project_application_oidc_model.go
@@ -258,77 +258,77 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent(
postLogoutRedirectURIs []string,
responseTypes []domain.OIDCResponseType,
grantTypes []domain.OIDCGrantType,
- appType domain.OIDCApplicationType,
- authMethodType domain.OIDCAuthMethodType,
- oidcVersion domain.OIDCVersion,
- accessTokenType domain.OIDCTokenType,
+ appType *domain.OIDCApplicationType,
+ authMethodType *domain.OIDCAuthMethodType,
+ oidcVersion *domain.OIDCVersion,
+ accessTokenType *domain.OIDCTokenType,
devMode,
accessTokenRoleAssertion,
idTokenRoleAssertion,
- idTokenUserinfoAssertion bool,
- clockSkew time.Duration,
+ idTokenUserinfoAssertion *bool,
+ clockSkew *time.Duration,
additionalOrigins []string,
- skipNativeAppSuccessPage bool,
- backChannelLogoutURI string,
- loginVersion domain.LoginVersion,
- loginBaseURI string,
+ skipNativeAppSuccessPage *bool,
+ backChannelLogoutURI *string,
+ loginVersion *domain.LoginVersion,
+ loginBaseURI *string,
) (*project.OIDCConfigChangedEvent, bool, error) {
changes := make([]project.OIDCConfigChanges, 0)
var err error
- if !slices.Equal(wm.RedirectUris, redirectURIS) {
+ if redirectURIS != nil && !slices.Equal(wm.RedirectUris, redirectURIS) {
changes = append(changes, project.ChangeRedirectURIs(redirectURIS))
}
- if !slices.Equal(wm.ResponseTypes, responseTypes) {
+ if responseTypes != nil && !slices.Equal(wm.ResponseTypes, responseTypes) {
changes = append(changes, project.ChangeResponseTypes(responseTypes))
}
- if !slices.Equal(wm.GrantTypes, grantTypes) {
+ if grantTypes != nil && !slices.Equal(wm.GrantTypes, grantTypes) {
changes = append(changes, project.ChangeGrantTypes(grantTypes))
}
- if wm.ApplicationType != appType {
- changes = append(changes, project.ChangeApplicationType(appType))
+ if appType != nil && wm.ApplicationType != *appType {
+ changes = append(changes, project.ChangeApplicationType(*appType))
}
- if wm.AuthMethodType != authMethodType {
- changes = append(changes, project.ChangeAuthMethodType(authMethodType))
+ if authMethodType != nil && wm.AuthMethodType != *authMethodType {
+ changes = append(changes, project.ChangeAuthMethodType(*authMethodType))
}
- if !slices.Equal(wm.PostLogoutRedirectUris, postLogoutRedirectURIs) {
+ if postLogoutRedirectURIs != nil && !slices.Equal(wm.PostLogoutRedirectUris, postLogoutRedirectURIs) {
changes = append(changes, project.ChangePostLogoutRedirectURIs(postLogoutRedirectURIs))
}
- if wm.OIDCVersion != oidcVersion {
- changes = append(changes, project.ChangeVersion(oidcVersion))
+ if oidcVersion != nil && wm.OIDCVersion != *oidcVersion {
+ changes = append(changes, project.ChangeVersion(*oidcVersion))
}
- if wm.DevMode != devMode {
- changes = append(changes, project.ChangeDevMode(devMode))
+ if devMode != nil && wm.DevMode != *devMode {
+ changes = append(changes, project.ChangeDevMode(*devMode))
}
- if wm.AccessTokenType != accessTokenType {
- changes = append(changes, project.ChangeAccessTokenType(accessTokenType))
+ if accessTokenType != nil && wm.AccessTokenType != *accessTokenType {
+ changes = append(changes, project.ChangeAccessTokenType(*accessTokenType))
}
- if wm.AccessTokenRoleAssertion != accessTokenRoleAssertion {
- changes = append(changes, project.ChangeAccessTokenRoleAssertion(accessTokenRoleAssertion))
+ if accessTokenRoleAssertion != nil && wm.AccessTokenRoleAssertion != *accessTokenRoleAssertion {
+ changes = append(changes, project.ChangeAccessTokenRoleAssertion(*accessTokenRoleAssertion))
}
- if wm.IDTokenRoleAssertion != idTokenRoleAssertion {
- changes = append(changes, project.ChangeIDTokenRoleAssertion(idTokenRoleAssertion))
+ if idTokenRoleAssertion != nil && wm.IDTokenRoleAssertion != *idTokenRoleAssertion {
+ changes = append(changes, project.ChangeIDTokenRoleAssertion(*idTokenRoleAssertion))
}
- if wm.IDTokenUserinfoAssertion != idTokenUserinfoAssertion {
- changes = append(changes, project.ChangeIDTokenUserinfoAssertion(idTokenUserinfoAssertion))
+ if idTokenUserinfoAssertion != nil && wm.IDTokenUserinfoAssertion != *idTokenUserinfoAssertion {
+ changes = append(changes, project.ChangeIDTokenUserinfoAssertion(*idTokenUserinfoAssertion))
}
- if wm.ClockSkew != clockSkew {
- changes = append(changes, project.ChangeClockSkew(clockSkew))
+ if clockSkew != nil && wm.ClockSkew != *clockSkew {
+ changes = append(changes, project.ChangeClockSkew(*clockSkew))
}
- if !slices.Equal(wm.AdditionalOrigins, additionalOrigins) {
+ if additionalOrigins != nil && !slices.Equal(wm.AdditionalOrigins, additionalOrigins) {
changes = append(changes, project.ChangeAdditionalOrigins(additionalOrigins))
}
- if wm.SkipNativeAppSuccessPage != skipNativeAppSuccessPage {
- changes = append(changes, project.ChangeSkipNativeAppSuccessPage(skipNativeAppSuccessPage))
+ if skipNativeAppSuccessPage != nil && wm.SkipNativeAppSuccessPage != *skipNativeAppSuccessPage {
+ changes = append(changes, project.ChangeSkipNativeAppSuccessPage(*skipNativeAppSuccessPage))
}
- if wm.BackChannelLogoutURI != backChannelLogoutURI {
- changes = append(changes, project.ChangeBackChannelLogoutURI(backChannelLogoutURI))
+ if backChannelLogoutURI != nil && wm.BackChannelLogoutURI != *backChannelLogoutURI {
+ changes = append(changes, project.ChangeBackChannelLogoutURI(*backChannelLogoutURI))
}
- if wm.LoginVersion != loginVersion {
- changes = append(changes, project.ChangeOIDCLoginVersion(loginVersion))
+ if loginVersion != nil && wm.LoginVersion != *loginVersion {
+ changes = append(changes, project.ChangeOIDCLoginVersion(*loginVersion))
}
- if wm.LoginBaseURI != loginBaseURI {
- changes = append(changes, project.ChangeOIDCLoginBaseURI(loginBaseURI))
+ if loginBaseURI != nil && wm.LoginBaseURI != *loginBaseURI {
+ changes = append(changes, project.ChangeOIDCLoginBaseURI(*loginBaseURI))
}
if len(changes) == 0 {
diff --git a/internal/command/project_application_oidc_test.go b/internal/command/project_application_oidc_test.go
index d0383b1b29..d728ffca45 100644
--- a/internal/command/project_application_oidc_test.go
+++ b/internal/command/project_application_oidc_test.go
@@ -5,6 +5,7 @@ import (
"testing"
"time"
+ "github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/api/authz"
@@ -401,6 +402,8 @@ func TestAddOIDCApp(t *testing.T) {
}
func TestCommandSide_AddOIDCApplication(t *testing.T) {
+ t.Parallel()
+
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
@@ -497,6 +500,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
domain.PrivateLabelingSettingUnspecified),
),
),
+ expectFilter(),
expectPush(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -538,24 +542,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
AggregateID: "project1",
},
AppName: "app",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
- OIDCVersion: domain.OIDCVersionV1,
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost),
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
RedirectUris: []string{" https://test.ch "},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
- ApplicationType: domain.OIDCApplicationTypeWeb,
+ ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
PostLogoutRedirectUris: []string{" https://test.ch/logout "},
- DevMode: true,
- AccessTokenType: domain.OIDCTokenTypeBearer,
- AccessTokenRoleAssertion: true,
- IDTokenRoleAssertion: true,
- IDTokenUserinfoAssertion: true,
- ClockSkew: time.Second * 1,
+ DevMode: gu.Ptr(true),
+ AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IDTokenRoleAssertion: gu.Ptr(true),
+ IDTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: gu.Ptr(time.Second * 1),
AdditionalOrigins: []string{" https://sub.test.ch "},
- SkipNativeAppSuccessPage: true,
- BackChannelLogoutURI: " https://test.ch/backchannel ",
- LoginVersion: domain.LoginVersion2,
- LoginBaseURI: " https://login.test.ch ",
+ SkipNativeAppSuccessPage: gu.Ptr(true),
+ BackChannelLogoutURI: gu.Ptr(" https://test.ch/backchannel "),
+ LoginVersion: gu.Ptr(domain.LoginVersion2),
+ LoginBaseURI: gu.Ptr(" https://login.test.ch "),
},
resourceOwner: "org1",
},
@@ -569,24 +573,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
AppName: "app",
ClientID: "client1",
ClientSecretString: "secret",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
- OIDCVersion: domain.OIDCVersionV1,
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost),
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
RedirectUris: []string{"https://test.ch"},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
- ApplicationType: domain.OIDCApplicationTypeWeb,
+ ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
PostLogoutRedirectUris: []string{"https://test.ch/logout"},
- DevMode: true,
- AccessTokenType: domain.OIDCTokenTypeBearer,
- AccessTokenRoleAssertion: true,
- IDTokenRoleAssertion: true,
- IDTokenUserinfoAssertion: true,
- ClockSkew: time.Second * 1,
+ DevMode: gu.Ptr(true),
+ AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IDTokenRoleAssertion: gu.Ptr(true),
+ IDTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: gu.Ptr(time.Second * 1),
AdditionalOrigins: []string{"https://sub.test.ch"},
- SkipNativeAppSuccessPage: true,
- BackChannelLogoutURI: "https://test.ch/backchannel",
- LoginVersion: domain.LoginVersion2,
- LoginBaseURI: "https://login.test.ch",
+ SkipNativeAppSuccessPage: gu.Ptr(true),
+ BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"),
+ LoginVersion: gu.Ptr(domain.LoginVersion2),
+ LoginBaseURI: gu.Ptr("https://login.test.ch"),
State: domain.AppStateActive,
Compliance: &domain.Compliance{},
},
@@ -604,6 +608,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
domain.PrivateLabelingSettingUnspecified),
),
),
+ expectFilter(),
expectPush(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
@@ -645,24 +650,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
AggregateID: "project1",
},
AppName: "app",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
- OIDCVersion: domain.OIDCVersionV1,
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost),
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
RedirectUris: []string{"https://test.ch"},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
- ApplicationType: domain.OIDCApplicationTypeWeb,
+ ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
PostLogoutRedirectUris: []string{"https://test.ch/logout"},
- DevMode: true,
- AccessTokenType: domain.OIDCTokenTypeBearer,
- AccessTokenRoleAssertion: true,
- IDTokenRoleAssertion: true,
- IDTokenUserinfoAssertion: true,
- ClockSkew: time.Second * 1,
+ DevMode: gu.Ptr(true),
+ AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IDTokenRoleAssertion: gu.Ptr(true),
+ IDTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: gu.Ptr(time.Second * 1),
AdditionalOrigins: []string{"https://sub.test.ch"},
- SkipNativeAppSuccessPage: true,
- BackChannelLogoutURI: "https://test.ch/backchannel",
- LoginVersion: domain.LoginVersion2,
- LoginBaseURI: "https://login.test.ch",
+ SkipNativeAppSuccessPage: gu.Ptr(true),
+ BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"),
+ LoginVersion: gu.Ptr(domain.LoginVersion2),
+ LoginBaseURI: gu.Ptr("https://login.test.ch"),
},
resourceOwner: "org1",
},
@@ -676,24 +681,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
AppName: "app",
ClientID: "client1",
ClientSecretString: "secret",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
- OIDCVersion: domain.OIDCVersionV1,
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost),
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
RedirectUris: []string{"https://test.ch"},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
- ApplicationType: domain.OIDCApplicationTypeWeb,
+ ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
PostLogoutRedirectUris: []string{"https://test.ch/logout"},
- DevMode: true,
- AccessTokenType: domain.OIDCTokenTypeBearer,
- AccessTokenRoleAssertion: true,
- IDTokenRoleAssertion: true,
- IDTokenUserinfoAssertion: true,
- ClockSkew: time.Second * 1,
+ DevMode: gu.Ptr(true),
+ AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IDTokenRoleAssertion: gu.Ptr(true),
+ IDTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: gu.Ptr(time.Second * 1),
AdditionalOrigins: []string{"https://sub.test.ch"},
- SkipNativeAppSuccessPage: true,
- BackChannelLogoutURI: "https://test.ch/backchannel",
- LoginVersion: domain.LoginVersion2,
- LoginBaseURI: "https://login.test.ch",
+ SkipNativeAppSuccessPage: gu.Ptr(true),
+ BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"),
+ LoginVersion: gu.Ptr(domain.LoginVersion2),
+ LoginBaseURI: gu.Ptr("https://login.test.ch"),
State: domain.AppStateActive,
Compliance: &domain.Compliance{},
},
@@ -702,6 +707,7 @@ func TestCommandSide_AddOIDCApplication(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,
@@ -709,6 +715,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
defaultSecretGenerators: &SecretGenerators{
ClientSecret: emptyConfig,
},
+ checkPermission: newMockPermissionCheckAllowed(),
}
c.setMilestonesCompletedForTest("instanceID")
got, err := c.AddOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner)
@@ -726,6 +733,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
}
func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
+ t.Parallel()
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
}
@@ -775,7 +783,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
AggregateID: "project1",
},
AppID: "",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost),
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
},
@@ -797,7 +805,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
AggregateID: "",
},
AppID: "appid",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost),
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
},
@@ -821,7 +829,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
AggregateID: "project1",
},
AppID: "app1",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost),
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
},
@@ -870,6 +878,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
),
),
),
+ expectFilter(),
),
},
args: args{
@@ -880,24 +889,24 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
},
AppID: "app1",
AppName: "app",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
- OIDCVersion: domain.OIDCVersionV1,
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost),
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
RedirectUris: []string{"https://test.ch"},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
- ApplicationType: domain.OIDCApplicationTypeWeb,
+ ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
PostLogoutRedirectUris: []string{"https://test.ch/logout"},
- DevMode: true,
- AccessTokenType: domain.OIDCTokenTypeBearer,
- AccessTokenRoleAssertion: true,
- IDTokenRoleAssertion: true,
- IDTokenUserinfoAssertion: true,
- ClockSkew: time.Second * 1,
+ DevMode: gu.Ptr(true),
+ AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IDTokenRoleAssertion: gu.Ptr(true),
+ IDTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: gu.Ptr(time.Second * 1),
AdditionalOrigins: []string{"https://sub.test.ch"},
- SkipNativeAppSuccessPage: true,
- BackChannelLogoutURI: "https://test.ch/backchannel",
- LoginVersion: domain.LoginVersion2,
- LoginBaseURI: "https://login.test.ch",
+ SkipNativeAppSuccessPage: gu.Ptr(true),
+ BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"),
+ LoginVersion: gu.Ptr(domain.LoginVersion2),
+ LoginBaseURI: gu.Ptr("https://login.test.ch"),
},
resourceOwner: "org1",
},
@@ -944,6 +953,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
),
),
),
+ expectFilter(),
),
},
args: args{
@@ -954,24 +964,24 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
},
AppID: "app1",
AppName: "app",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
- OIDCVersion: domain.OIDCVersionV1,
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost),
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
RedirectUris: []string{"https://test.ch "},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
- ApplicationType: domain.OIDCApplicationTypeWeb,
+ ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
PostLogoutRedirectUris: []string{" https://test.ch/logout"},
- DevMode: true,
- AccessTokenType: domain.OIDCTokenTypeBearer,
- AccessTokenRoleAssertion: true,
- IDTokenRoleAssertion: true,
- IDTokenUserinfoAssertion: true,
- ClockSkew: time.Second * 1,
+ DevMode: gu.Ptr(true),
+ AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IDTokenRoleAssertion: gu.Ptr(true),
+ IDTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: gu.Ptr(time.Second * 1),
AdditionalOrigins: []string{" https://sub.test.ch "},
- SkipNativeAppSuccessPage: true,
- BackChannelLogoutURI: " https://test.ch/backchannel ",
- LoginVersion: domain.LoginVersion2,
- LoginBaseURI: " https://login.test.ch ",
+ SkipNativeAppSuccessPage: gu.Ptr(true),
+ BackChannelLogoutURI: gu.Ptr(" https://test.ch/backchannel "),
+ LoginVersion: gu.Ptr(domain.LoginVersion2),
+ LoginBaseURI: gu.Ptr(" https://login.test.ch "),
},
resourceOwner: "org1",
},
@@ -980,7 +990,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
},
},
{
- name: "change oidc app, ok",
+ name: "partial change oidc app, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
@@ -1018,6 +1028,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
),
),
),
+ expectFilter(),
expectPush(
newOIDCAppChangedEvent(context.Background(),
"app1",
@@ -1032,26 +1043,11 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
- AppID: "app1",
- AppName: "app",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
- OIDCVersion: domain.OIDCVersionV1,
- RedirectUris: []string{" https://test-change.ch "},
- ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
- GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
- ApplicationType: domain.OIDCApplicationTypeWeb,
- PostLogoutRedirectUris: []string{" https://test-change.ch/logout "},
- DevMode: true,
- AccessTokenType: domain.OIDCTokenTypeJWT,
- AccessTokenRoleAssertion: false,
- IDTokenRoleAssertion: false,
- IDTokenUserinfoAssertion: false,
- ClockSkew: time.Second * 2,
- AdditionalOrigins: []string{"https://sub.test.ch"},
- SkipNativeAppSuccessPage: true,
- BackChannelLogoutURI: "https://test.ch/backchannel",
- LoginVersion: domain.LoginVersion2,
- LoginBaseURI: "https://login.test.ch",
+ AppID: "app1",
+ AppName: "app",
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic),
+ GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
},
resourceOwner: "org1",
},
@@ -1064,24 +1060,24 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
AppID: "app1",
ClientID: "client1@project",
AppName: "app",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
- OIDCVersion: domain.OIDCVersionV1,
- RedirectUris: []string{"https://test-change.ch"},
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic),
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
+ RedirectUris: []string{"https://test.ch"},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
- ApplicationType: domain.OIDCApplicationTypeWeb,
- PostLogoutRedirectUris: []string{"https://test-change.ch/logout"},
- DevMode: true,
- AccessTokenType: domain.OIDCTokenTypeJWT,
- AccessTokenRoleAssertion: false,
- IDTokenRoleAssertion: false,
- IDTokenUserinfoAssertion: false,
- ClockSkew: time.Second * 2,
+ ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
+ PostLogoutRedirectUris: []string{"https://test.ch/logout"},
+ DevMode: gu.Ptr(false),
+ AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IDTokenRoleAssertion: gu.Ptr(true),
+ IDTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: gu.Ptr(time.Second * 1),
AdditionalOrigins: []string{"https://sub.test.ch"},
- SkipNativeAppSuccessPage: true,
- BackChannelLogoutURI: "https://test.ch/backchannel",
- LoginVersion: domain.LoginVersion2,
- LoginBaseURI: "https://login.test.ch",
+ SkipNativeAppSuccessPage: gu.Ptr(true),
+ BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"),
+ LoginVersion: gu.Ptr(domain.LoginVersion1),
+ LoginBaseURI: gu.Ptr(""),
Compliance: &domain.Compliance{},
State: domain.AppStateActive,
},
@@ -1090,10 +1086,12 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ // t.Parallel()
r := &Commands{
- eventstore: tt.fields.eventstore(t),
+ eventstore: tt.fields.eventstore(t),
+ checkPermission: newMockPermissionCheckAllowed(),
}
- got, err := r.ChangeOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner)
+ got, err := r.UpdateOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -1108,6 +1106,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
}
func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
+ t.Parallel()
+
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
}
@@ -1237,36 +1237,40 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
AppName: "app",
ClientID: "client1@project",
ClientSecretString: "secret",
- AuthMethodType: domain.OIDCAuthMethodTypePost,
- OIDCVersion: domain.OIDCVersionV1,
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost),
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
RedirectUris: []string{"https://test.ch"},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
- ApplicationType: domain.OIDCApplicationTypeWeb,
+ ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
PostLogoutRedirectUris: []string{"https://test.ch/logout"},
- DevMode: true,
- AccessTokenType: domain.OIDCTokenTypeBearer,
- AccessTokenRoleAssertion: true,
- IDTokenRoleAssertion: true,
- IDTokenUserinfoAssertion: true,
- ClockSkew: time.Second * 1,
+ DevMode: gu.Ptr(true),
+ AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IDTokenRoleAssertion: gu.Ptr(true),
+ IDTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: gu.Ptr(time.Second * 1),
AdditionalOrigins: []string{"https://sub.test.ch"},
- SkipNativeAppSuccessPage: false,
- BackChannelLogoutURI: "",
- LoginVersion: domain.LoginVersionUnspecified,
+ SkipNativeAppSuccessPage: gu.Ptr(false),
+ BackChannelLogoutURI: gu.Ptr(""),
+ LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
+ LoginBaseURI: gu.Ptr(""),
State: domain.AppStateActive,
},
},
},
}
for _, tt := range tests {
- t.Run(tt.name, func(*testing.T) {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
r := &Commands{
eventstore: tt.fields.eventstore(t),
newHashedSecret: mockHashedSecret("secret"),
defaultSecretGenerators: &SecretGenerators{
ClientSecret: emptyConfig,
},
+ checkPermission: newMockPermissionCheckAllowed(),
}
got, err := r.ChangeOIDCApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner)
if tt.res.err == nil {
@@ -1284,16 +1288,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
func newOIDCAppChangedEvent(ctx context.Context, appID, projectID, resourceOwner string) *project.OIDCConfigChangedEvent {
changes := []project.OIDCConfigChanges{
- project.ChangeRedirectURIs([]string{"https://test-change.ch"}),
- project.ChangePostLogoutRedirectURIs([]string{"https://test-change.ch/logout"}),
- project.ChangeDevMode(true),
- project.ChangeAccessTokenType(domain.OIDCTokenTypeJWT),
- project.ChangeAccessTokenRoleAssertion(false),
- project.ChangeIDTokenRoleAssertion(false),
- project.ChangeIDTokenUserinfoAssertion(false),
- project.ChangeClockSkew(time.Second * 2),
- project.ChangeOIDCLoginVersion(domain.LoginVersion2),
- project.ChangeOIDCLoginBaseURI("https://login.test.ch"),
+ project.ChangeAuthMethodType(domain.OIDCAuthMethodTypeBasic),
}
event, _ := project.NewOIDCConfigChangedEvent(ctx,
&project.NewAggregate(projectID, resourceOwner).Aggregate,
diff --git a/internal/command/project_application_saml.go b/internal/command/project_application_saml.go
index 1a5cefa221..9b1dc9e97a 100644
--- a/internal/command/project_application_saml.go
+++ b/internal/command/project_application_saml.go
@@ -3,6 +3,7 @@ package command
import (
"context"
+ "github.com/muhlemmer/gu"
"github.com/zitadel/saml/pkg/provider/xml"
"github.com/zitadel/zitadel/internal/domain"
@@ -16,10 +17,22 @@ func (c *Commands) AddSAMLApplication(ctx context.Context, application *domain.S
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-35Fn0", "Errors.Project.App.Invalid")
}
- if _, err := c.checkProjectExists(ctx, application.AggregateID, resourceOwner); err != nil {
+ projectResOwner, err := c.checkProjectExists(ctx, application.AggregateID, resourceOwner)
+ if err != nil {
return nil, err
}
+ if resourceOwner == "" {
+ resourceOwner = projectResOwner
+ }
+
addedApplication := NewSAMLApplicationWriteModel(application.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)
events, err := c.addSAMLApplication(ctx, projectAgg, application)
if err != nil {
@@ -49,12 +62,8 @@ func (c *Commands) addSAMLApplication(ctx context.Context, projectAgg *eventstor
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1n9df", "Errors.Project.App.Invalid")
}
- if samlApp.Metadata == nil && samlApp.MetadataURL == "" {
- return nil, zerrors.ThrowInvalidArgument(nil, "SAML-podix9", "Errors.Project.App.SAMLMetadataMissing")
- }
-
- if samlApp.MetadataURL != "" {
- data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL)
+ if samlApp.MetadataURL != nil && *samlApp.MetadataURL != "" {
+ data, err := xml.ReadMetadataFromURL(c.httpClient, *samlApp.MetadataURL)
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "SAML-wmqlo1", "Errors.Project.App.SAMLMetadataMissing")
}
@@ -78,14 +87,14 @@ func (c *Commands) addSAMLApplication(ctx context.Context, projectAgg *eventstor
samlApp.AppID,
string(entity.EntityID),
samlApp.Metadata,
- samlApp.MetadataURL,
- samlApp.LoginVersion,
- samlApp.LoginBaseURI,
+ gu.Value(samlApp.MetadataURL),
+ gu.Value(samlApp.LoginVersion),
+ gu.Value(samlApp.LoginBaseURI),
),
}, nil
}
-func (c *Commands) ChangeSAMLApplication(ctx context.Context, samlApp *domain.SAMLApp, resourceOwner string) (*domain.SAMLApp, error) {
+func (c *Commands) UpdateSAMLApplication(ctx context.Context, samlApp *domain.SAMLApp, resourceOwner string) (*domain.SAMLApp, error) {
if !samlApp.IsValid() || samlApp.AppID == "" || samlApp.AggregateID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-5n9fs", "Errors.Project.App.SAMLConfigInvalid")
}
@@ -100,10 +109,15 @@ func (c *Commands) ChangeSAMLApplication(ctx context.Context, samlApp *domain.SA
if !existingSAML.IsSAML() {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-GBr35", "Errors.Project.App.IsNotSAML")
}
+
+ if err := c.checkPermissionUpdateApplication(ctx, existingSAML.ResourceOwner, existingSAML.AggregateID); err != nil {
+ return nil, err
+ }
+
projectAgg := ProjectAggregateFromWriteModel(&existingSAML.WriteModel)
- if samlApp.MetadataURL != "" {
- data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL)
+ if samlApp.MetadataURL != nil && *samlApp.MetadataURL != "" {
+ data, err := xml.ReadMetadataFromURL(c.httpClient, *samlApp.MetadataURL)
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "SAML-J3kg3", "Errors.Project.App.SAMLMetadataMissing")
}
diff --git a/internal/command/project_application_saml_model.go b/internal/command/project_application_saml_model.go
index f219039b58..f3097914f3 100644
--- a/internal/command/project_application_saml_model.go
+++ b/internal/command/project_application_saml_model.go
@@ -2,7 +2,7 @@ package command
import (
"context"
- "reflect"
+ "slices"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -170,26 +170,26 @@ func (wm *SAMLApplicationWriteModel) NewChangedEvent(
appID string,
entityID string,
metadata []byte,
- metadataURL string,
- loginVersion domain.LoginVersion,
- loginBaseURI string,
+ metadataURL *string,
+ loginVersion *domain.LoginVersion,
+ loginBaseURI *string,
) (*project.SAMLConfigChangedEvent, bool, error) {
changes := make([]project.SAMLConfigChanges, 0)
var err error
- if !reflect.DeepEqual(wm.Metadata, metadata) {
+ if metadata != nil && !slices.Equal(wm.Metadata, metadata) {
changes = append(changes, project.ChangeMetadata(metadata))
}
- if wm.MetadataURL != metadataURL {
- changes = append(changes, project.ChangeMetadataURL(metadataURL))
+ if metadataURL != nil && wm.MetadataURL != *metadataURL {
+ changes = append(changes, project.ChangeMetadataURL(*metadataURL))
}
if wm.EntityID != entityID {
changes = append(changes, project.ChangeEntityID(entityID))
}
- if wm.LoginVersion != loginVersion {
- changes = append(changes, project.ChangeSAMLLoginVersion(loginVersion))
+ if loginVersion != nil && wm.LoginVersion != *loginVersion {
+ changes = append(changes, project.ChangeSAMLLoginVersion(*loginVersion))
}
- if wm.LoginBaseURI != loginBaseURI {
- changes = append(changes, project.ChangeSAMLLoginBaseURI(loginBaseURI))
+ if loginBaseURI != nil && wm.LoginBaseURI != *loginBaseURI {
+ changes = append(changes, project.ChangeSAMLLoginBaseURI(*loginBaseURI))
}
if len(changes) == 0 {
diff --git a/internal/command/project_application_saml_test.go b/internal/command/project_application_saml_test.go
index c6f6f7cf21..5d18d9587c 100644
--- a/internal/command/project_application_saml_test.go
+++ b/internal/command/project_application_saml_test.go
@@ -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(`
`)
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)
}
diff --git a/internal/command/project_application_test.go b/internal/command/project_application_test.go
index 050a41d29f..a67e6886ed 100644
--- a/internal/command/project_application_test.go
+++ b/internal/command/project_application_test.go
@@ -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 {
diff --git a/internal/command/project_converter.go b/internal/command/project_converter.go
index 01b5a4e63d..e88a1cb75a 100644
--- a/internal/command/project_converter.go
+++ b/internal/command/project_converter.go
@@ -1,6 +1,8 @@
package command
import (
+ "github.com/muhlemmer/gu"
+
"github.com/zitadel/zitadel/internal/domain"
)
@@ -35,21 +37,21 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O
RedirectUris: writeModel.RedirectUris,
ResponseTypes: writeModel.ResponseTypes,
GrantTypes: writeModel.GrantTypes,
- ApplicationType: writeModel.ApplicationType,
- AuthMethodType: writeModel.AuthMethodType,
+ ApplicationType: gu.Ptr(writeModel.ApplicationType),
+ AuthMethodType: gu.Ptr(writeModel.AuthMethodType),
PostLogoutRedirectUris: writeModel.PostLogoutRedirectUris,
- OIDCVersion: writeModel.OIDCVersion,
- DevMode: writeModel.DevMode,
- AccessTokenType: writeModel.AccessTokenType,
- AccessTokenRoleAssertion: writeModel.AccessTokenRoleAssertion,
- IDTokenRoleAssertion: writeModel.IDTokenRoleAssertion,
- IDTokenUserinfoAssertion: writeModel.IDTokenUserinfoAssertion,
- ClockSkew: writeModel.ClockSkew,
+ OIDCVersion: gu.Ptr(writeModel.OIDCVersion),
+ DevMode: gu.Ptr(writeModel.DevMode),
+ AccessTokenType: gu.Ptr(writeModel.AccessTokenType),
+ AccessTokenRoleAssertion: gu.Ptr(writeModel.AccessTokenRoleAssertion),
+ IDTokenRoleAssertion: gu.Ptr(writeModel.IDTokenRoleAssertion),
+ IDTokenUserinfoAssertion: gu.Ptr(writeModel.IDTokenUserinfoAssertion),
+ ClockSkew: gu.Ptr(writeModel.ClockSkew),
AdditionalOrigins: writeModel.AdditionalOrigins,
- SkipNativeAppSuccessPage: writeModel.SkipNativeAppSuccessPage,
- BackChannelLogoutURI: writeModel.BackChannelLogoutURI,
- LoginVersion: writeModel.LoginVersion,
- LoginBaseURI: writeModel.LoginBaseURI,
+ SkipNativeAppSuccessPage: gu.Ptr(writeModel.SkipNativeAppSuccessPage),
+ BackChannelLogoutURI: gu.Ptr(writeModel.BackChannelLogoutURI),
+ LoginVersion: gu.Ptr(writeModel.LoginVersion),
+ LoginBaseURI: gu.Ptr(writeModel.LoginBaseURI),
}
}
@@ -60,10 +62,10 @@ func samlWriteModelToSAMLConfig(writeModel *SAMLApplicationWriteModel) *domain.S
AppName: writeModel.AppName,
State: writeModel.State,
Metadata: writeModel.Metadata,
- MetadataURL: writeModel.MetadataURL,
+ MetadataURL: gu.Ptr(writeModel.MetadataURL),
EntityID: writeModel.EntityID,
- LoginVersion: writeModel.LoginVersion,
- LoginBaseURI: writeModel.LoginBaseURI,
+ LoginVersion: gu.Ptr(writeModel.LoginVersion),
+ LoginBaseURI: gu.Ptr(writeModel.LoginBaseURI),
}
}
@@ -78,15 +80,6 @@ func apiWriteModelToAPIConfig(writeModel *APIApplicationWriteModel) *domain.APIA
}
}
-func roleWriteModelToRole(writeModel *ProjectRoleWriteModel) *domain.ProjectRole {
- return &domain.ProjectRole{
- ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
- Key: writeModel.Key,
- DisplayName: writeModel.DisplayName,
- Group: writeModel.Group,
- }
-}
-
func memberWriteModelToProjectGrantMember(writeModel *ProjectGrantMemberWriteModel) *domain.ProjectGrantMember {
return &domain.ProjectGrantMember{
ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
diff --git a/internal/command/project_model.go b/internal/command/project_model.go
index cabceb8500..4c9496b3ad 100644
--- a/internal/command/project_model.go
+++ b/internal/command/project_model.go
@@ -2,6 +2,7 @@ package command
import (
"context"
+ "slices"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -120,7 +121,7 @@ func (wm *ProjectWriteModel) NewChangedEvent(
}
func isProjectStateExists(state domain.ProjectState) bool {
- return !hasProjectState(state, domain.ProjectStateRemoved, domain.ProjectStateUnspecified)
+ return !slices.Contains([]domain.ProjectState{domain.ProjectStateRemoved, domain.ProjectStateUnspecified}, state)
}
func ProjectAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
@@ -130,12 +131,3 @@ func ProjectAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggre
func ProjectAggregateFromWriteModelWithCTX(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate {
return project.AggregateFromWriteModel(ctx, wm)
}
-
-func hasProjectState(check domain.ProjectState, states ...domain.ProjectState) bool {
- for _, state := range states {
- if check == state {
- return true
- }
- }
- return false
-}
diff --git a/internal/domain/application_oidc.go b/internal/domain/application_oidc.go
index 5d466c689d..10a70a1776 100644
--- a/internal/domain/application_oidc.go
+++ b/internal/domain/application_oidc.go
@@ -1,6 +1,7 @@
package domain
import (
+ "slices"
"strings"
"time"
@@ -32,22 +33,22 @@ type OIDCApp struct {
RedirectUris []string
ResponseTypes []OIDCResponseType
GrantTypes []OIDCGrantType
- ApplicationType OIDCApplicationType
- AuthMethodType OIDCAuthMethodType
+ ApplicationType *OIDCApplicationType
+ AuthMethodType *OIDCAuthMethodType
PostLogoutRedirectUris []string
- OIDCVersion OIDCVersion
+ OIDCVersion *OIDCVersion
Compliance *Compliance
- DevMode bool
- AccessTokenType OIDCTokenType
- AccessTokenRoleAssertion bool
- IDTokenRoleAssertion bool
- IDTokenUserinfoAssertion bool
- ClockSkew time.Duration
+ DevMode *bool
+ AccessTokenType *OIDCTokenType
+ AccessTokenRoleAssertion *bool
+ IDTokenRoleAssertion *bool
+ IDTokenUserinfoAssertion *bool
+ ClockSkew *time.Duration
AdditionalOrigins []string
- SkipNativeAppSuccessPage bool
- BackChannelLogoutURI string
- LoginVersion LoginVersion
- LoginBaseURI string
+ SkipNativeAppSuccessPage *bool
+ BackChannelLogoutURI *string
+ LoginVersion *LoginVersion
+ LoginBaseURI *string
State AppState
}
@@ -69,7 +70,7 @@ func (a *OIDCApp) setClientSecret(encodedHash string) {
}
func (a *OIDCApp) requiresClientSecret() bool {
- return a.AuthMethodType == OIDCAuthMethodTypeBasic || a.AuthMethodType == OIDCAuthMethodTypePost
+ return a.AuthMethodType != nil && (*a.AuthMethodType == OIDCAuthMethodTypeBasic || *a.AuthMethodType == OIDCAuthMethodTypePost)
}
type OIDCVersion int32
@@ -137,7 +138,7 @@ const (
)
func (a *OIDCApp) IsValid() bool {
- if a.ClockSkew > time.Second*5 || a.ClockSkew < time.Second*0 || !a.OriginsValid() {
+ if (a.ClockSkew != nil && (*a.ClockSkew > time.Second*5 || *a.ClockSkew < time.Second*0)) || !a.OriginsValid() {
return false
}
grantTypes := a.getRequiredGrantTypes()
@@ -204,30 +205,25 @@ func ContainsOIDCGrantTypes(shouldContain, list []OIDCGrantType) bool {
}
func containsOIDCGrantType(grantTypes []OIDCGrantType, grantType OIDCGrantType) bool {
- for _, gt := range grantTypes {
- if gt == grantType {
- return true
- }
- }
- return false
+ return slices.Contains(grantTypes, grantType)
}
func (a *OIDCApp) FillCompliance() {
a.Compliance = GetOIDCCompliance(a.OIDCVersion, a.ApplicationType, a.GrantTypes, a.ResponseTypes, a.AuthMethodType, a.RedirectUris)
}
-func GetOIDCCompliance(version OIDCVersion, appType OIDCApplicationType, grantTypes []OIDCGrantType, responseTypes []OIDCResponseType, authMethod OIDCAuthMethodType, redirectUris []string) *Compliance {
- switch version {
- case OIDCVersionV1:
+func GetOIDCCompliance(version *OIDCVersion, appType *OIDCApplicationType, grantTypes []OIDCGrantType, responseTypes []OIDCResponseType, authMethod *OIDCAuthMethodType, redirectUris []string) *Compliance {
+ if version != nil && *version == OIDCVersionV1 {
return GetOIDCV1Compliance(appType, grantTypes, authMethod, redirectUris)
}
+
return &Compliance{
NoneCompliant: true,
Problems: []string{"Application.OIDC.UnsupportedVersion"},
}
}
-func GetOIDCV1Compliance(appType OIDCApplicationType, grantTypes []OIDCGrantType, authMethod OIDCAuthMethodType, redirectUris []string) *Compliance {
+func GetOIDCV1Compliance(appType *OIDCApplicationType, grantTypes []OIDCGrantType, authMethod *OIDCAuthMethodType, redirectUris []string) *Compliance {
compliance := &Compliance{NoneCompliant: false}
checkGrantTypesCombination(compliance, grantTypes)
@@ -247,7 +243,7 @@ func checkGrantTypesCombination(compliance *Compliance, grantTypes []OIDCGrantTy
}
}
-func checkRedirectURIs(compliance *Compliance, grantTypes []OIDCGrantType, appType OIDCApplicationType, redirectUris []string) {
+func checkRedirectURIs(compliance *Compliance, grantTypes []OIDCGrantType, appType *OIDCApplicationType, redirectUris []string) {
// See #5684 for OIDCGrantTypeDeviceCode and redirectUris further explanation
if len(redirectUris) == 0 && (!containsOIDCGrantType(grantTypes, OIDCGrantTypeDeviceCode) || (containsOIDCGrantType(grantTypes, OIDCGrantTypeDeviceCode) && containsOIDCGrantType(grantTypes, OIDCGrantTypeAuthorizationCode))) {
compliance.NoneCompliant = true
@@ -266,53 +262,58 @@ func checkRedirectURIs(compliance *Compliance, grantTypes []OIDCGrantType, appTy
}
}
-func checkApplicationType(compliance *Compliance, appType OIDCApplicationType, authMethod OIDCAuthMethodType) {
- switch appType {
- case OIDCApplicationTypeNative:
- GetOIDCV1NativeApplicationCompliance(compliance, authMethod)
- case OIDCApplicationTypeUserAgent:
- GetOIDCV1UserAgentApplicationCompliance(compliance, authMethod)
+func checkApplicationType(compliance *Compliance, appType *OIDCApplicationType, authMethod *OIDCAuthMethodType) {
+ if appType != nil {
+ switch *appType {
+ case OIDCApplicationTypeNative:
+ GetOIDCV1NativeApplicationCompliance(compliance, authMethod)
+ case OIDCApplicationTypeUserAgent:
+ GetOIDCV1UserAgentApplicationCompliance(compliance, authMethod)
+ case OIDCApplicationTypeWeb:
+ return
+ }
}
+
if compliance.NoneCompliant {
compliance.Problems = append([]string{"Application.OIDC.V1.NotCompliant"}, compliance.Problems...)
}
}
-func GetOIDCV1NativeApplicationCompliance(compliance *Compliance, authMethod OIDCAuthMethodType) {
- if authMethod != OIDCAuthMethodTypeNone {
+func GetOIDCV1NativeApplicationCompliance(compliance *Compliance, authMethod *OIDCAuthMethodType) {
+ if authMethod != nil && *authMethod != OIDCAuthMethodTypeNone {
compliance.NoneCompliant = true
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.AuthMethodType.NotNone")
}
}
-func GetOIDCV1UserAgentApplicationCompliance(compliance *Compliance, authMethod OIDCAuthMethodType) {
- if authMethod != OIDCAuthMethodTypeNone {
+func GetOIDCV1UserAgentApplicationCompliance(compliance *Compliance, authMethod *OIDCAuthMethodType) {
+ if authMethod != nil && *authMethod != OIDCAuthMethodTypeNone {
compliance.NoneCompliant = true
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.UserAgent.AuthMethodType.NotNone")
}
}
-func CheckRedirectUrisCode(compliance *Compliance, appType OIDCApplicationType, redirectUris []string) {
+func CheckRedirectUrisCode(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) {
if urlsAreHttps(redirectUris) {
return
}
if urlContainsPrefix(redirectUris, http) {
- if appType == OIDCApplicationTypeUserAgent {
+ if appType != nil && *appType == OIDCApplicationTypeUserAgent {
compliance.NoneCompliant = true
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb")
}
- if appType == OIDCApplicationTypeNative && !onlyLocalhostIsHttp(redirectUris) {
+ if appType != nil && *appType == OIDCApplicationTypeNative && !onlyLocalhostIsHttp(redirectUris) {
compliance.NoneCompliant = true
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost")
}
}
- if containsCustom(redirectUris) && appType != OIDCApplicationTypeNative {
+ if containsCustom(redirectUris) && appType != nil && *appType != OIDCApplicationTypeNative {
compliance.NoneCompliant = true
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.CustomOnlyForNative")
}
}
-func CheckRedirectUrisImplicit(compliance *Compliance, appType OIDCApplicationType, redirectUris []string) {
+func CheckRedirectUrisImplicit(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) {
if urlsAreHttps(redirectUris) {
return
}
@@ -321,7 +322,7 @@ func CheckRedirectUrisImplicit(compliance *Compliance, appType OIDCApplicationTy
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed")
}
if urlContainsPrefix(redirectUris, http) {
- if appType == OIDCApplicationTypeNative {
+ if appType != nil && *appType == OIDCApplicationTypeNative {
if !onlyLocalhostIsHttp(redirectUris) {
compliance.NoneCompliant = true
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost")
@@ -333,20 +334,20 @@ func CheckRedirectUrisImplicit(compliance *Compliance, appType OIDCApplicationTy
}
}
-func CheckRedirectUrisImplicitAndCode(compliance *Compliance, appType OIDCApplicationType, redirectUris []string) {
+func CheckRedirectUrisImplicitAndCode(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) {
if urlsAreHttps(redirectUris) {
return
}
- if containsCustom(redirectUris) && appType != OIDCApplicationTypeNative {
+ if containsCustom(redirectUris) && appType != nil && *appType != OIDCApplicationTypeNative {
compliance.NoneCompliant = true
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed")
}
if urlContainsPrefix(redirectUris, http) {
- if appType == OIDCApplicationTypeUserAgent {
+ if appType != nil && *appType == OIDCApplicationTypeUserAgent {
compliance.NoneCompliant = true
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb")
}
- if !onlyLocalhostIsHttp(redirectUris) && appType == OIDCApplicationTypeNative {
+ if !onlyLocalhostIsHttp(redirectUris) && appType != nil && *appType == OIDCApplicationTypeNative {
compliance.NoneCompliant = true
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost")
}
diff --git a/internal/domain/application_oidc_test.go b/internal/domain/application_oidc_test.go
index b3d9488827..4208917cdd 100644
--- a/internal/domain/application_oidc_test.go
+++ b/internal/domain/application_oidc_test.go
@@ -6,6 +6,8 @@ import (
"testing"
"time"
+ "github.com/muhlemmer/gu"
+
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
@@ -25,7 +27,7 @@ func TestApplicationValid(t *testing.T) {
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
AppID: "AppID",
AppName: "AppName",
- ClockSkew: time.Minute * 1,
+ ClockSkew: gu.Ptr(time.Minute * 1),
ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode},
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode},
},
@@ -39,7 +41,7 @@ func TestApplicationValid(t *testing.T) {
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
AppID: "AppID",
AppName: "AppName",
- ClockSkew: time.Minute * -1,
+ ClockSkew: gu.Ptr(time.Minute * -1),
ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode},
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode},
},
@@ -190,9 +192,9 @@ func TestApplicationValid(t *testing.T) {
func TestGetOIDCV1Compliance(t *testing.T) {
type args struct {
- appType OIDCApplicationType
+ appType *OIDCApplicationType
grantTypes []OIDCGrantType
- authMethod OIDCAuthMethodType
+ authMethod *OIDCAuthMethodType
redirectUris []string
}
tests := []struct {
@@ -266,7 +268,7 @@ func Test_checkGrantTypesCombination(t *testing.T) {
func Test_checkRedirectURIs(t *testing.T) {
type args struct {
grantTypes []OIDCGrantType
- appType OIDCApplicationType
+ appType *OIDCApplicationType
redirectUris []string
}
tests := []struct {
@@ -304,7 +306,7 @@ func Test_checkRedirectURIs(t *testing.T) {
args: args{
redirectUris: []string{"http://redirect.to/me"},
grantTypes: []OIDCGrantType{OIDCGrantTypeImplicit},
- appType: OIDCApplicationTypeUserAgent,
+ appType: gu.Ptr(OIDCApplicationTypeUserAgent),
},
},
{
@@ -316,7 +318,7 @@ func Test_checkRedirectURIs(t *testing.T) {
args: args{
redirectUris: []string{"http://redirect.to/me"},
grantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode},
- appType: OIDCApplicationTypeUserAgent,
+ appType: gu.Ptr(OIDCApplicationTypeUserAgent),
},
},
}
@@ -338,7 +340,7 @@ func Test_checkRedirectURIs(t *testing.T) {
func Test_CheckRedirectUrisImplicitAndCode(t *testing.T) {
type args struct {
- appType OIDCApplicationType
+ appType *OIDCApplicationType
redirectUris []string
}
tests := []struct {
@@ -356,17 +358,6 @@ func Test_CheckRedirectUrisImplicitAndCode(t *testing.T) {
redirectUris: []string{"https://redirect.to/me"},
},
},
- // {
- // name: "custom protocol, not native",
- // want: &Compliance{
- // NoneCompliant: true,
- // Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed"},
- // },
- // args: args{
- // redirectUris: []string{"protocol://redirect.to/me"},
- // appType: OIDCApplicationTypeWeb,
- // },
- // },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -386,7 +377,7 @@ func Test_CheckRedirectUrisImplicitAndCode(t *testing.T) {
func TestCheckRedirectUrisImplicitAndCode(t *testing.T) {
type args struct {
- appType OIDCApplicationType
+ appType *OIDCApplicationType
redirectUris []string
}
tests := []struct {
@@ -402,7 +393,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) {
{
name: "custom protocol not native app",
args: args{
- appType: OIDCApplicationTypeWeb,
+ appType: gu.Ptr(OIDCApplicationTypeWeb),
redirectUris: []string{"custom://nirvana.com"},
},
want: &Compliance{
@@ -413,7 +404,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) {
{
name: "http localhost user agent app",
args: args{
- appType: OIDCApplicationTypeUserAgent,
+ appType: gu.Ptr(OIDCApplicationTypeUserAgent),
redirectUris: []string{"http://localhost:9009"},
},
want: &Compliance{
@@ -424,7 +415,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) {
{
name: "http, not only localhost native app",
args: args{
- appType: OIDCApplicationTypeNative,
+ appType: gu.Ptr(OIDCApplicationTypeNative),
redirectUris: []string{"http://nirvana.com", "http://localhost:9009"},
},
want: &Compliance{
@@ -435,7 +426,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) {
{
name: "not allowed combination",
args: args{
- appType: OIDCApplicationTypeNative,
+ appType: gu.Ptr(OIDCApplicationTypeNative),
redirectUris: []string{"https://nirvana.com", "cutom://nirvana.com"},
},
want: &Compliance{
@@ -461,7 +452,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) {
func TestCheckRedirectUrisImplicit(t *testing.T) {
type args struct {
- appType OIDCApplicationType
+ appType *OIDCApplicationType
redirectUris []string
}
tests := []struct {
@@ -488,7 +479,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) {
name: "only http protocol, app type native, not only localhost",
args: args{
redirectUris: []string{"http://nirvana.com"},
- appType: OIDCApplicationTypeNative,
+ appType: gu.Ptr(OIDCApplicationTypeNative),
},
want: &Compliance{
NoneCompliant: true,
@@ -499,7 +490,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) {
name: "only http protocol, app type native, only localhost",
args: args{
redirectUris: []string{"http://localhost:8080"},
- appType: OIDCApplicationTypeNative,
+ appType: gu.Ptr(OIDCApplicationTypeNative),
},
want: &Compliance{
NoneCompliant: false,
@@ -510,7 +501,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) {
name: "only http protocol, app type web",
args: args{
redirectUris: []string{"http://nirvana.com"},
- appType: OIDCApplicationTypeWeb,
+ appType: gu.Ptr(OIDCApplicationTypeWeb),
},
want: &Compliance{
NoneCompliant: true,
@@ -535,7 +526,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) {
func TestCheckRedirectUrisCode(t *testing.T) {
type args struct {
- appType OIDCApplicationType
+ appType *OIDCApplicationType
redirectUris []string
}
tests := []struct {
@@ -552,7 +543,7 @@ func TestCheckRedirectUrisCode(t *testing.T) {
name: "custom prefix, app type web",
args: args{
redirectUris: []string{"custom://nirvana.com"},
- appType: OIDCApplicationTypeWeb,
+ appType: gu.Ptr(OIDCApplicationTypeWeb),
},
want: &Compliance{
NoneCompliant: true,
@@ -563,7 +554,7 @@ func TestCheckRedirectUrisCode(t *testing.T) {
name: "only http protocol, app type user agent",
args: args{
redirectUris: []string{"http://nirvana.com"},
- appType: OIDCApplicationTypeUserAgent,
+ appType: gu.Ptr(OIDCApplicationTypeUserAgent),
},
want: &Compliance{
NoneCompliant: true,
@@ -574,7 +565,7 @@ func TestCheckRedirectUrisCode(t *testing.T) {
name: "only http protocol, app type native, only localhost",
args: args{
redirectUris: []string{"http://localhost:8080", "http://nirvana.com:8080"},
- appType: OIDCApplicationTypeNative,
+ appType: gu.Ptr(OIDCApplicationTypeNative),
},
want: &Compliance{
NoneCompliant: true,
@@ -585,7 +576,7 @@ func TestCheckRedirectUrisCode(t *testing.T) {
name: "custom protocol, not native",
args: args{
redirectUris: []string{"custom://nirvana.com"},
- appType: OIDCApplicationTypeWeb,
+ appType: gu.Ptr(OIDCApplicationTypeWeb),
},
want: &Compliance{
NoneCompliant: true,
diff --git a/internal/domain/application_saml.go b/internal/domain/application_saml.go
index de7ef789ee..aff1875c7e 100644
--- a/internal/domain/application_saml.go
+++ b/internal/domain/application_saml.go
@@ -11,9 +11,9 @@ type SAMLApp struct {
AppName string
EntityID string
Metadata []byte
- MetadataURL string
- LoginVersion LoginVersion
- LoginBaseURI string
+ MetadataURL *string
+ LoginVersion *LoginVersion
+ LoginBaseURI *string
State AppState
}
@@ -31,11 +31,14 @@ func (a *SAMLApp) GetMetadata() []byte {
}
func (a *SAMLApp) GetMetadataURL() string {
- return a.MetadataURL
+ if a.MetadataURL != nil {
+ return *a.MetadataURL
+ }
+ return ""
}
func (a *SAMLApp) IsValid() bool {
- if a.MetadataURL == "" && a.Metadata == nil {
+ if (a.MetadataURL == nil || *a.MetadataURL == "") && a.Metadata == nil {
return false
}
return true
diff --git a/internal/domain/permission.go b/internal/domain/permission.go
index bb569955f5..119e8c2d3e 100644
--- a/internal/domain/permission.go
+++ b/internal/domain/permission.go
@@ -47,6 +47,9 @@ const (
PermissionProjectRoleWrite = "project.role.write"
PermissionProjectRoleRead = "project.role.read"
PermissionProjectRoleDelete = "project.role.delete"
+ PermissionProjectAppWrite = "project.app.write"
+ PermissionProjectAppDelete = "project.app.delete"
+ PermissionProjectAppRead = "project.app.read"
)
// ProjectPermissionCheck is used as a check for preconditions dependent on application, project, user resourceowner and usergrants.
diff --git a/internal/integration/client.go b/internal/integration/client.go
index 20c98b5628..326d6fa8b4 100644
--- a/internal/integration/client.go
+++ b/internal/integration/client.go
@@ -22,6 +22,7 @@ import (
"github.com/zitadel/zitadel/internal/integration/scim"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/admin"
+ app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/auth"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
feature_v2beta "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
@@ -75,6 +76,7 @@ type Client struct {
SCIM *scim.Client
Projectv2Beta project_v2beta.ProjectServiceClient
InstanceV2Beta instance.InstanceServiceClient
+ AppV2Beta app.AppServiceClient
}
func NewDefaultClient(ctx context.Context) (*Client, error) {
@@ -114,6 +116,7 @@ func newClient(ctx context.Context, target string) (*Client, error) {
SCIM: scim.NewScimClient(target),
Projectv2Beta: project_v2beta.NewProjectServiceClient(cc),
InstanceV2Beta: instance.NewInstanceServiceClient(cc),
+ AppV2Beta: app.NewAppServiceClient(cc),
}
return client, client.pollHealth(ctx)
}
diff --git a/internal/project/model/oidc_config.go b/internal/project/model/oidc_config.go
index 50be6c318a..2c482a67a7 100644
--- a/internal/project/model/oidc_config.go
+++ b/internal/project/model/oidc_config.go
@@ -3,6 +3,8 @@ package model
import (
"time"
+ "github.com/muhlemmer/gu"
+
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -98,7 +100,7 @@ func GetOIDCCompliance(version OIDCVersion, appType OIDCApplicationType, grantTy
for i, grantType := range grantTypes {
domainGrantTypes[i] = domain.OIDCGrantType(grantType)
}
- compliance := domain.GetOIDCV1Compliance(domain.OIDCApplicationType(appType), domainGrantTypes, domain.OIDCAuthMethodType(authMethod), redirectUris)
+ compliance := domain.GetOIDCV1Compliance(gu.Ptr(domain.OIDCApplicationType(appType)), domainGrantTypes, gu.Ptr(domain.OIDCAuthMethodType(authMethod)), redirectUris)
return &Compliance{
NoneCompliant: compliance.NoneCompliant,
Problems: compliance.Problems,
diff --git a/internal/query/app.go b/internal/query/app.go
index bc97c1807e..777c295139 100644
--- a/internal/query/app.go
+++ b/internal/query/app.go
@@ -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
diff --git a/pkg/grpc/app/v2beta/application.go b/pkg/grpc/app/v2beta/application.go
new file mode 100644
index 0000000000..bbce4289f9
--- /dev/null
+++ b/pkg/grpc/app/v2beta/application.go
@@ -0,0 +1,5 @@
+package app
+
+type ApplicationConfig = isApplication_Config
+
+type MetaType = isUpdateSAMLApplicationConfigurationRequest_Metadata
\ No newline at end of file
diff --git a/proto/zitadel/app/v2beta/api.proto b/proto/zitadel/app/v2beta/api.proto
new file mode 100644
index 0000000000..9ef09d5ad8
--- /dev/null
+++ b/proto/zitadel/app/v2beta/api.proto
@@ -0,0 +1,26 @@
+syntax = "proto3";
+
+package zitadel.app.v2beta;
+
+import "protoc-gen-openapiv2/options/annotations.proto";
+
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app";
+
+enum APIAuthMethodType {
+ API_AUTH_METHOD_TYPE_BASIC = 0;
+ API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT = 1;
+}
+
+message APIConfig {
+ string client_id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"69629023906488334@ZITADEL\"";
+ description: "generated oauth2/oidc client_id";
+ }
+ ];
+ APIAuthMethodType auth_method_type = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "defines how the API passes the login credentials";
+ }
+ ];
+}
\ No newline at end of file
diff --git a/proto/zitadel/app/v2beta/app.proto b/proto/zitadel/app/v2beta/app.proto
new file mode 100644
index 0000000000..f85e3c021d
--- /dev/null
+++ b/proto/zitadel/app/v2beta/app.proto
@@ -0,0 +1,94 @@
+syntax = "proto3";
+
+package zitadel.app.v2beta;
+
+import "zitadel/app/v2beta/oidc.proto";
+import "zitadel/app/v2beta/saml.proto";
+import "zitadel/app/v2beta/api.proto";
+import "zitadel/filter/v2/filter.proto";
+import "protoc-gen-openapiv2/options/annotations.proto";
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+import "validate/validate.proto";
+
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app";
+
+message Application {
+ string id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"69629023906488334\"";
+ }
+ ];
+
+ // The timestamp of the app creation.
+ google.protobuf.Timestamp creation_date = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2024-12-18T07:50:47.492Z\"";
+ }
+ ];
+
+ // The timestamp of the app update.
+ google.protobuf.Timestamp change_date = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2024-12-18T07:50:47.492Z\"";
+ }
+ ];
+
+ AppState state = 4 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "current state of the application";
+ }
+ ];
+ string name = 5 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"Console\"";
+ }
+ ];
+ oneof config {
+ OIDCConfig oidc_config = 6;
+ APIConfig api_config = 7;
+ SAMLConfig saml_config = 8;
+ }
+}
+
+enum AppState {
+ APP_STATE_UNSPECIFIED = 0;
+ APP_STATE_ACTIVE = 1;
+ APP_STATE_INACTIVE = 2;
+ APP_STATE_REMOVED = 3;
+}
+
+enum AppSorting {
+ APP_SORT_BY_ID = 0;
+ APP_SORT_BY_NAME = 1;
+ APP_SORT_BY_STATE = 2;
+ APP_SORT_BY_CREATION_DATE = 3;
+ APP_SORT_BY_CHANGE_DATE = 4;
+}
+
+message ApplicationSearchFilter {
+ oneof filter {
+ option (validate.required) = true;
+ ApplicationNameQuery name_filter = 1;
+ AppState state_filter = 2;
+ bool api_app_only = 3;
+ bool oidc_app_only = 4;
+ bool saml_app_only = 5;
+ }
+}
+
+message ApplicationNameQuery {
+ string name = 1 [
+ (validate.rules).string = {max_len: 200},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"Conso\""
+ }
+ ];
+
+ zitadel.filter.v2.TextFilterMethod method = 2 [
+ (validate.rules).enum.defined_only = true,
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "defines which text equality method is used"
+ }
+ ];
+}
diff --git a/proto/zitadel/app/v2beta/app_service.proto b/proto/zitadel/app/v2beta/app_service.proto
new file mode 100644
index 0000000000..a881022caa
--- /dev/null
+++ b/proto/zitadel/app/v2beta/app_service.proto
@@ -0,0 +1,788 @@
+syntax = "proto3";
+
+package zitadel.app.v2beta;
+
+import "google/api/annotations.proto";
+import "google/api/field_behavior.proto";
+import "google/protobuf/duration.proto";
+import "google/protobuf/struct.proto";
+import "protoc-gen-openapiv2/options/annotations.proto";
+import "validate/validate.proto";
+import "zitadel/app/v2beta/login.proto";
+import "zitadel/app/v2beta/oidc.proto";
+import "zitadel/app/v2beta/api.proto";
+import "zitadel/app/v2beta/app.proto";
+import "google/protobuf/timestamp.proto";
+import "zitadel/protoc_gen_zitadel/v2/options.proto";
+import "zitadel/filter/v2/filter.proto";
+
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app";
+
+option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
+ info: {
+ title: "Application Service";
+ version: "2.0-beta";
+ description: "This API is intended to manage apps (SAML, OIDC, etc..) in a ZITADEL instance. This service is in beta state. It can AND will continue breaking until a stable version is released.";
+ contact:{
+ name: "ZITADEL"
+ url: "https://zitadel.com"
+ email: "hi@zitadel.com"
+ }
+ license: {
+ name: "Apache 2.0",
+ url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md";
+ };
+ };
+ schemes: HTTPS;
+ schemes: HTTP;
+
+ consumes: "application/json";
+ consumes: "application/grpc";
+
+ produces: "application/json";
+ produces: "application/grpc";
+
+ consumes: "application/grpc-web+proto";
+ produces: "application/grpc-web+proto";
+
+ host: "$CUSTOM-DOMAIN";
+ base_path: "/";
+
+ external_docs: {
+ description: "Detailed information about ZITADEL",
+ url: "https://zitadel.com/docs"
+ }
+ security_definitions: {
+ security: {
+ key: "OAuth2";
+ value: {
+ type: TYPE_OAUTH2;
+ flow: FLOW_ACCESS_CODE;
+ authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
+ token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
+ scopes: {
+ scope: {
+ key: "openid";
+ value: "openid";
+ }
+ scope: {
+ key: "urn:zitadel:iam:org:project:id:zitadel:aud";
+ value: "urn:zitadel:iam:org:project:id:zitadel:aud";
+ }
+ }
+ }
+ }
+ }
+ security: {
+ security_requirement: {
+ key: "OAuth2";
+ value: {
+ scope: "openid";
+ scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
+ }
+ }
+ }
+ responses: {
+ key: "403";
+ value: {
+ description: "Returned when the user does not have permission to access the resource.";
+ schema: {
+ json_schema: {
+ ref: "#/definitions/rpcStatus";
+ }
+ }
+ }
+ }
+ responses: {
+ key: "404";
+ value: {
+ description: "Returned when the resource does not exist.";
+ schema: {
+ json_schema: {
+ ref: "#/definitions/rpcStatus";
+ }
+ }
+ }
+ }
+};
+
+// Service to manage apps.
+// The service provides methods to create, update, delete and list apps and app keys.
+service AppService {
+
+ // Create Application
+ //
+ // Create an application. The application can be OIDC, API or SAML type, based on the input.
+ //
+ // The user needs to have project.app.write permission
+ //
+ // Required permissions:
+ // - project.app.write
+ rpc CreateApplication(CreateApplicationRequest) returns (CreateApplicationResponse) {
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ responses: {
+ key: "200";
+ value: {
+ description: "The created application";
+ }
+ };
+ };
+
+ option (google.api.http) = {
+ post: "/v2beta/applications"
+ body: "*"
+ };
+
+ option (zitadel.protoc_gen_zitadel.v2.options) = {
+ auth_option: {
+ permission: "authenticated"
+ }
+ };
+ }
+
+ // Update Application
+ //
+ // Changes the configuration of an OIDC, API or SAML type application, as well as
+ // the application name, based on the input provided.
+ //
+ // The user needs to have project.app.write permission
+ //
+ // Required permissions:
+ // - project.app.write
+ rpc UpdateApplication(UpdateApplicationRequest) returns (UpdateApplicationResponse) {
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ responses: {
+ key: "200";
+ value: {
+ description: "The updated app.";
+ }
+ };
+ };
+
+ option (google.api.http) = {
+ patch: "/v2beta/applications/{id}"
+ body: "*"
+ };
+
+ option (zitadel.protoc_gen_zitadel.v2.options) = {
+ auth_option: {
+ permission: "authenticated"
+ }
+ };
+ }
+
+ // Get Application
+ //
+ // Retrieves the application matching the provided ID.
+ //
+ // The user needs to have project.app.read permission
+ //
+ // Required permissions:
+ // - project.app.read
+ rpc GetApplication(GetApplicationRequest) returns (GetApplicationResponse) {
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ responses: {
+ key: "200";
+ value: {
+ description: "The fetched app.";
+ }
+ };
+ };
+
+ option (google.api.http) = {
+ get: "/v2beta/applications/{id}"
+ };
+
+ option (zitadel.protoc_gen_zitadel.v2.options) = {
+ auth_option: {
+ permission: "authenticated"
+ }
+ };
+ }
+
+ // Delete Application
+ //
+ // Deletes the application belonging to the input project and matching the provided
+ // application ID
+ //
+ // The user needs to have project.app.delete permission
+ //
+ // Required permissions:
+ // - project.app.delete
+ rpc DeleteApplication(DeleteApplicationRequest) returns (DeleteApplicationResponse) {
+ option (google.api.http) = {
+ delete: "/v2beta/applications/{id}"
+ };
+
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ responses: {
+ key: "200";
+ value: {
+ description: "The time of deletion.";
+ }
+ };
+ };
+
+ option (zitadel.protoc_gen_zitadel.v2.options) = {
+ auth_option: {
+ permission: "authenticated"
+ }
+ };
+ }
+
+ // Deactivate Application
+ //
+ // Deactivates the application belonging to the input project and matching the provided
+ // application ID
+ //
+ // The user needs to have project.app.write permission
+ //
+ // Required permissions:
+ // - project.app.write
+ rpc DeactivateApplication(DeactivateApplicationRequest) returns (DeactivateApplicationResponse) {
+ option (google.api.http) = {
+ post: "/v2beta/applications/{id}/deactivate"
+ body: "*"
+ };
+
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ responses: {
+ key: "200";
+ value: {
+ description: "The time of deactivation.";
+ }
+ };
+ };
+
+ option (zitadel.protoc_gen_zitadel.v2.options) = {
+ auth_option: {
+ permission: "authenticated"
+ }
+ };
+ }
+
+ // Reactivate Application
+ //
+ // Reactivates the application belonging to the input project and matching the provided
+ // application ID
+ //
+ // The user needs to have project.app.write permission
+ //
+ // Required permissions:
+ // - project.app.write
+ rpc ReactivateApplication(ReactivateApplicationRequest) returns (ReactivateApplicationResponse) {
+ option (google.api.http) = {
+ post: "/v2beta/applications/{id}/reactivate"
+ body: "*"
+ };
+
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ responses: {
+ key: "200";
+ value: {
+ description: "The time of reactivation.";
+ }
+ };
+ };
+
+ option (zitadel.protoc_gen_zitadel.v2.options) = {
+ auth_option: {
+ permission: "authenticated"
+ }
+ };
+ }
+
+
+ // Regenerate Client Secret
+ //
+ // Regenerates the client secret of an API or OIDC application that belongs to the input project.
+ //
+ // The user needs to have project.app.write permission
+ //
+ // Required permissions:
+ // - project.app.write
+ rpc RegenerateClientSecret(RegenerateClientSecretRequest) returns (RegenerateClientSecretResponse) {
+ option (google.api.http) = {
+ post: "/v2beta/applications/{application_id}/generate_client_secret"
+ body: "*"
+ };
+
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ responses: {
+ key: "200";
+ value: {
+ description: "The regenerated client secret.";
+ }
+ };
+ };
+
+ option (zitadel.protoc_gen_zitadel.v2.options) = {
+ auth_option: {
+ permission: "authenticated"
+ }
+ };
+ }
+
+ // List Applications
+ //
+ // Returns a list of applications matching the input parameters that belong to the provided
+ // project.
+ //
+ // The result can be sorted by app id, name, creation date, change date or state. It can also
+ // be filtered by app state, app type and app name.
+ //
+ // The user needs to have project.app.read permission
+ //
+ // Required permissions:
+ // - project.app.read
+ rpc ListApplications(ListApplicationsRequest) returns (ListApplicationsResponse) {
+ option (google.api.http) = {
+ post: "/v2beta/applications/search"
+ body: "*"
+ };
+
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ responses: {
+ key: "200";
+ value: {
+ description: "The matching applications";
+ }
+ };
+ };
+
+ option (zitadel.protoc_gen_zitadel.v2.options) = {
+ auth_option: {
+ permission: "authenticated"
+ }
+ };
+ }
+}
+
+message CreateApplicationRequest {
+ string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string id = 2 [(validate.rules).string = {max_len: 200}];
+ string name = 3 [
+ (validate.rules).string = {min_len: 1, max_len: 200},
+ (google.api.field_behavior) = REQUIRED,
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ min_length: 1;
+ max_length: 200;
+ example: "\"MyApp\"";
+ }
+ ];
+ oneof creation_request_type {
+ option (validate.required) = true;
+ CreateOIDCApplicationRequest oidc_request = 4;
+ CreateSAMLApplicationRequest saml_request = 5;
+ CreateAPIApplicationRequest api_request = 6;
+ }
+}
+
+message CreateApplicationResponse {
+ string app_id = 1;
+ // The timestamp of the app creation.
+ google.protobuf.Timestamp creation_date = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2024-12-18T07:50:47.492Z\"";
+ }
+ ];
+
+ oneof creation_response_type {
+ CreateOIDCApplicationResponse oidc_response = 3;
+ CreateSAMLApplicationResponse saml_response = 4;
+ CreateAPIApplicationResponse api_response = 5;
+ }
+}
+
+message CreateOIDCApplicationRequest {
+ // Callback URI of the authorization request where the code or tokens will be sent to
+ repeated string redirect_uris = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"http://localhost:4200/auth/callback\"]";
+ description: "Callback URI of the authorization request where the code or tokens will be sent to";
+ }
+ ];
+ repeated OIDCResponseType response_types = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Determines whether a code, id_token token or just id_token will be returned"
+ }
+ ];
+ repeated OIDCGrantType grant_types = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "The flow type the application uses to gain access";
+ }
+ ];
+ OIDCAppType app_type = 4 [
+ (validate.rules).enum = {defined_only: true},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Determines the paradigm of the application";
+ }
+ ];
+ OIDCAuthMethodType auth_method_type = 5 [
+ (validate.rules).enum = {defined_only: true},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Defines how the application passes login credentials";
+ }
+ ];
+
+ // ZITADEL will redirect to this link after a successful logout
+ repeated string post_logout_redirect_uris = 6 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"http://localhost:4200/signedout\"]";
+ description: "ZITADEL will redirect to this link after a successful logout";
+ }
+ ];
+ OIDCVersion version = 7 [(validate.rules).enum = {defined_only: true}];
+ bool dev_mode = 8 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Used for development, some checks of the OIDC specification will not be checked.";
+ }
+ ];
+ OIDCTokenType access_token_type = 9 [
+ (validate.rules).enum = {defined_only: true},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Type of the access token returned from ZITADEL";
+ }
+ ];
+ bool access_token_role_assertion = 10 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Adds roles to the claims of the access token (only if type == JWT) even if they are not requested by scopes";
+ }
+ ];
+ bool id_token_role_assertion = 11 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Adds roles to the claims of the id token even if they are not requested by scopes";
+ }
+ ];
+ bool id_token_userinfo_assertion = 12 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Claims of profile, email, address and phone scopes are added to the id token even if an access token is issued. Attention this violates the OIDC specification";
+ }
+ ];
+ google.protobuf.Duration clock_skew = 13 [
+ (validate.rules).duration = {gte: {}, lte: {seconds: 5}},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Used to compensate time difference of servers. Duration added to the \"exp\" claim and subtracted from \"iat\", \"auth_time\" and \"nbf\" claims";
+ example: "\"1s\"";
+ }
+ ];
+ repeated string additional_origins = 14 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"scheme://localhost:8080\"]";
+ description: "Additional origins (other than the redirect_uris) from where the API can be used, provided string has to be an origin (scheme://hostname[:port]) without path, query or fragment";
+ }
+ ];
+ bool skip_native_app_success_page = 15 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Skip the successful login page on native apps and directly redirect the user to the callback.";
+ }
+ ];
+ string back_channel_logout_uri = 16 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"https://example.com/auth/backchannel\"]";
+ description: "ZITADEL will use this URI to notify the application about terminated session according to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html)";
+ }
+ ];
+ LoginVersion login_version = 17 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default.";
+ }
+ ];
+}
+
+message CreateOIDCApplicationResponse {
+ string client_id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"1035496534033449\"";
+ description: "generated client id for this config";
+ }
+ ];
+ string client_secret = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"gjoq34589uasgh\"";
+ description: "generated secret for this config";
+ }
+ ];
+ bool none_compliant = 3;
+ repeated OIDCLocalizedMessage compliance_problems = 4;
+}
+
+message CreateSAMLApplicationRequest {
+ oneof metadata {
+ option (validate.required) = true;
+ bytes metadata_xml = 1 [(validate.rules).bytes.max_len = 500000];
+ string metadata_url = 2 [(validate.rules).string.max_len = 200];
+ }
+ LoginVersion login_version = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default.";
+ }
+ ];
+}
+
+message CreateSAMLApplicationResponse {}
+
+message CreateAPIApplicationRequest {
+ APIAuthMethodType auth_method_type = 1 [(validate.rules).enum = {defined_only: true}];
+}
+
+message CreateAPIApplicationResponse {
+ string client_id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"3950723409029374\"";
+ description: "generated secret for this config";
+ }
+ ];
+ string client_secret = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"gjoq34589uasgh\"";
+ description: "generated secret for this config";
+ }
+ ];
+}
+
+message UpdateApplicationRequest {
+ string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string id = 2 [
+ (validate.rules).string = {min_len: 1, max_len: 200},
+ (google.api.field_behavior) = REQUIRED,
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ min_length: 1;
+ max_length: 200;
+ example: "\"45984352431\"";
+ }
+ ];
+ string name = 3 [
+ (validate.rules).string = {max_len: 200},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"MyApplicationName\"";
+ min_length: 1;
+ max_length: 200;
+ }
+ ];
+
+ oneof update_request_type {
+ UpdateSAMLApplicationConfigurationRequest saml_configuration_request = 4;
+ UpdateOIDCApplicationConfigurationRequest oidc_configuration_request = 5;
+ UpdateAPIApplicationConfigurationRequest api_configuration_request = 6;
+ }
+}
+
+message UpdateApplicationResponse {
+ // The timestamp of the app update.
+ google.protobuf.Timestamp change_date = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2024-12-18T07:50:47.492Z\"";
+ }
+ ];
+}
+
+message UpdateSAMLApplicationConfigurationRequest {
+ oneof metadata {
+ option (validate.required) = true;
+ bytes metadata_xml = 1 [(validate.rules).bytes.max_len = 500000];
+ string metadata_url = 2 [(validate.rules).string.max_len = 200];
+ }
+ optional LoginVersion login_version = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default.";
+ }
+ ];
+}
+
+message UpdateOIDCApplicationConfigurationRequest {
+ repeated string redirect_uris = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"http://localhost:4200/auth/callback\"]";
+ description: "Callback URI of the authorization request where the code or tokens will be sent to";
+ }
+ ];
+ repeated OIDCResponseType response_types = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Determines whether a code, id_token token or just id_token will be returned"
+ }
+ ];
+ repeated OIDCGrantType grant_types = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "The flow type the application uses to gain access";
+ }
+ ];
+ optional OIDCAppType app_type = 4 [
+ (validate.rules).enum = {defined_only: true},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Determines the paradigm of the application";
+ }
+ ];
+ optional OIDCAuthMethodType auth_method_type = 5 [
+ (validate.rules).enum = {defined_only: true},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Defines how the application passes login credentials";
+ }
+ ];
+ repeated string post_logout_redirect_uris = 6 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"http://localhost:4200/signedout\"]";
+ description: "ZITADEL will redirect to this link after a successful logout";
+ }
+ ];
+ optional OIDCVersion version = 7 [(validate.rules).enum = {defined_only: true}];
+ optional bool dev_mode = 8 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Used for development, some checks of the OIDC specification will not be checked.";
+ }
+ ];
+ optional OIDCTokenType access_token_type = 9 [
+ (validate.rules).enum = {defined_only: true},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Type of the access token returned from ZITADEL";
+ }
+ ];
+ optional bool access_token_role_assertion = 10 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Adds roles to the claims of the access token (only if type == JWT) even if they are not requested by scopes";
+ }
+ ];
+ optional bool id_token_role_assertion = 11 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Adds roles to the claims of the id token even if they are not requested by scopes";
+ }
+ ];
+ optional bool id_token_userinfo_assertion = 12 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Claims of profile, email, address and phone scopes are added to the id token even if an access token is issued. Attention this violates the OIDC specification";
+ }
+ ];
+ optional google.protobuf.Duration clock_skew = 13 [
+ (validate.rules).duration = {gte: {}, lte: {seconds: 5}},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Used to compensate time difference of servers. Duration added to the \"exp\" claim and subtracted from \"iat\", \"auth_time\" and \"nbf\" claims";
+ example: "\"1s\"";
+ }
+ ];
+ repeated string additional_origins = 14 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"scheme://localhost:8080\"]";
+ description: "Additional origins (other than the redirect_uris) from where the API can be used, provided string has to be an origin (scheme://hostname[:port]) without path, query or fragment";
+ }
+ ];
+ optional bool skip_native_app_success_page = 15 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Skip the successful login page on native apps and directly redirect the user to the callback.";
+ }
+ ];
+ optional string back_channel_logout_uri = 16 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"https://example.com/auth/backchannel\"]";
+ description: "ZITADEL will use this URI to notify the application about terminated session according to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html)";
+ }
+ ];
+ optional LoginVersion login_version = 17 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default.";
+ }
+ ];
+}
+
+message UpdateAPIApplicationConfigurationRequest {
+ APIAuthMethodType auth_method_type = 1 [(validate.rules).enum = {defined_only: true}];
+}
+
+message GetApplicationRequest {
+ string id = 1 [
+ (validate.rules).string = {min_len: 1, max_len: 200},
+ (google.api.field_behavior) = REQUIRED,
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ min_length: 1;
+ max_length: 200;
+ example: "\"45984352431\"";
+ }
+ ];
+}
+
+message GetApplicationResponse {
+ Application app = 1;
+}
+
+message DeleteApplicationRequest {
+ string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message DeleteApplicationResponse {
+ google.protobuf.Timestamp deletion_date = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2025-01-23T10:34:18.051Z\"";
+ }
+ ];
+}
+
+message DeactivateApplicationRequest{
+ string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message DeactivateApplicationResponse{
+ google.protobuf.Timestamp deactivation_date = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2025-01-23T10:34:18.051Z\"";
+ }
+ ];
+}
+
+message ReactivateApplicationRequest{
+ string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message ReactivateApplicationResponse{
+ google.protobuf.Timestamp reactivation_date = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2025-01-23T10:34:18.051Z\"";
+ }
+ ];
+}
+
+message RegenerateClientSecretRequest{
+ string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string application_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ oneof app_type {
+ option (validate.required) = true;
+ bool is_oidc = 3;
+ bool is_api = 4;
+ }
+}
+
+message RegenerateClientSecretResponse{
+ string client_secret = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"gjoq34589uasgh\"";
+ description: "generated secret for the client";
+ }
+ ];
+
+ // The timestamp of the creation of the new client secret
+ google.protobuf.Timestamp creation_date = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2025-01-23T10:34:18.051Z\"";
+ }
+ ];
+}
+
+message ListApplicationsRequest {
+ string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+
+ // Pagination and sorting.
+ zitadel.filter.v2.PaginationRequest pagination = 2;
+
+ //criteria the client is looking for
+ repeated ApplicationSearchFilter filters = 3;
+
+ AppSorting sorting_column = 4;
+}
+
+message ListApplicationsResponse {
+ repeated Application applications = 1;
+
+ // Contains the total number of apps matching the query and the applied limit.
+ zitadel.filter.v2.PaginationResponse pagination = 2;
+}
\ No newline at end of file
diff --git a/proto/zitadel/app/v2beta/login.proto b/proto/zitadel/app/v2beta/login.proto
new file mode 100644
index 0000000000..567b4b5167
--- /dev/null
+++ b/proto/zitadel/app/v2beta/login.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+package zitadel.app.v2beta;
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app";
+
+message LoginVersion {
+ oneof version {
+ LoginV1 login_v1 = 1;
+ LoginV2 login_v2 = 2;
+ }
+}
+
+message LoginV1 {}
+
+message LoginV2 {
+ // Optionally specify a base uri of the login UI. If unspecified the default URI will be used.
+ optional string base_uri = 1;
+}
\ No newline at end of file
diff --git a/proto/zitadel/app/v2beta/oidc.proto b/proto/zitadel/app/v2beta/oidc.proto
new file mode 100644
index 0000000000..7cfd1dcc43
--- /dev/null
+++ b/proto/zitadel/app/v2beta/oidc.proto
@@ -0,0 +1,166 @@
+syntax = "proto3";
+
+package zitadel.app.v2beta;
+import "zitadel/app/v2beta/login.proto";
+import "protoc-gen-openapiv2/options/annotations.proto";
+import "google/protobuf/duration.proto";
+
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app";
+
+message OIDCLocalizedMessage {
+ string key = 1;
+ string localized_message = 2;
+}
+
+enum OIDCResponseType {
+ OIDC_RESPONSE_TYPE_UNSPECIFIED = 0;
+ OIDC_RESPONSE_TYPE_CODE = 1;
+ OIDC_RESPONSE_TYPE_ID_TOKEN = 2;
+ OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN = 3;
+}
+
+enum OIDCGrantType{
+ OIDC_GRANT_TYPE_AUTHORIZATION_CODE = 0;
+ OIDC_GRANT_TYPE_IMPLICIT = 1;
+ OIDC_GRANT_TYPE_REFRESH_TOKEN = 2;
+ OIDC_GRANT_TYPE_DEVICE_CODE = 3;
+ OIDC_GRANT_TYPE_TOKEN_EXCHANGE = 4;
+}
+
+enum OIDCAppType {
+ OIDC_APP_TYPE_WEB = 0;
+ OIDC_APP_TYPE_USER_AGENT = 1;
+ OIDC_APP_TYPE_NATIVE = 2;
+}
+
+enum OIDCAuthMethodType {
+ OIDC_AUTH_METHOD_TYPE_BASIC = 0;
+ OIDC_AUTH_METHOD_TYPE_POST = 1;
+ OIDC_AUTH_METHOD_TYPE_NONE = 2;
+ OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT = 3;
+}
+
+enum OIDCVersion {
+ OIDC_VERSION_1_0 = 0;
+}
+
+enum OIDCTokenType {
+ OIDC_TOKEN_TYPE_BEARER = 0;
+ OIDC_TOKEN_TYPE_JWT = 1;
+}
+
+message OIDCConfig {
+ repeated string redirect_uris = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"https://console.zitadel.ch/auth/callback\"]";
+ description: "Callback URI of the authorization request where the code or tokens will be sent to";
+ }
+ ];
+ repeated OIDCResponseType response_types = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Determines whether a code, id_token token or just id_token will be returned"
+ }
+ ];
+ repeated OIDCGrantType grant_types = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "The flow type the application uses to gain access";
+ }
+ ];
+ OIDCAppType app_type = 4 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "determines the paradigm of the application";
+ }
+ ];
+ string client_id = 5 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"69629023906488334@ZITADEL\"";
+ description: "generated oauth2/oidc client id";
+ }
+ ];
+ OIDCAuthMethodType auth_method_type = 6 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "defines how the application passes login credentials";
+ }
+ ];
+ repeated string post_logout_redirect_uris = 7 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"https://console.zitadel.ch/logout\"]";
+ description: "ZITADEL will redirect to this link after a successful logout";
+ }
+ ];
+ OIDCVersion version = 8 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "the OIDC version used by the application";
+ }
+ ];
+ bool none_compliant = 9 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "specifies whether the config is OIDC compliant. A production configuration SHOULD be compliant";
+ }
+ ];
+ repeated OIDCLocalizedMessage compliance_problems = 10 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "lists the problems for non-compliancy";
+ }
+ ];
+ bool dev_mode = 11 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "used for development";
+ }
+ ];
+ OIDCTokenType access_token_type = 12 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "type of the access token returned from ZITADEL";
+ }
+ ];
+ bool access_token_role_assertion = 13 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "adds roles to the claims of the access token (only if type == JWT) even if they are not requested by scopes";
+ }
+ ];
+ bool id_token_role_assertion = 14 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "adds roles to the claims of the id token even if they are not requested by scopes";
+ }
+ ];
+ bool id_token_userinfo_assertion = 15 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "claims of profile, email, address and phone scopes are added to the id token even if an access token is issued. Attention this violates the OIDC specification";
+ }
+ ];
+ google.protobuf.Duration clock_skew = 16 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Used to compensate time difference of servers. Duration added to the \"exp\" claim and subtracted from \"iat\", \"auth_time\" and \"nbf\" claims";
+ // min: "0s";
+ // max: "5s";
+ }
+ ];
+ repeated string additional_origins = 17 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"https://console.zitadel.ch/auth/callback\"]";
+ description: "additional origins (other than the redirect_uris) from where the API can be used";
+ }
+ ];
+ repeated string allowed_origins = 18 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"https://console.zitadel.ch/auth/callback\"]";
+ description: "all allowed origins from where the API can be used";
+ }
+ ];
+ bool skip_native_app_success_page = 19 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Skip the successful login page on native apps and directly redirect the user to the callback.";
+ }
+ ];
+ string back_channel_logout_uri = 20 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"https://example.com/auth/backchannel\"]";
+ description: "ZITADEL will use this URI to notify the application about terminated session according to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html)";
+ }
+ ];
+ LoginVersion login_version = 21 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default.";
+ }
+ ];
+}
\ No newline at end of file
diff --git a/proto/zitadel/app/v2beta/saml.proto b/proto/zitadel/app/v2beta/saml.proto
new file mode 100644
index 0000000000..7c85447880
--- /dev/null
+++ b/proto/zitadel/app/v2beta/saml.proto
@@ -0,0 +1,20 @@
+syntax = "proto3";
+
+package zitadel.app.v2beta;
+
+import "zitadel/app/v2beta/login.proto";
+import "protoc-gen-openapiv2/options/annotations.proto";
+
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app";
+
+message SAMLConfig {
+ oneof metadata{
+ bytes metadata_xml = 1;
+ string metadata_url = 2;
+ }
+ LoginVersion login_version = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default.";
+ }
+ ];
+}
\ No newline at end of file
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index d633fbe8c5..74d5dcf60b 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -3287,6 +3287,7 @@ service ManagementService {
};
}
+ // Deprecated: Use [GetApplication](/apis/resources/application_service_v2/application-service-get-application.api.mdx) instead to fetch an app
rpc GetAppByID(GetAppByIDRequest) returns (GetAppByIDResponse) {
option (google.api.http) = {
get: "/projects/{project_id}/apps/{app_id}"
@@ -3309,9 +3310,11 @@ service ManagementService {
required: false;
};
};
+ deprecated: true;
};
}
+ // Deprecated: Use [ListApplications](/apis/resources/application_service_v2/application-service-list-applications.api.mdx) instead to list applications
rpc ListApps(ListAppsRequest) returns (ListAppsResponse) {
option (google.api.http) = {
post: "/projects/{project_id}/apps/_search"
@@ -3335,6 +3338,7 @@ service ManagementService {
required: false;
};
};
+ deprecated: true;
};
}
@@ -3363,6 +3367,7 @@ service ManagementService {
};
}
+ // Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/application-service-create-application.api.mdx) instead to create an OIDC application
rpc AddOIDCApp(AddOIDCAppRequest) returns (AddOIDCAppResponse) {
option (google.api.http) = {
post: "/projects/{project_id}/apps/oidc"
@@ -3386,62 +3391,74 @@ service ManagementService {
required: false;
};
};
+ deprecated: true;
};
}
- rpc AddSAMLApp(AddSAMLAppRequest) returns (AddSAMLAppResponse) {
- option (google.api.http) = {
- post: "/projects/{project_id}/apps/saml"
- body: "*"
- };
-
- option (zitadel.v1.auth_option) = {
- permission: "project.app.write"
- check_field_name: "ProjectId"
- };
-
- option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
- tags: "Applications";
- summary: "Create Application (SAML)";
- description: "Create a new SAML client. Returns an entity ID"
- parameters: {
- headers: {
- name: "x-zitadel-orgid";
- description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data.";
- type: STRING,
- required: false;
- };
- };
- };
- }
-
- rpc AddAPIApp(AddAPIAppRequest) returns (AddAPIAppResponse) {
- option (google.api.http) = {
- post: "/projects/{project_id}/apps/api"
- body: "*"
- };
-
- option (zitadel.v1.auth_option) = {
- permission: "project.app.write"
- check_field_name: "ProjectId"
+ // Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/application-service-create-application.api.mdx) instead to create a SAML application
+ rpc AddSAMLApp(AddSAMLAppRequest) returns (AddSAMLAppResponse) {
+ option (google.api.http) = {
+ post: "/projects/{project_id}/apps/saml"
+ body: "*"
};
- option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
- tags: "Applications";
- summary: "Create Application (API)";
- description: "Create a new API client. The client id will be generated and returned in the response. Depending on the chosen configuration also a secret will be generated and returned."
- parameters: {
- headers: {
- name: "x-zitadel-orgid";
- description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data.";
- type: STRING,
- required: false;
- };
- };
- };
+ option (zitadel.v1.auth_option) = {
+ permission: "project.app.write"
+ check_field_name: "ProjectId"
+ };
+
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ tags: "Applications";
+ summary: "Create Application (SAML)";
+ description: "Create a new SAML client. Returns an entity ID"
+ parameters: {
+ headers: {
+ name: "x-zitadel-orgid";
+ description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data.";
+ type: STRING,
+ required: false;
+ };
+ };
+ deprecated: true;
+ };
+ }
+
+ // Create Application (API)
+ //
+ // Create a new API client. The client id will be generated and returned in the response.
+ // Depending on the chosen configuration also a secret will be generated and returned.
+ //
+ // Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/application-service-create-application.api.mdx) instead to create an API application
+ rpc AddAPIApp(AddAPIAppRequest) returns (AddAPIAppResponse) {
+ option (google.api.http) = {
+ post: "/projects/{project_id}/apps/api"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "project.app.write"
+ check_field_name: "ProjectId"
+ };
+
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ tags: "Applications";
+ summary: "Create Application (API)";
+ description: "Create a new API client. The client id will be generated and returned in the response. Depending on the chosen configuration also a secret will be generated and returned."
+ parameters: {
+ headers: {
+ name: "x-zitadel-orgid";
+ description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data.";
+ type: STRING,
+ required: false;
+ };
+ };
+ deprecated: true;
+ };
}
// Changes application
+ //
+ // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the generic params of an app
rpc UpdateApp(UpdateAppRequest) returns (UpdateAppResponse) {
option (google.api.http) = {
put: "/projects/{project_id}/apps/{app_id}"
@@ -3465,9 +3482,11 @@ service ManagementService {
required: false;
};
};
+ deprecated: true;
};
}
+ // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the config of an OIDC app
rpc UpdateOIDCAppConfig(UpdateOIDCAppConfigRequest) returns (UpdateOIDCAppConfigResponse) {
option (google.api.http) = {
put: "/projects/{project_id}/apps/{app_id}/oidc_config"
@@ -3491,61 +3510,67 @@ service ManagementService {
required: false;
};
};
+ deprecated: true
};
}
- rpc UpdateSAMLAppConfig(UpdateSAMLAppConfigRequest) returns (UpdateSAMLAppConfigResponse) {
- option (google.api.http) = {
- put: "/projects/{project_id}/apps/{app_id}/saml_config"
- body: "*"
- };
-
- option (zitadel.v1.auth_option) = {
- permission: "project.app.write"
- check_field_name: "ProjectId"
- };
-
- option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
- tags: "Applications";
- summary: "Update SAML Application Config";
- description: "Update the SAML specific configuration of an application."
- parameters: {
- headers: {
- name: "x-zitadel-orgid";
- description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data.";
- type: STRING,
- required: false;
- };
- };
- };
- }
-
- rpc UpdateAPIAppConfig(UpdateAPIAppConfigRequest) returns (UpdateAPIAppConfigResponse) {
- option (google.api.http) = {
- put: "/projects/{project_id}/apps/{app_id}/api_config"
- body: "*"
- };
+ // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the config of a SAML app
+ rpc UpdateSAMLAppConfig(UpdateSAMLAppConfigRequest) returns (UpdateSAMLAppConfigResponse) {
+ option (google.api.http) = {
+ put: "/projects/{project_id}/apps/{app_id}/saml_config"
+ body: "*"
+ };
option (zitadel.v1.auth_option) = {
- permission: "project.app.write"
- check_field_name: "ProjectId"
+ permission: "project.app.write"
+ check_field_name: "ProjectId"
};
- option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
- tags: "Applications";
- summary: "Update API Application Config";
- description: "Update the OIDC-specific configuration of an application."
- parameters: {
- headers: {
- name: "x-zitadel-orgid";
- description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data.";
- type: STRING,
- required: false;
- };
- };
- };
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ tags: "Applications";
+ summary: "Update SAML Application Config";
+ description: "Update the SAML specific configuration of an application."
+ parameters: {
+ headers: {
+ name: "x-zitadel-orgid";
+ description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data.";
+ type: STRING,
+ required: false;
+ };
+ };
+ deprecated: true;
+ };
}
+ // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the config of an API app
+ rpc UpdateAPIAppConfig(UpdateAPIAppConfigRequest) returns (UpdateAPIAppConfigResponse) {
+ option (google.api.http) = {
+ put: "/projects/{project_id}/apps/{app_id}/api_config"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "project.app.write"
+ check_field_name: "ProjectId"
+ };
+
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ tags: "Applications";
+ summary: "Update API Application Config";
+ description: "Update the OIDC-specific configuration of an application."
+ parameters: {
+ headers: {
+ name: "x-zitadel-orgid";
+ description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data.";
+ type: STRING,
+ required: false;
+ };
+ };
+ deprecated: true;
+ };
+ }
+
+ // Deprecated: Use [DeactivateApplication](/apis/resources/application_service_v2/application-service-deactivate-application.api.mdx) instead to deactivate an app
rpc DeactivateApp(DeactivateAppRequest) returns (DeactivateAppResponse) {
option (google.api.http) = {
post: "/projects/{project_id}/apps/{app_id}/_deactivate"
@@ -3569,9 +3594,11 @@ service ManagementService {
required: false;
};
};
+ deprecated: true;
};
}
+ // Deprecated: Use [ReactivateApplication](/apis/resources/application_service_v2/application-service-reactivate-application.api.mdx) instead to reactivate an app
rpc ReactivateApp(ReactivateAppRequest) returns (ReactivateAppResponse) {
option (google.api.http) = {
post: "/projects/{project_id}/apps/{app_id}/_reactivate"
@@ -3595,9 +3622,11 @@ service ManagementService {
required: false;
};
};
+ deprecated: true;
};
}
+ // Deprecated: Use [DeleteApplication](/apis/resources/application_service_v2/application-service-delete-application.api.mdx) instead to delete an app
rpc RemoveApp(RemoveAppRequest) returns (RemoveAppResponse) {
option (google.api.http) = {
delete: "/projects/{project_id}/apps/{app_id}"
@@ -3620,9 +3649,11 @@ service ManagementService {
required: false;
};
};
+ deprecated: true;
};
}
+ // Deprecated: Use [RegenerateClientSecret](/apis/resources/application_service_v2/application-service-regenerate-client-secret.api.mdx) instead to regenerate an OIDC app client secret
rpc RegenerateOIDCClientSecret(RegenerateOIDCClientSecretRequest) returns (RegenerateOIDCClientSecretResponse) {
option (google.api.http) = {
post: "/projects/{project_id}/apps/{app_id}/oidc_config/_generate_client_secret"
@@ -3646,9 +3677,11 @@ service ManagementService {
required: false;
};
};
+ deprecated: true;
};
}
+ // Deprecated: Use [RegenerateClientSecret](/apis/resources/application_service_v2/application-service-regenerate-client-secret.api.mdx) instead to regenerate an API app client secret
rpc RegenerateAPIClientSecret(RegenerateAPIClientSecretRequest) returns (RegenerateAPIClientSecretResponse) {
option (google.api.http) = {
post: "/projects/{project_id}/apps/{app_id}/api_config/_generate_client_secret"
@@ -3672,6 +3705,7 @@ service ManagementService {
required: false;
};
};
+ deprecated: true;
};
}