feat(api): move application service v2beta to GA (and deprecate v2beta) (#10846)

# Which Problems Are Solved

As part of our efforts to simplify the structure and versions of our
APIs, were moving all existing v2beta endpoints to v2 and deprecate
them. They will be removed in Zitadel V5.

# How the Problems Are Solved

- This PR moves app v2beta service and its endpoints to a corresponding
to application v2 version. The v2beta service and endpoints are
deprecated.
- The comments and have been improved and, where not already done, moved
from swagger annotations to proto.
- All required fields have been marked with (google.api.field_behavior)
= REQUIRED and validation rules have been added where missing.
- Name ID of the application always `application_id`, previously was
also `id` and `app_id`.
- Get rid of all `app` abbreviations and name it `application` including
the service name, `AppState` -> `ApplicationState` and `AppSorting` ->
`ApplicationSorting`
- Updated `CreateApplicationRequest`:
- renamed `creation_request_type` to `application_type` and all its
options to `XY_configuration` instead of `XY_request`
- `RegenerateClientSecret`
  - renamed method to `GenerateClientSecret`
  - removed `app_type` from request
- `ListApplicationRequest`:
  - removed required `project_id` and provided it as a filter
- Type `ApplicationNameQuery` has been renamed to
`ApplicationNameFilter` as its usage in the request
- Renamed all fields and types from `config` to `configuration`
- Updated `DeleteApplicationKeyRequest`
  - removed `organization_id`
- Updated `GetApplicationKeyRequest`:
  - removed `project_id`, `application_id` and `organization_id``
- Updated `ListApplicationKeysRequest`:
  - removed oneOf `resource_id` and moved the options into filters
- Name ID of the application key always `key_id`.
- removed unnecessary package prefixed (`zitadel.application.v2`)
- formatted using `buf`

# Additional Changes

None

# Additional Context

- part of https://github.com/zitadel/zitadel/issues/10772
- requires backport to v4.x
This commit is contained in:
Livio Spring
2025-10-17 10:12:20 +02:00
committed by GitHub
parent 7e11f7a032
commit 0281670030
36 changed files with 7475 additions and 31 deletions

View File

@@ -38,7 +38,8 @@ import (
action_v2 "github.com/zitadel/zitadel/internal/api/grpc/action/v2"
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"
app_v2beta "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta"
application "github.com/zitadel/zitadel/internal/api/grpc/application/v2"
"github.com/zitadel/zitadel/internal/api/grpc/auth"
authorization_v2beta "github.com/zitadel/zitadel/internal/api/grpc/authorization/v2beta"
feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2"
@@ -558,7 +559,10 @@ func startAPIs(
if err := apis.RegisterService(ctx, authorization_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, app.CreateServer(commands, queries, permissionCheck)); err != nil {
if err := apis.RegisterService(ctx, app_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, application.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, group_v2.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil {

View File

@@ -13,6 +13,7 @@ plugins:
out: .artifacts/openapi3
strategy: all
opt:
- allow-get
- short-service-tags
- ignore-googleapi-http
- base=base.yaml

View File

@@ -401,7 +401,7 @@ module.exports = {
},
},
application_v2: {
specPath: ".artifacts/openapi3/zitadel/app/v2beta/app_service.openapi.yaml",
specPath: ".artifacts/openapi3/zitadel/application/v2/application_service.openapi.yaml",
outputDir: "docs/apis/resources/application_service_v2",
sidebarOptions: {
groupPathsBy: "tag",

View File

@@ -19,7 +19,7 @@ const sidebar_api_webkey_service_v2 = require("./docs/apis/resources/webkey_serv
const sidebar_api_instance_service_v2 = require("./docs/apis/resources/instance_service_v2/sidebar.ts").default
const sidebar_api_authorization_service_v2 = require("./docs/apis/resources/authorization_service_v2/sidebar.ts").default
const sidebar_api_internal_permission_service_v2 = require("./docs/apis/resources/internal_permission_service_v2/sidebar.ts").default
const sidebar_api_app_v2 = require("./docs/apis/resources/application_service_v2/sidebar.ts").default
const sidebar_api_application_v2 = require("./docs/apis/resources/application_service_v2/sidebar.ts").default
module.exports = {
guides: [
@@ -874,19 +874,17 @@ module.exports = {
},
{
type: "category",
label: "App (Beta)",
label: "Application",
link: {
type: "generated-index",
title: "Application Service API (Beta)",
title: "Application Service API",
slug: "/apis/resources/application_service_v2",
description:
"This API lets you manage Zitadel applications (API, SAML, OIDC).\n" +
"\n" +
"The API offers generic endpoints that work for all app types (API, SAML, OIDC), " +
"\n" +
"This API is in beta state. It can AND will continue breaking until a stable version is released.\n"
"The API offers generic endpoints that work for all app types (API, SAML, OIDC), "
},
items: sidebar_api_app_v2,
items: sidebar_api_application_v2,
},
{
type: "category",

View File

@@ -0,0 +1,48 @@
package app
import (
"context"
"strings"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/application/v2/convert"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
)
func (s *Server) CreateApplicationKey(ctx context.Context, req *connect.Request[application.CreateApplicationKeyRequest]) (*connect.Response[application.CreateApplicationKeyResponse], error) {
domainReq := convert.CreateAPIClientKeyRequestToDomain(req.Msg)
appKey, err := s.command.AddApplicationKey(ctx, domainReq, "")
if err != nil {
return nil, err
}
keyDetails, err := appKey.Detail()
if err != nil {
return nil, err
}
return connect.NewResponse(&application.CreateApplicationKeyResponse{
KeyId: appKey.KeyID,
CreationDate: timestamppb.New(appKey.ChangeDate),
KeyDetails: keyDetails,
}), nil
}
func (s *Server) DeleteApplicationKey(ctx context.Context, req *connect.Request[application.DeleteApplicationKeyRequest]) (*connect.Response[application.DeleteApplicationKeyResponse], error) {
deletionDetails, err := s.command.RemoveApplicationKey(ctx,
strings.TrimSpace(req.Msg.GetProjectId()),
strings.TrimSpace(req.Msg.GetApplicationId()),
strings.TrimSpace(req.Msg.GetKeyId()),
"",
)
if err != nil {
return nil, err
}
return connect.NewResponse(&application.DeleteApplicationKeyResponse{
DeletionDate: timestamppb.New(deletionDetails.EventDate),
}), nil
}

View File

@@ -0,0 +1,191 @@
package app
import (
"context"
"strings"
"time"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/application/v2/convert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
)
func (s *Server) CreateApplication(ctx context.Context, req *connect.Request[application.CreateApplicationRequest]) (*connect.Response[application.CreateApplicationResponse], error) {
switch t := req.Msg.GetApplicationType().(type) {
case *application.CreateApplicationRequest_ApiConfiguration:
apiApp, err := s.command.AddAPIApplication(ctx, convert.CreateAPIApplicationRequestToDomain(req.Msg.GetName(), req.Msg.GetProjectId(), req.Msg.GetApplicationId(), t.ApiConfiguration), "")
if err != nil {
return nil, err
}
return connect.NewResponse(&application.CreateApplicationResponse{
ApplicationId: apiApp.AppID,
CreationDate: timestamppb.New(apiApp.ChangeDate),
ApplicationType: &application.CreateApplicationResponse_ApiConfiguration{
ApiConfiguration: &application.CreateAPIApplicationResponse{
ClientId: apiApp.ClientID,
ClientSecret: apiApp.ClientSecretString,
},
},
}), nil
case *application.CreateApplicationRequest_OidcConfiguration:
oidcAppRequest, err := convert.CreateOIDCAppRequestToDomain(req.Msg.GetName(), req.Msg.GetProjectId(), req.Msg.GetOidcConfiguration())
if err != nil {
return nil, err
}
oidcApp, err := s.command.AddOIDCApplication(ctx, oidcAppRequest, "")
if err != nil {
return nil, err
}
return connect.NewResponse(&application.CreateApplicationResponse{
ApplicationId: oidcApp.AppID,
CreationDate: timestamppb.New(oidcApp.ChangeDate),
ApplicationType: &application.CreateApplicationResponse_OidcConfiguration{
OidcConfiguration: &application.CreateOIDCApplicationResponse{
ClientId: oidcApp.ClientID,
ClientSecret: oidcApp.ClientSecretString,
NonCompliant: oidcApp.Compliance.NoneCompliant,
ComplianceProblems: convert.ComplianceProblemsToLocalizedMessages(oidcApp.Compliance.Problems),
},
},
}), nil
case *application.CreateApplicationRequest_SamlConfiguration:
samlAppRequest, err := convert.CreateSAMLAppRequestToDomain(req.Msg.GetName(), req.Msg.GetProjectId(), req.Msg.GetSamlConfiguration())
if err != nil {
return nil, err
}
samlApp, err := s.command.AddSAMLApplication(ctx, samlAppRequest, "")
if err != nil {
return nil, err
}
return connect.NewResponse(&application.CreateApplicationResponse{
ApplicationId: samlApp.AppID,
CreationDate: timestamppb.New(samlApp.ChangeDate),
ApplicationType: &application.CreateApplicationResponse_SamlConfiguration{
SamlConfiguration: &application.CreateSAMLApplicationResponse{},
},
}), nil
default:
return nil, zerrors.ThrowInvalidArgument(nil, "APP-0iiN46", "unknown app type")
}
}
func (s *Server) UpdateApplication(ctx context.Context, req *connect.Request[application.UpdateApplicationRequest]) (*connect.Response[application.UpdateApplicationResponse], error) {
var changedTime time.Time
if name := strings.TrimSpace(req.Msg.GetName()); name != "" {
updatedDetails, err := s.command.UpdateApplicationName(
ctx,
req.Msg.GetProjectId(),
&domain.ChangeApp{
AppID: req.Msg.GetApplicationId(),
AppName: name,
},
"",
)
if err != nil {
return nil, err
}
changedTime = updatedDetails.EventDate
}
switch t := req.Msg.GetApplicationType().(type) {
case *application.UpdateApplicationRequest_ApiConfiguration:
updatedAPIApp, err := s.command.UpdateAPIApplication(ctx, convert.UpdateAPIApplicationConfigurationRequestToDomain(req.Msg.GetApplicationId(), req.Msg.GetProjectId(), t.ApiConfiguration), "")
if err != nil {
return nil, err
}
changedTime = updatedAPIApp.ChangeDate
case *application.UpdateApplicationRequest_OidcConfiguration:
oidcApp, err := convert.UpdateOIDCAppConfigRequestToDomain(req.Msg.GetApplicationId(), req.Msg.GetProjectId(), t.OidcConfiguration)
if err != nil {
return nil, err
}
updatedOIDCApp, err := s.command.UpdateOIDCApplication(ctx, oidcApp, "")
if err != nil {
return nil, err
}
changedTime = updatedOIDCApp.ChangeDate
case *application.UpdateApplicationRequest_SamlConfiguration:
samlApp, err := convert.UpdateSAMLAppConfigRequestToDomain(req.Msg.GetApplicationId(), req.Msg.GetProjectId(), t.SamlConfiguration)
if err != nil {
return nil, err
}
updatedSAMLApp, err := s.command.UpdateSAMLApplication(ctx, samlApp, "")
if err != nil {
return nil, err
}
changedTime = updatedSAMLApp.ChangeDate
}
return connect.NewResponse(&application.UpdateApplicationResponse{
ChangeDate: timestamppb.New(changedTime),
}), nil
}
func (s *Server) DeleteApplication(ctx context.Context, req *connect.Request[application.DeleteApplicationRequest]) (*connect.Response[application.DeleteApplicationResponse], error) {
details, err := s.command.RemoveApplication(ctx, req.Msg.GetProjectId(), req.Msg.GetApplicationId(), "")
if err != nil {
return nil, err
}
return connect.NewResponse(&application.DeleteApplicationResponse{
DeletionDate: timestamppb.New(details.EventDate),
}), nil
}
func (s *Server) DeactivateApplication(ctx context.Context, req *connect.Request[application.DeactivateApplicationRequest]) (*connect.Response[application.DeactivateApplicationResponse], error) {
details, err := s.command.DeactivateApplication(ctx, req.Msg.GetProjectId(), req.Msg.GetApplicationId(), "")
if err != nil {
return nil, err
}
return connect.NewResponse(&application.DeactivateApplicationResponse{
DeactivationDate: timestamppb.New(details.EventDate),
}), nil
}
func (s *Server) ReactivateApplication(ctx context.Context, req *connect.Request[application.ReactivateApplicationRequest]) (*connect.Response[application.ReactivateApplicationResponse], error) {
details, err := s.command.ReactivateApplication(ctx, req.Msg.GetProjectId(), req.Msg.GetApplicationId(), "")
if err != nil {
return nil, err
}
return connect.NewResponse(&application.ReactivateApplicationResponse{
ReactivationDate: timestamppb.New(details.EventDate),
}), nil
}
func (s *Server) GenerateClientSecret(ctx context.Context, req *connect.Request[application.GenerateClientSecretRequest]) (*connect.Response[application.GenerateClientSecretResponse], error) {
var secret string
var changeDate time.Time
secret, changeDate, err := s.command.ChangeApplicationSecret(ctx, req.Msg.GetProjectId(), req.Msg.GetApplicationId(), "")
if err != nil {
return nil, err
}
return connect.NewResponse(&application.GenerateClientSecretResponse{
ClientSecret: secret,
CreationDate: timestamppb.New(changeDate),
}), nil
}

View File

@@ -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"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
)
func CreateAPIApplicationRequestToDomain(name, projectID, appID string, app *application.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 *application.UpdateAPIApplicationConfigurationRequest) *domain.APIApp {
return &domain.APIApp{
ObjectRoot: models.ObjectRoot{
AggregateID: projectID,
},
AppID: appID,
AuthMethodType: apiAuthMethodTypeToDomain(app.GetAuthMethodType()),
}
}
func appAPIConfigToPb(apiApp *query.APIApp) application.IsApplicationConfiguration {
return &application.Application_ApiConfiguration{
ApiConfiguration: &application.APIConfiguration{
ClientId: apiApp.ClientID,
AuthMethodType: apiAuthMethodTypeToPb(apiApp.AuthMethodType),
},
}
}
func apiAuthMethodTypeToDomain(authType application.APIAuthMethodType) domain.APIAuthMethodType {
switch authType {
case application.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC:
return domain.APIAuthMethodTypeBasic
case application.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT:
return domain.APIAuthMethodTypePrivateKeyJWT
default:
return domain.APIAuthMethodTypeBasic
}
}
func apiAuthMethodTypeToPb(methodType domain.APIAuthMethodType) application.APIAuthMethodType {
switch methodType {
case domain.APIAuthMethodTypeBasic:
return application.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC
case domain.APIAuthMethodTypePrivateKeyJWT:
return application.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT
default:
return application.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC
}
}

View File

@@ -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"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
)
func TestCreateAPIApplicationRequestToDomain(t *testing.T) {
t.Parallel()
tests := []struct {
name string
appName string
projectID string
appID string
req *application.CreateAPIApplicationRequest
want *domain.APIApp
}{
{
name: "basic auth method",
appName: "my-application",
projectID: "proj-1",
appID: "someID",
req: &application.CreateAPIApplicationRequest{
AuthMethodType: application.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
},
want: &domain.APIApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
AppName: "my-application",
AuthMethodType: domain.APIAuthMethodTypeBasic,
AppID: "someID",
},
},
{
name: "private key jwt",
appName: "jwt-application",
projectID: "proj-2",
req: &application.CreateAPIApplicationRequest{
AuthMethodType: application.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
},
want: &domain.APIApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"},
AppName: "jwt-application",
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 *application.UpdateAPIApplicationConfigurationRequest
want *domain.APIApp
}{
{
name: "basic auth method",
appID: "application-1",
projectID: "proj-1",
req: &application.UpdateAPIApplicationConfigurationRequest{
AuthMethodType: application.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
},
want: &domain.APIApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
AppID: "application-1",
AuthMethodType: domain.APIAuthMethodTypeBasic,
},
},
{
name: "private key jwt",
appID: "application-2",
projectID: "proj-2",
req: &application.UpdateAPIApplicationConfigurationRequest{
AuthMethodType: application.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
},
want: &domain.APIApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"},
AppID: "application-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 application.APIAuthMethodType
}{
{
name: "basic auth method",
methodType: domain.APIAuthMethodTypeBasic,
expectedResult: application.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
},
{
name: "private key jwt",
methodType: domain.APIAuthMethodTypePrivateKeyJWT,
expectedResult: application.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
},
{
name: "unknown auth method defaults to basic",
expectedResult: application.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)
})
}
}

View File

@@ -0,0 +1,273 @@
package convert
import (
"net/url"
"strings"
"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/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
)
func AppToPb(queryApp *query.App) *application.Application {
if queryApp == nil {
return &application.Application{}
}
return &application.Application{
ApplicationId: queryApp.ID,
CreationDate: timestamppb.New(queryApp.CreationDate),
ChangeDate: timestamppb.New(queryApp.ChangeDate),
State: appStateToPb(queryApp.State),
Name: queryApp.Name,
Configuration: appConfigToPb(queryApp),
}
}
func AppsToPb(queryApps []*query.App) []*application.Application {
pbApps := make([]*application.Application, len(queryApps))
for i, queryApp := range queryApps {
pbApps[i] = AppToPb(queryApp)
}
return pbApps
}
func ListApplicationsRequestToModel(sysDefaults systemdefaults.SystemDefaults, req *application.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
}
return &query.AppSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: appSortingToColumn(req.GetSortingColumn()),
},
Queries: queries,
}, nil
}
func appSortingToColumn(sortingCriteria application.ApplicationSorting) query.Column {
switch sortingCriteria {
case application.ApplicationSorting_APPLICATION_SORT_BY_CHANGE_DATE:
return query.AppColumnChangeDate
case application.ApplicationSorting_APPLICATION_SORT_BY_CREATION_DATE:
return query.AppColumnCreationDate
case application.ApplicationSorting_APPLICATION_SORT_BY_NAME:
return query.AppColumnName
case application.ApplicationSorting_APPLICATION_SORT_BY_STATE:
return query.AppColumnState
case application.ApplicationSorting_APPLICATION_SORT_BY_ID:
fallthrough
default:
return query.AppColumnID
}
}
func appStateToPb(state domain.AppState) application.ApplicationState {
switch state {
case domain.AppStateActive:
return application.ApplicationState_APPLICATION_STATE_ACTIVE
case domain.AppStateInactive:
return application.ApplicationState_APPLICATION_STATE_INACTIVE
case domain.AppStateRemoved:
return application.ApplicationState_APPLICATION_STATE_REMOVED
case domain.AppStateUnspecified:
fallthrough
default:
return application.ApplicationState_APPLICATION_STATE_UNSPECIFIED
}
}
func appConfigToPb(app *query.App) application.IsApplicationConfiguration {
if app.OIDCConfig != nil {
return appOIDCConfigToPb(app.OIDCConfig)
}
if app.SAMLConfig != nil {
return appSAMLConfigToPb(app.SAMLConfig)
}
return appAPIConfigToPb(app.APIConfig)
}
func loginVersionToDomain(version *application.LoginVersion) (*domain.LoginVersion, *string, error) {
switch v := version.GetVersion().(type) {
case nil:
return gu.Ptr(domain.LoginVersionUnspecified), gu.Ptr(""), nil
case *application.LoginVersion_LoginV1:
return gu.Ptr(domain.LoginVersion1), gu.Ptr(""), nil
case *application.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) *application.LoginVersion {
switch version {
case domain.LoginVersionUnspecified:
return nil
case domain.LoginVersion1:
return &application.LoginVersion{Version: &application.LoginVersion_LoginV1{LoginV1: &application.LoginV1{}}}
case domain.LoginVersion2:
return &application.LoginVersion{Version: &application.LoginVersion_LoginV2{LoginV2: &application.LoginV2{BaseUri: baseURI}}}
default:
return nil
}
}
func appQueriesToModel(filters []*application.ApplicationSearchFilter) (queries []query.SearchQuery, err error) {
queries = make([]query.SearchQuery, len(filters))
for i, f := range filters {
queries[i], err = applicationFilterToQuery(f)
if err != nil {
return nil, err
}
}
return queries, nil
}
func applicationFilterToQuery(applicationFilter *application.ApplicationSearchFilter) (query.SearchQuery, error) {
switch q := applicationFilter.GetFilter().(type) {
case *application.ApplicationSearchFilter_ProjectIdFilter:
return query.NewAppProjectIDSearchQuery(q.ProjectIdFilter.GetProjectId())
case *application.ApplicationSearchFilter_NameFilter:
return query.NewAppNameSearchQuery(filter.TextMethodPbToQuery(q.NameFilter.GetMethod()), q.NameFilter.GetName())
case *application.ApplicationSearchFilter_StateFilter:
return query.NewAppStateSearchQuery(domain.AppState(q.StateFilter))
case *application.ApplicationSearchFilter_TypeFilter:
return applicationTypeFilterToQuery(q.TypeFilter)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid")
}
}
func applicationTypeFilterToQuery(t application.ApplicationType) (*query.NotNullQuery, error) {
switch t {
case application.ApplicationType_APPLICATION_TYPE_OIDC:
return query.NewNotNullQuery(query.AppOIDCConfigColumnAppID)
case application.ApplicationType_APPLICATION_TYPE_API:
return query.NewNotNullQuery(query.AppAPIConfigColumnAppID)
case application.ApplicationType_APPLICATION_TYPE_SAML:
return query.NewNotNullQuery(query.AppSAMLConfigColumnAppID)
case application.ApplicationType_APPLICATION_TYPE_UNSPECIFIED:
return nil, zerrors.ThrowInvalidArgument(nil, "CONV-Jke83s", "List.Query.Invalid")
default:
return nil, zerrors.ThrowInvalidArgument(nil, "CONV-Skj3q", "List.Query.Invalid")
}
}
func CreateAPIClientKeyRequestToDomain(key *application.CreateApplicationKeyRequest) *domain.ApplicationKey {
return &domain.ApplicationKey{
ObjectRoot: models.ObjectRoot{
AggregateID: strings.TrimSpace(key.GetProjectId()),
},
ExpirationDate: key.GetExpirationDate().AsTime(),
Type: domain.AuthNKeyTypeJSON,
ApplicationID: strings.TrimSpace(key.GetApplicationId()),
}
}
func ListApplicationKeysRequestToDomain(sysDefaults systemdefaults.SystemDefaults, req *application.ListApplicationKeysRequest) (*query.AuthNKeySearchQueries, error) {
var queries []query.SearchQuery
queries, err := ApplicationKeySearchQueriesToQuery(req.GetFilters())
if err != nil {
return nil, err
}
offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination())
if err != nil {
return nil, err
}
return &query.AuthNKeySearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: appKeysSortingToColumn(req.GetSortingColumn()),
},
Queries: queries,
}, nil
}
func ApplicationKeySearchQueriesToQuery(queries []*application.ApplicationKeySearchFilter) (_ []query.SearchQuery, err error) {
searchQueries := make([]query.SearchQuery, len(queries))
for i, query := range queries {
searchQueries[i], err = ApplicationKeySearchFilterToQuery(query)
if err != nil {
return nil, err
}
}
return searchQueries, nil
}
func ApplicationKeySearchFilterToQuery(searchQuery *application.ApplicationKeySearchFilter) (query.SearchQuery, error) {
switch f := searchQuery.GetFilter().(type) {
case *application.ApplicationKeySearchFilter_ApplicationIdFilter:
return query.NewAuthNKeyObjectIDQuery(strings.TrimSpace(f.ApplicationIdFilter.GetApplicationId()))
case *application.ApplicationKeySearchFilter_OrganizationIdFilter:
return query.NewAuthNKeyResourceOwnerQuery(strings.TrimSpace(f.OrganizationIdFilter.GetOrganizationId()))
case *application.ApplicationKeySearchFilter_ProjectIdFilter:
return query.NewAuthNKeyAggregateIDQuery(strings.TrimSpace(f.ProjectIdFilter.GetProjectId()))
default:
return nil, zerrors.ThrowInvalidArgument(nil, "CONV-t3ENme", "List.Query.Invalid")
}
}
func appKeysSortingToColumn(sortingCriteria application.ApplicationKeysSorting) query.Column {
switch sortingCriteria {
case application.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_PROJECT_ID:
return query.AuthNKeyColumnAggregateID
case application.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_CREATION_DATE:
return query.AuthNKeyColumnCreationDate
case application.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_EXPIRATION:
return query.AuthNKeyColumnExpiration
case application.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_ORGANIZATION_ID:
return query.AuthNKeyColumnResourceOwner
case application.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_TYPE:
return query.AuthNKeyColumnType
case application.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_APPLICATION_ID:
return query.AuthNKeyColumnObjectID
case application.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_ID:
fallthrough
default:
return query.AuthNKeyColumnID
}
}
func ApplicationKeysToPb(keys []*query.AuthNKey) []*application.ApplicationKey {
pbAppKeys := make([]*application.ApplicationKey, len(keys))
for i, k := range keys {
pbKey := &application.ApplicationKey{
KeyId: k.ID,
ApplicationId: k.ApplicationID,
ProjectId: k.AggregateID,
CreationDate: timestamppb.New(k.CreationDate),
OrganizationId: k.ResourceOwner,
ExpirationDate: timestamppb.New(k.Expiration),
}
pbAppKeys[i] = pbKey
}
return pbAppKeys
}

View File

@@ -0,0 +1,720 @@
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"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
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 *application.Application
}{
{
testName: "full application conversion",
inputQueryApp: &query.App{
ID: "id",
CreationDate: now,
ChangeDate: now,
State: domain.AppStateActive,
Name: "test-application",
APIConfig: &query.APIApp{},
},
expectedPbApp: &application.Application{
ApplicationId: "id",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
State: application.ApplicationState_APPLICATION_STATE_ACTIVE,
Name: "test-application",
Configuration: &application.Application_ApiConfiguration{
ApiConfiguration: &application.APIConfiguration{},
},
},
},
{
testName: "nil application",
inputQueryApp: nil,
expectedPbApp: &application.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 *application.ListApplicationsRequest
expectedResponse *query.AppSearchQueries
expectedError error
}{
{
testName: "invalid pagination limit",
req: &application.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: &application.ListApplicationsRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
Filters: []*application.ApplicationSearchFilter{},
},
expectedResponse: &query.AppSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AppColumnID,
},
Queries: []query.SearchQuery{},
},
},
{
testName: "valid request",
req: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{ProjectIdFilter: &application.ProjectIDFilter{ProjectId: "project1"}},
},
{
Filter: &application.ApplicationSearchFilter_NameFilter{NameFilter: &application.ApplicationNameFilter{Name: "test"}},
},
},
SortingColumn: application.ApplicationSorting_APPLICATION_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{
validSearchByProjectQuery,
validSearchByNameQuery,
},
},
},
}
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 TestApplicationSortingToColumn(t *testing.T) {
t.Parallel()
tt := []struct {
name string
sorting application.ApplicationSorting
expected query.Column
}{
{
name: "sort by change date",
sorting: application.ApplicationSorting_APPLICATION_SORT_BY_CHANGE_DATE,
expected: query.AppColumnChangeDate,
},
{
name: "sort by creation date",
sorting: application.ApplicationSorting_APPLICATION_SORT_BY_CREATION_DATE,
expected: query.AppColumnCreationDate,
},
{
name: "sort by name",
sorting: application.ApplicationSorting_APPLICATION_SORT_BY_NAME,
expected: query.AppColumnName,
},
{
name: "sort by state",
sorting: application.ApplicationSorting_APPLICATION_SORT_BY_STATE,
expected: query.AppColumnState,
},
{
name: "sort by ID",
sorting: application.ApplicationSorting_APPLICATION_SORT_BY_ID,
expected: query.AppColumnID,
},
{
name: "unknown sorting defaults to ID",
sorting: application.ApplicationSorting(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 application.ApplicationState
}{
{
name: "active state",
state: domain.AppStateActive,
expected: application.ApplicationState_APPLICATION_STATE_ACTIVE,
},
{
name: "inactive state",
state: domain.AppStateInactive,
expected: application.ApplicationState_APPLICATION_STATE_INACTIVE,
},
{
name: "removed state",
state: domain.AppStateRemoved,
expected: application.ApplicationState_APPLICATION_STATE_REMOVED,
},
{
name: "unspecified state",
state: domain.AppStateUnspecified,
expected: application.ApplicationState_APPLICATION_STATE_UNSPECIFIED,
},
{
name: "unknown state defaults to unspecified",
state: domain.AppState(99),
expected: application.ApplicationState_APPLICATION_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 application.IsApplicationConfiguration
}{
{
name: "OIDC config",
app: &query.App{
OIDCConfig: &query.OIDCApp{},
},
expected: &application.Application_OidcConfiguration{
OidcConfiguration: &application.OIDCConfiguration{
ResponseTypes: []application.OIDCResponseType{},
GrantTypes: []application.OIDCGrantType{},
ComplianceProblems: []*application.OIDCLocalizedMessage{},
ClockSkew: &durationpb.Duration{},
},
},
},
{
name: "SAML config",
app: &query.App{
SAMLConfig: &query.SAMLApp{},
},
expected: &application.Application_SamlConfiguration{
SamlConfiguration: &application.SAMLConfiguration{},
},
},
{
name: "API config",
app: &query.App{
APIConfig: &query.APIApp{},
},
expected: &application.Application_ApiConfiguration{
ApiConfiguration: &application.APIConfiguration{},
},
},
}
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 *application.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: &application.LoginVersion{Version: &application.LoginVersion_LoginV1{LoginV1: &application.LoginV1{}}},
expectedVer: gu.Ptr(domain.LoginVersion1),
expectedURI: gu.Ptr(""),
},
{
name: "login v2 valid URI",
version: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{LoginV2: &application.LoginV2{BaseUri: gu.Ptr("https://valid.url")}}},
expectedVer: gu.Ptr(domain.LoginVersion2),
expectedURI: gu.Ptr("https://valid.url"),
},
{
name: "login v2 invalid URI",
version: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{LoginV2: &application.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: &application.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 *application.LoginVersion
}{
{
name: "unspecified version",
version: domain.LoginVersionUnspecified,
baseURI: nil,
expected: nil,
},
{
name: "login v1",
version: domain.LoginVersion1,
baseURI: nil,
expected: &application.LoginVersion{
Version: &application.LoginVersion_LoginV1{
LoginV1: &application.LoginV1{},
},
},
},
{
name: "login v2",
version: domain.LoginVersion2,
baseURI: gu.Ptr("https://example.com"),
expected: &application.LoginVersion{
Version: &application.LoginVersion_LoginV2{
LoginV2: &application.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 *application.ApplicationSearchFilter
expectedQuery query.SearchQuery
expectedError error
}{
{
name: "name query",
query: &application.ApplicationSearchFilter{
Filter: &application.ApplicationSearchFilter_NameFilter{
NameFilter: &application.ApplicationNameFilter{
Name: "test",
Method: filter_pb_v2.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
expectedQuery: validAppNameSearchQuery,
},
{
name: "state query",
query: &application.ApplicationSearchFilter{
Filter: &application.ApplicationSearchFilter_StateFilter{
StateFilter: application.ApplicationState_APPLICATION_STATE_ACTIVE,
},
},
expectedQuery: validAppStateSearchQuery,
},
{
name: "api application only query",
query: &application.ApplicationSearchFilter{
Filter: &application.ApplicationSearchFilter_TypeFilter{
TypeFilter: application.ApplicationType_APPLICATION_TYPE_API,
},
},
expectedQuery: &query.NotNullQuery{
Column: query.AppAPIConfigColumnAppID,
},
},
{
name: "oidc application only query",
query: &application.ApplicationSearchFilter{
Filter: &application.ApplicationSearchFilter_TypeFilter{
TypeFilter: application.ApplicationType_APPLICATION_TYPE_OIDC,
},
},
expectedQuery: &query.NotNullQuery{
Column: query.AppOIDCConfigColumnAppID,
},
},
{
name: "saml application only query",
query: &application.ApplicationSearchFilter{
Filter: &application.ApplicationSearchFilter_TypeFilter{
TypeFilter: application.ApplicationType_APPLICATION_TYPE_SAML,
},
},
expectedQuery: &query.NotNullQuery{
Column: query.AppSAMLConfigColumnAppID,
},
},
{
name: "invalid query type",
query: &application.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 := applicationFilterToQuery(tc.query)
// Then
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedQuery, result)
})
}
}
func TestListApplicationKeysRequestToDomain(t *testing.T) {
t.Parallel()
resourceOwnerQuery, err := query.NewAuthNKeyResourceOwnerQuery("org1")
require.NoError(t, err)
projectIDQuery, err := query.NewAuthNKeyAggregateIDQuery("project1")
require.NoError(t, err)
appIDQuery, err := query.NewAuthNKeyObjectIDQuery("app1")
require.NoError(t, err)
sysDefaults := systemdefaults.SystemDefaults{DefaultQueryLimit: 100, MaxQueryLimit: 150}
tt := []struct {
name string
req *application.ListApplicationKeysRequest
expectedResult *query.AuthNKeySearchQueries
expectedError error
}{
{
name: "invalid pagination limit",
req: &application.ListApplicationKeysRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true, Limit: uint32(sysDefaults.MaxQueryLimit + 1)},
},
expectedResult: nil,
expectedError: zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", sysDefaults.MaxQueryLimit+1, sysDefaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded"),
},
{
name: "empty request",
req: &application.ListApplicationKeysRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
},
expectedResult: &query.AuthNKeySearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AuthNKeyColumnID,
},
Queries: []query.SearchQuery{},
},
},
{
name: "only organization id",
req: &application.ListApplicationKeysRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
Filters: []*application.ApplicationKeySearchFilter{
{Filter: &application.ApplicationKeySearchFilter_OrganizationIdFilter{
OrganizationIdFilter: &application.ApplicationKeyOrganizationIDFilter{OrganizationId: "org1"},
}},
},
},
expectedResult: &query.AuthNKeySearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AuthNKeyColumnID,
},
Queries: []query.SearchQuery{
resourceOwnerQuery,
},
},
},
{
name: "only project id",
req: &application.ListApplicationKeysRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
Filters: []*application.ApplicationKeySearchFilter{
{Filter: &application.ApplicationKeySearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ApplicationKeyProjectIDFilter{ProjectId: "project1"},
}},
},
},
expectedResult: &query.AuthNKeySearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AuthNKeyColumnID,
},
Queries: []query.SearchQuery{
projectIDQuery,
},
},
},
{
name: "only application id",
req: &application.ListApplicationKeysRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
Filters: []*application.ApplicationKeySearchFilter{
{Filter: &application.ApplicationKeySearchFilter_ApplicationIdFilter{
ApplicationIdFilter: &application.ApplicationKeyApplicationIDFilter{ApplicationId: "app1"},
}},
},
},
expectedResult: &query.AuthNKeySearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AuthNKeyColumnID,
},
Queries: []query.SearchQuery{
appIDQuery,
},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result, err := ListApplicationKeysRequestToDomain(sysDefaults, tc.req)
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedResult, result)
})
}
}
func TestApplicationKeysToPb(t *testing.T) {
t.Parallel()
now := time.Now()
tt := []struct {
name string
input []*query.AuthNKey
expected []*application.ApplicationKey
}{
{
name: "multiple keys",
input: []*query.AuthNKey{
{
ID: "key1",
AggregateID: "project1",
ApplicationID: "app1",
CreationDate: now,
ResourceOwner: "org1",
Expiration: now.Add(24 * time.Hour),
Type: domain.AuthNKeyTypeJSON,
},
{
ID: "key2",
AggregateID: "project2",
ApplicationID: "app1",
CreationDate: now.Add(-time.Hour),
ResourceOwner: "org2",
Expiration: now.Add(48 * time.Hour),
Type: domain.AuthNKeyTypeNONE,
},
},
expected: []*application.ApplicationKey{
{
KeyId: "key1",
ApplicationId: "app1",
ProjectId: "project1",
CreationDate: timestamppb.New(now),
OrganizationId: "org1",
ExpirationDate: timestamppb.New(now.Add(24 * time.Hour)),
},
{
KeyId: "key2",
ApplicationId: "app1",
ProjectId: "project2",
CreationDate: timestamppb.New(now.Add(-time.Hour)),
OrganizationId: "org2",
ExpirationDate: timestamppb.New(now.Add(48 * time.Hour)),
},
},
},
{
name: "empty slice",
input: []*query.AuthNKey{},
expected: []*application.ApplicationKey{},
},
{
name: "nil input",
input: nil,
expected: []*application.ApplicationKey{},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result := ApplicationKeysToPb(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}

View File

@@ -0,0 +1,292 @@
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"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
)
func CreateOIDCAppRequestToDomain(name, projectID string, req *application.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.GetApplicationType())),
AuthMethodType: gu.Ptr(oidcAuthMethodTypeToDomain(req.GetAuthMethodType())),
PostLogoutRedirectUris: req.GetPostLogoutRedirectUris(),
DevMode: &req.DevelopmentMode,
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 *application.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.ApplicationType),
AuthMethodType: oidcAuthMethodTypeToDomainPtr(app.AuthMethodType),
PostLogoutRedirectUris: app.PostLogoutRedirectUris,
DevMode: app.DevelopmentMode,
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 []application.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 application.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED:
oidcResponseTypes[i] = domain.OIDCResponseTypeUnspecified
case application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE:
oidcResponseTypes[i] = domain.OIDCResponseTypeCode
case application.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN:
oidcResponseTypes[i] = domain.OIDCResponseTypeIDToken
case application.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN:
oidcResponseTypes[i] = domain.OIDCResponseTypeIDTokenToken
}
}
return oidcResponseTypes
}
func oidcGrantTypesToDomain(grantTypes []application.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 application.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE:
oidcGrantTypes[i] = domain.OIDCGrantTypeAuthorizationCode
case application.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT:
oidcGrantTypes[i] = domain.OIDCGrantTypeImplicit
case application.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN:
oidcGrantTypes[i] = domain.OIDCGrantTypeRefreshToken
case application.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE:
oidcGrantTypes[i] = domain.OIDCGrantTypeDeviceCode
case application.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE:
oidcGrantTypes[i] = domain.OIDCGrantTypeTokenExchange
}
}
return oidcGrantTypes
}
func oidcApplicationTypeToDomainPtr(appType *application.OIDCApplicationType) *domain.OIDCApplicationType {
if appType == nil {
return nil
}
res := oidcApplicationTypeToDomain(*appType)
return &res
}
func oidcApplicationTypeToDomain(appType application.OIDCApplicationType) domain.OIDCApplicationType {
switch appType {
case application.OIDCApplicationType_OIDC_APP_TYPE_WEB:
return domain.OIDCApplicationTypeWeb
case application.OIDCApplicationType_OIDC_APP_TYPE_USER_AGENT:
return domain.OIDCApplicationTypeUserAgent
case application.OIDCApplicationType_OIDC_APP_TYPE_NATIVE:
return domain.OIDCApplicationTypeNative
default:
return domain.OIDCApplicationTypeWeb
}
}
func oidcAuthMethodTypeToDomainPtr(authType *application.OIDCAuthMethodType) *domain.OIDCAuthMethodType {
if authType == nil {
return nil
}
res := oidcAuthMethodTypeToDomain(*authType)
return &res
}
func oidcAuthMethodTypeToDomain(authType application.OIDCAuthMethodType) domain.OIDCAuthMethodType {
switch authType {
case application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC:
return domain.OIDCAuthMethodTypeBasic
case application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST:
return domain.OIDCAuthMethodTypePost
case application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE:
return domain.OIDCAuthMethodTypeNone
case application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT:
return domain.OIDCAuthMethodTypePrivateKeyJWT
default:
return domain.OIDCAuthMethodTypeBasic
}
}
func oidcTokenTypeToDomainPtr(tokenType *application.OIDCTokenType) *domain.OIDCTokenType {
if tokenType == nil {
return nil
}
res := oidcTokenTypeToDomain(*tokenType)
return &res
}
func oidcTokenTypeToDomain(tokenType application.OIDCTokenType) domain.OIDCTokenType {
switch tokenType {
case application.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER:
return domain.OIDCTokenTypeBearer
case application.OIDCTokenType_OIDC_TOKEN_TYPE_JWT:
return domain.OIDCTokenTypeJWT
default:
return domain.OIDCTokenTypeBearer
}
}
func ComplianceProblemsToLocalizedMessages(complianceProblems []string) []*application.OIDCLocalizedMessage {
converted := make([]*application.OIDCLocalizedMessage, len(complianceProblems))
for i, p := range complianceProblems {
converted[i] = &application.OIDCLocalizedMessage{Key: p}
}
return converted
}
func appOIDCConfigToPb(oidcApp *query.OIDCApp) *application.Application_OidcConfiguration {
return &application.Application_OidcConfiguration{
OidcConfiguration: &application.OIDCConfiguration{
RedirectUris: oidcApp.RedirectURIs,
ResponseTypes: oidcResponseTypesFromModel(oidcApp.ResponseTypes),
GrantTypes: oidcGrantTypesFromModel(oidcApp.GrantTypes),
ApplicationType: oidcApplicationTypeToPb(oidcApp.AppType),
ClientId: oidcApp.ClientID,
AuthMethodType: oidcAuthMethodTypeToPb(oidcApp.AuthMethodType),
PostLogoutRedirectUris: oidcApp.PostLogoutRedirectURIs,
Version: application.OIDCVersion_OIDC_VERSION_1_0,
NonCompliant: len(oidcApp.ComplianceProblems) != 0,
ComplianceProblems: ComplianceProblemsToLocalizedMessages(oidcApp.ComplianceProblems),
DevelopmentMode: 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) []application.OIDCResponseType {
oidcResponseTypes := make([]application.OIDCResponseType, len(responseTypes))
for i, responseType := range responseTypes {
switch responseType {
case domain.OIDCResponseTypeUnspecified:
oidcResponseTypes[i] = application.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED
case domain.OIDCResponseTypeCode:
oidcResponseTypes[i] = application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE
case domain.OIDCResponseTypeIDToken:
oidcResponseTypes[i] = application.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN
case domain.OIDCResponseTypeIDTokenToken:
oidcResponseTypes[i] = application.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN
}
}
return oidcResponseTypes
}
func oidcGrantTypesFromModel(grantTypes []domain.OIDCGrantType) []application.OIDCGrantType {
oidcGrantTypes := make([]application.OIDCGrantType, len(grantTypes))
for i, grantType := range grantTypes {
switch grantType {
case domain.OIDCGrantTypeAuthorizationCode:
oidcGrantTypes[i] = application.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE
case domain.OIDCGrantTypeImplicit:
oidcGrantTypes[i] = application.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT
case domain.OIDCGrantTypeRefreshToken:
oidcGrantTypes[i] = application.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN
case domain.OIDCGrantTypeDeviceCode:
oidcGrantTypes[i] = application.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE
case domain.OIDCGrantTypeTokenExchange:
oidcGrantTypes[i] = application.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE
}
}
return oidcGrantTypes
}
func oidcApplicationTypeToPb(appType domain.OIDCApplicationType) application.OIDCApplicationType {
switch appType {
case domain.OIDCApplicationTypeWeb:
return application.OIDCApplicationType_OIDC_APP_TYPE_WEB
case domain.OIDCApplicationTypeUserAgent:
return application.OIDCApplicationType_OIDC_APP_TYPE_USER_AGENT
case domain.OIDCApplicationTypeNative:
return application.OIDCApplicationType_OIDC_APP_TYPE_NATIVE
default:
return application.OIDCApplicationType_OIDC_APP_TYPE_WEB
}
}
func oidcAuthMethodTypeToPb(authType domain.OIDCAuthMethodType) application.OIDCAuthMethodType {
switch authType {
case domain.OIDCAuthMethodTypeBasic:
return application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC
case domain.OIDCAuthMethodTypePost:
return application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST
case domain.OIDCAuthMethodTypeNone:
return application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE
case domain.OIDCAuthMethodTypePrivateKeyJWT:
return application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT
default:
return application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC
}
}
func oidcTokenTypeToPb(tokenType domain.OIDCTokenType) application.OIDCTokenType {
switch tokenType {
case domain.OIDCTokenTypeBearer:
return application.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER
case domain.OIDCTokenTypeJWT:
return application.OIDCTokenType_OIDC_TOKEN_TYPE_JWT
default:
return application.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER
}
}

View File

@@ -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"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
)
func TestCreateOIDCAppRequestToDomain(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
projectID string
req *application.CreateOIDCApplicationRequest
expectedModel *domain.OIDCApp
expectedError error
}{
{
testName: "unparsable login version 2 URL",
projectID: "pid",
req: &application.CreateOIDCApplicationRequest{
LoginVersion: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{
LoginV2: &application.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: &application.CreateOIDCApplicationRequest{
RedirectUris: []string{"https://redirect"},
ResponseTypes: []application.OIDCResponseType{application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
GrantTypes: []application.OIDCGrantType{application.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
ApplicationType: application.OIDCApplicationType_OIDC_APP_TYPE_WEB,
AuthMethodType: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
PostLogoutRedirectUris: []string{"https://logout"},
DevelopmentMode: true,
AccessTokenType: application.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: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{LoginV2: &application.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 *application.UpdateOIDCApplicationConfigurationRequest
expectedModel *domain.OIDCApp
expectedError error
}{
{
testName: "unparsable login version 2 URL",
appID: "app1",
projectID: "pid",
req: &application.UpdateOIDCApplicationConfigurationRequest{
LoginVersion: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{
LoginV2: &application.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: &application.UpdateOIDCApplicationConfigurationRequest{
RedirectUris: []string{"https://redirect"},
ResponseTypes: []application.OIDCResponseType{application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
GrantTypes: []application.OIDCGrantType{application.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
ApplicationType: gu.Ptr(application.OIDCApplicationType_OIDC_APP_TYPE_WEB),
AuthMethodType: gu.Ptr(application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC),
PostLogoutRedirectUris: []string{"https://logout"},
DevelopmentMode: gu.Ptr(true),
AccessTokenType: gu.Ptr(application.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: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{
LoginV2: &application.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 []application.OIDCResponseType
expectedResponse []domain.OIDCResponseType
}{
{
testName: "empty response types",
inputResponseType: []application.OIDCResponseType{},
expectedResponse: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
},
{
testName: "all response types",
inputResponseType: []application.OIDCResponseType{
application.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED,
application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
application.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN,
application.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN,
},
expectedResponse: []domain.OIDCResponseType{
domain.OIDCResponseTypeUnspecified,
domain.OIDCResponseTypeCode,
domain.OIDCResponseTypeIDToken,
domain.OIDCResponseTypeIDTokenToken,
},
},
{
testName: "single response type",
inputResponseType: []application.OIDCResponseType{
application.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 []application.OIDCGrantType
expectedGrants []domain.OIDCGrantType
}{
{
testName: "empty grant types",
inputGrantType: []application.OIDCGrantType{},
expectedGrants: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
},
{
testName: "all grant types",
inputGrantType: []application.OIDCGrantType{
application.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
application.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT,
application.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN,
application.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE,
application.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
},
expectedGrants: []domain.OIDCGrantType{
domain.OIDCGrantTypeAuthorizationCode,
domain.OIDCGrantTypeImplicit,
domain.OIDCGrantTypeRefreshToken,
domain.OIDCGrantTypeDeviceCode,
domain.OIDCGrantTypeTokenExchange,
},
},
{
testName: "single grant type",
inputGrantType: []application.OIDCGrantType{
application.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 application.OIDCApplicationType
expected domain.OIDCApplicationType
}{
{
name: "web type",
appType: application.OIDCApplicationType_OIDC_APP_TYPE_WEB,
expected: domain.OIDCApplicationTypeWeb,
},
{
name: "user agent type",
appType: application.OIDCApplicationType_OIDC_APP_TYPE_USER_AGENT,
expected: domain.OIDCApplicationTypeUserAgent,
},
{
name: "native type",
appType: application.OIDCApplicationType_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 application.OIDCAuthMethodType
expectedResponse domain.OIDCAuthMethodType
}{
{
name: "basic auth type",
authType: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
expectedResponse: domain.OIDCAuthMethodTypeBasic,
},
{
name: "post auth type",
authType: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST,
expectedResponse: domain.OIDCAuthMethodTypePost,
},
{
name: "none auth type",
authType: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
expectedResponse: domain.OIDCAuthMethodTypeNone,
},
{
name: "private key jwt auth type",
authType: application.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 application.OIDCTokenType
expectedType domain.OIDCTokenType
}{
{
name: "bearer token type",
tokenType: application.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
expectedType: domain.OIDCTokenTypeBearer,
},
{
name: "jwt token type",
tokenType: application.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 *application.Application_OidcConfiguration
}{
{
name: "empty config",
input: &query.OIDCApp{},
expected: &application.Application_OidcConfiguration{
OidcConfiguration: &application.OIDCConfiguration{
Version: application.OIDCVersion_OIDC_VERSION_1_0,
ComplianceProblems: []*application.OIDCLocalizedMessage{},
ClockSkew: durationpb.New(0),
ResponseTypes: []application.OIDCResponseType{},
GrantTypes: []application.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: &application.Application_OidcConfiguration{
OidcConfiguration: &application.OIDCConfiguration{
RedirectUris: []string{"https://example.com/callback"},
ResponseTypes: []application.OIDCResponseType{application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
GrantTypes: []application.OIDCGrantType{application.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
ApplicationType: application.OIDCApplicationType_OIDC_APP_TYPE_WEB,
ClientId: "client123",
AuthMethodType: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
PostLogoutRedirectUris: []string{"https://example.com/logout"},
Version: application.OIDCVersion_OIDC_VERSION_1_0,
NonCompliant: true,
ComplianceProblems: []*application.OIDCLocalizedMessage{
{Key: "problem1"},
{Key: "problem2"},
},
DevelopmentMode: true,
AccessTokenType: application.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: &application.LoginVersion{
Version: &application.LoginVersion_LoginV2{
LoginV2: &application.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 []application.OIDCResponseType
}{
{
name: "empty response types",
responseTypes: []domain.OIDCResponseType{},
expected: []application.OIDCResponseType{},
},
{
name: "all response types",
responseTypes: []domain.OIDCResponseType{
domain.OIDCResponseTypeUnspecified,
domain.OIDCResponseTypeCode,
domain.OIDCResponseTypeIDToken,
domain.OIDCResponseTypeIDTokenToken,
},
expected: []application.OIDCResponseType{
application.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED,
application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
application.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN,
application.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN,
},
},
{
name: "single response type",
responseTypes: []domain.OIDCResponseType{
domain.OIDCResponseTypeCode,
},
expected: []application.OIDCResponseType{
application.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 []application.OIDCGrantType
}{
{
name: "empty grant types",
grantTypes: []domain.OIDCGrantType{},
expected: []application.OIDCGrantType{},
},
{
name: "all grant types",
grantTypes: []domain.OIDCGrantType{
domain.OIDCGrantTypeAuthorizationCode,
domain.OIDCGrantTypeImplicit,
domain.OIDCGrantTypeRefreshToken,
domain.OIDCGrantTypeDeviceCode,
domain.OIDCGrantTypeTokenExchange,
},
expected: []application.OIDCGrantType{
application.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
application.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT,
application.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN,
application.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE,
application.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
},
},
{
name: "single grant type",
grantTypes: []domain.OIDCGrantType{
domain.OIDCGrantTypeAuthorizationCode,
},
expected: []application.OIDCGrantType{
application.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 application.OIDCApplicationType
}{
{
name: "web type",
appType: domain.OIDCApplicationTypeWeb,
expected: application.OIDCApplicationType_OIDC_APP_TYPE_WEB,
},
{
name: "user agent type",
appType: domain.OIDCApplicationTypeUserAgent,
expected: application.OIDCApplicationType_OIDC_APP_TYPE_USER_AGENT,
},
{
name: "native type",
appType: domain.OIDCApplicationTypeNative,
expected: application.OIDCApplicationType_OIDC_APP_TYPE_NATIVE,
},
{
name: "unspecified type defaults to web",
appType: domain.OIDCApplicationType(999), // Invalid value to trigger default case
expected: application.OIDCApplicationType_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 application.OIDCAuthMethodType
}{
{
name: "basic auth type",
authType: domain.OIDCAuthMethodTypeBasic,
expected: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
},
{
name: "post auth type",
authType: domain.OIDCAuthMethodTypePost,
expected: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST,
},
{
name: "none auth type",
authType: domain.OIDCAuthMethodTypeNone,
expected: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
},
{
name: "private key jwt auth type",
authType: domain.OIDCAuthMethodTypePrivateKeyJWT,
expected: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
},
{
name: "unknown auth type defaults to basic",
authType: domain.OIDCAuthMethodType(999),
expected: application.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 application.OIDCTokenType
}{
{
name: "bearer token type",
tokenType: domain.OIDCTokenTypeBearer,
expected: application.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
},
{
name: "jwt token type",
tokenType: domain.OIDCTokenTypeJWT,
expected: application.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
},
{
name: "unknown token type defaults to bearer",
tokenType: domain.OIDCTokenType(999), // Invalid value to trigger default case
expected: application.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)
})
}
}

View File

@@ -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"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
)
func CreateSAMLAppRequestToDomain(name, projectID string, req *application.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 *application.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 application.MetaType) ([]byte, *string) {
switch t := metas.(type) {
case *application.UpdateSAMLApplicationConfigurationRequest_MetadataXml:
return t.MetadataXml, nil
case *application.UpdateSAMLApplicationConfigurationRequest_MetadataUrl:
return nil, &t.MetadataUrl
case nil:
return nil, nil
default:
return nil, nil
}
}
func appSAMLConfigToPb(samlApp *query.SAMLApp) application.IsApplicationConfiguration {
if samlApp == nil {
return &application.Application_SamlConfiguration{
SamlConfiguration: &application.SAMLConfiguration{
LoginVersion: &application.LoginVersion{},
},
}
}
return &application.Application_SamlConfiguration{
SamlConfiguration: &application.SAMLConfiguration{
MetadataXml: samlApp.Metadata,
MetadataUrl: samlApp.MetadataURL,
LoginVersion: loginVersionToPb(samlApp.LoginVersion, samlApp.LoginBaseURI),
},
}
}

View File

@@ -0,0 +1,253 @@
package convert
import (
"fmt"
"net/url"
"testing"
"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/integration"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
)
func samlMetadataGen(entityID string) []byte {
str := fmt.Sprintf(`<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
validUntil="2022-08-26T14:08:16Z"
cacheDuration="PT604800S"
entityID="%s">
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://test.com/saml/acs"
index="1" />
</md:SPSSODescriptor>
</md:EntityDescriptor>
`,
entityID)
return []byte(str)
}
func TestCreateSAMLAppRequestToDomain(t *testing.T) {
t.Parallel()
genMetaForValidRequest := samlMetadataGen(integration.URL())
tt := []struct {
testName string
appName string
projectID string
req *application.CreateSAMLApplicationRequest
expectedResponse *domain.SAMLApp
expectedError error
}{
{
testName: "login version error",
appName: "test-application",
projectID: "proj-1",
req: &application.CreateSAMLApplicationRequest{
Metadata: &application.CreateSAMLApplicationRequest_MetadataXml{
MetadataXml: samlMetadataGen(integration.URL()),
},
LoginVersion: &application.LoginVersion{
Version: &application.LoginVersion_LoginV2{
LoginV2: &application.LoginV2{BaseUri: gu.Ptr("%+o")},
},
},
},
expectedError: &url.Error{
URL: "%+o",
Op: "parse",
Err: url.EscapeError("%+o"),
},
},
{
testName: "valid request",
appName: "test-application",
projectID: "proj-1",
req: &application.CreateSAMLApplicationRequest{
Metadata: &application.CreateSAMLApplicationRequest_MetadataXml{
MetadataXml: genMetaForValidRequest,
},
LoginVersion: nil,
},
expectedResponse: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
AppName: "test-application",
Metadata: genMetaForValidRequest,
MetadataURL: gu.Ptr(""),
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
LoginBaseURI: gu.Ptr(""),
State: 0,
},
},
{
testName: "nil request",
appName: "test-application",
projectID: "proj-1",
req: nil,
expectedResponse: &domain.SAMLApp{
AppName: "test-application",
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(integration.URL())
tt := []struct {
testName string
appID string
projectID string
req *application.UpdateSAMLApplicationConfigurationRequest
expectedResponse *domain.SAMLApp
expectedError error
}{
{
testName: "login version error",
appID: "application-1",
projectID: "proj-1",
req: &application.UpdateSAMLApplicationConfigurationRequest{
Metadata: &application.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
MetadataXml: samlMetadataGen(integration.URL()),
},
LoginVersion: &application.LoginVersion{
Version: &application.LoginVersion_LoginV2{
LoginV2: &application.LoginV2{BaseUri: gu.Ptr("%+o")},
},
},
},
expectedError: &url.Error{
URL: "%+o",
Op: "parse",
Err: url.EscapeError("%+o"),
},
},
{
testName: "valid request",
appID: "application-1",
projectID: "proj-1",
req: &application.UpdateSAMLApplicationConfigurationRequest{
Metadata: &application.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
MetadataXml: genMetaForValidRequest,
},
LoginVersion: nil,
},
expectedResponse: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
AppID: "application-1",
Metadata: genMetaForValidRequest,
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
LoginBaseURI: gu.Ptr(""),
},
},
{
testName: "nil request",
appID: "application-1",
projectID: "proj-1",
req: nil,
expectedResponse: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
AppID: "application-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(integration.URL())
tt := []struct {
name string
inputSAMLApp *query.SAMLApp
expectedPbApp application.IsApplicationConfiguration
}{
{
name: "valid conversion",
inputSAMLApp: &query.SAMLApp{
Metadata: metadata,
LoginVersion: domain.LoginVersion2,
LoginBaseURI: gu.Ptr("https://example.com"),
},
expectedPbApp: &application.Application_SamlConfiguration{
SamlConfiguration: &application.SAMLConfiguration{
MetadataXml: metadata,
LoginVersion: &application.LoginVersion{
Version: &application.LoginVersion_LoginV2{
LoginV2: &application.LoginV2{BaseUri: gu.Ptr("https://example.com")},
},
},
},
},
},
{
name: "nil saml application",
inputSAMLApp: nil,
expectedPbApp: &application.Application_SamlConfiguration{
SamlConfiguration: &application.SAMLConfiguration{
LoginVersion: &application.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)
})
}
}

View File

@@ -0,0 +1,206 @@
//go:build integration
package app_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
)
func TestCreateApplicationKey(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
createdApp := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
t.Parallel()
tt := []struct {
testName string
creationRequest *application.CreateApplicationKeyRequest
inputCtx context.Context
expectedErrorType codes.Code
}{
{
testName: "when application id is not found should return failed precondition",
inputCtx: IAMOwnerCtx,
creationRequest: &application.CreateApplicationKeyRequest{
ProjectId: p.GetId(),
ApplicationId: integration.ID(),
ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()),
},
expectedErrorType: codes.FailedPrecondition,
},
{
testName: "when CreateAPIApp request is valid should create application and return no error",
inputCtx: IAMOwnerCtx,
creationRequest: &application.CreateApplicationKeyRequest{
ProjectId: p.GetId(),
ApplicationId: createdApp.GetApplicationId(),
ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()),
},
},
// LoginUser
{
testName: "when user has no project.application.write permission for application key generation should return permission error",
inputCtx: LoginUserCtx,
creationRequest: &application.CreateApplicationKeyRequest{
ProjectId: p.GetId(),
ApplicationId: createdApp.GetApplicationId(),
ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()),
},
expectedErrorType: codes.PermissionDenied,
},
// OrgOwner
{
testName: "when user is OrgOwner application key request should succeed",
inputCtx: OrgOwnerCtx,
creationRequest: &application.CreateApplicationKeyRequest{
ProjectId: p.GetId(),
ApplicationId: createdApp.GetApplicationId(),
ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()),
},
},
// ProjectOwner
{
testName: "when user is ProjectOwner application key request should succeed",
inputCtx: projectOwnerCtx,
creationRequest: &application.CreateApplicationKeyRequest{
ProjectId: p.GetId(),
ApplicationId: createdApp.GetApplicationId(),
ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()),
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
res, err := instance.Client.ApplicationV2.CreateApplicationKey(tc.inputCtx, tc.creationRequest)
require.Equal(t, tc.expectedErrorType, status.Code(err))
if tc.expectedErrorType == codes.OK {
assert.NotZero(t, res.GetKeyId())
assert.NotZero(t, res.GetCreationDate())
assert.NotZero(t, res.GetKeyDetails())
}
})
}
}
func TestDeleteApplicationKey(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
createdApp := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
t.Parallel()
tt := []struct {
testName string
deletionRequest func(ttt *testing.T) *application.DeleteApplicationKeyRequest
inputCtx context.Context
expectedErrorType codes.Code
}{
{
testName: "when application key ID is not found should return not found error",
inputCtx: IAMOwnerCtx,
deletionRequest: func(ttt *testing.T) *application.DeleteApplicationKeyRequest {
return &application.DeleteApplicationKeyRequest{
KeyId: integration.ID(),
ProjectId: p.GetId(),
ApplicationId: createdApp.GetApplicationId(),
}
},
expectedErrorType: codes.NotFound,
},
{
testName: "when valid application key ID should delete successfully",
inputCtx: IAMOwnerCtx,
deletionRequest: func(ttt *testing.T) *application.DeleteApplicationKeyRequest {
createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetApplicationId(), time.Now().AddDate(0, 0, 1))
return &application.DeleteApplicationKeyRequest{
KeyId: createdAppKey.GetKeyId(),
ProjectId: p.GetId(),
ApplicationId: createdApp.GetApplicationId(),
}
},
},
// LoginUser
{
testName: "when user has no project.application.write permission for application key deletion should return permission error",
inputCtx: LoginUserCtx,
deletionRequest: func(ttt *testing.T) *application.DeleteApplicationKeyRequest {
createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetApplicationId(), time.Now().AddDate(0, 0, 1))
return &application.DeleteApplicationKeyRequest{
KeyId: createdAppKey.GetKeyId(),
ProjectId: p.GetId(),
ApplicationId: createdApp.GetApplicationId(),
}
},
expectedErrorType: codes.PermissionDenied,
},
// ProjectOwner
{
testName: "when user is OrgOwner API request should succeed",
inputCtx: projectOwnerCtx,
deletionRequest: func(ttt *testing.T) *application.DeleteApplicationKeyRequest {
createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetApplicationId(), time.Now().AddDate(0, 0, 1))
return &application.DeleteApplicationKeyRequest{
KeyId: createdAppKey.GetKeyId(),
ProjectId: p.GetId(),
ApplicationId: createdApp.GetApplicationId(),
}
},
},
// OrganizationOwner
{
testName: "when user is OrgOwner application key deletion request should succeed",
inputCtx: OrgOwnerCtx,
deletionRequest: func(ttt *testing.T) *application.DeleteApplicationKeyRequest {
createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetApplicationId(), time.Now().AddDate(0, 0, 1))
return &application.DeleteApplicationKeyRequest{
KeyId: createdAppKey.GetKeyId(),
ProjectId: p.GetId(),
ApplicationId: createdApp.GetApplicationId(),
}
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
deletionReq := tc.deletionRequest(t)
// When
res, err := instance.Client.ApplicationV2.DeleteApplicationKey(tc.inputCtx, deletionReq)
// Then
require.Equal(t, tc.expectedErrorType, status.Code(err))
if tc.expectedErrorType == codes.OK {
assert.NotEmpty(t, res.GetDeletionDate())
}
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,913 @@
//go:build integration
package app_test
import (
"context"
"fmt"
"slices"
"testing"
"time"
"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"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
)
func TestGetApplication(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
apiAppName := integration.ApplicationName()
createdApiApp, errAPIAppCreation := instance.Client.ApplicationV2.CreateApplication(IAMOwnerCtx, &application.CreateApplicationRequest{
ProjectId: p.GetId(),
Name: apiAppName,
ApplicationType: &application.CreateApplicationRequest_ApiConfiguration{
ApiConfiguration: &application.CreateAPIApplicationRequest{
AuthMethodType: application.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
},
},
})
require.Nil(t, errAPIAppCreation)
samlAppName := integration.ApplicationName()
createdSAMLApp, errSAMLAppCreation := instance.Client.ApplicationV2.CreateApplication(IAMOwnerCtx, &application.CreateApplicationRequest{
ProjectId: p.GetId(),
Name: samlAppName,
ApplicationType: &application.CreateApplicationRequest_SamlConfiguration{
SamlConfiguration: &application.CreateSAMLApplicationRequest{
LoginVersion: &application.LoginVersion{Version: &application.LoginVersion_LoginV1{LoginV1: &application.LoginV1{}}},
Metadata: &application.CreateSAMLApplicationRequest_MetadataXml{MetadataXml: samlMetadataGen(integration.URL())},
},
},
})
require.Nil(t, errSAMLAppCreation)
oidcAppName := integration.ApplicationName()
createdOIDCApp, errOIDCAppCreation := instance.Client.ApplicationV2.CreateApplication(IAMOwnerCtx, &application.CreateApplicationRequest{
ProjectId: p.GetId(),
Name: oidcAppName,
ApplicationType: &application.CreateApplicationRequest_OidcConfiguration{
OidcConfiguration: &application.CreateOIDCApplicationRequest{
RedirectUris: []string{"http://example.com"},
ResponseTypes: []application.OIDCResponseType{application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
GrantTypes: []application.OIDCGrantType{application.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
ApplicationType: application.OIDCApplicationType_OIDC_APP_TYPE_WEB,
AuthMethodType: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
PostLogoutRedirectUris: []string{"http://example.com/home"},
Version: application.OIDCVersion_OIDC_VERSION_1_0,
AccessTokenType: application.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
BackChannelLogoutUri: "http://example.com/logout",
LoginVersion: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{LoginV2: &application.LoginV2{BaseUri: &baseURI}}},
},
},
})
require.Nil(t, errOIDCAppCreation)
t.Parallel()
tt := []struct {
testName string
inputRequest *application.GetApplicationRequest
inputCtx context.Context
expectedErrorType codes.Code
expectedAppName string
expectedAppID string
expectedApplicationType string
}{
{
testName: "when unknown application ID should return not found error",
inputCtx: IAMOwnerCtx,
inputRequest: &application.GetApplicationRequest{
ApplicationId: integration.ID(),
},
expectedErrorType: codes.NotFound,
},
{
testName: "when user has no permission should return membership not found error",
inputCtx: NoPermissionCtx,
inputRequest: &application.GetApplicationRequest{
ApplicationId: createdApiApp.GetApplicationId(),
},
expectedErrorType: codes.NotFound,
},
{
testName: "when providing API application ID should return valid API application result",
inputCtx: projectOwnerCtx,
inputRequest: &application.GetApplicationRequest{
ApplicationId: createdApiApp.GetApplicationId(),
},
expectedAppName: apiAppName,
expectedAppID: createdApiApp.GetApplicationId(),
expectedApplicationType: fmt.Sprintf("%T", &application.Application_ApiConfiguration{}),
},
{
testName: "when providing SAML application ID should return valid SAML application result",
inputCtx: IAMOwnerCtx,
inputRequest: &application.GetApplicationRequest{
ApplicationId: createdSAMLApp.GetApplicationId(),
},
expectedAppName: samlAppName,
expectedAppID: createdSAMLApp.GetApplicationId(),
expectedApplicationType: fmt.Sprintf("%T", &application.Application_SamlConfiguration{}),
},
{
testName: "when providing OIDC application ID should return valid OIDC application result",
inputCtx: IAMOwnerCtx,
inputRequest: &application.GetApplicationRequest{
ApplicationId: createdOIDCApp.GetApplicationId(),
},
expectedAppName: oidcAppName,
expectedAppID: createdOIDCApp.GetApplicationId(),
expectedApplicationType: fmt.Sprintf("%T", &application.Application_OidcConfiguration{}),
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
// When
res, err := instance.Client.ApplicationV2.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.GetApplication().GetApplicationId())
assert.Equal(t, tc.expectedAppName, res.GetApplication().GetName())
assert.NotZero(t, res.GetApplication().GetCreationDate())
assert.NotZero(t, res.GetApplication().GetChangeDate())
appType := fmt.Sprintf("%T", res.GetApplication().GetConfiguration())
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, IAMOwnerCtx, instance, p.GetId())
createdDeactivatedApiApp, deactivatedApiAppName := createAPIAppWithName(t, IAMOwnerCtx, instance, p.GetId())
deactivateApp(t, createdDeactivatedApiApp, p.GetId())
_, createdSAMLApp, samlAppName := createSAMLAppWithName(t, integration.URL(), p.GetId())
createdOIDCApp, oidcAppName := createOIDCAppWithName(t, integration.URL(), p.GetId())
type appWithName struct {
app *application.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.GetApplicationId() < b.app.GetApplicationId() {
return -1
}
if a.app.GetApplicationId() > b.app.GetApplicationId() {
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 *application.ListApplicationsRequest
inputCtx context.Context
expectedOrderedList []appWithName
expectedOrderedKeys func(keys []appWithName) any
actualOrderedKeys func(keys []*application.Application) any
}{
{
testName: "when no apps found should return empty list",
inputCtx: IAMOwnerCtx,
inputRequest: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: "another-id"},
},
},
},
},
expectedOrderedList: []appWithName{},
expectedOrderedKeys: func(keys []appWithName) any { return keys },
actualOrderedKeys: func(keys []*application.Application) any { return keys },
},
{
testName: "when user has no read permission should return empty set",
inputCtx: NoPermissionCtx,
inputRequest: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: p.GetId()},
},
},
},
},
expectedOrderedList: []appWithName{},
expectedOrderedKeys: func(keys []appWithName) any { return keys },
actualOrderedKeys: func(keys []*application.Application) any { return keys },
},
{
testName: "when sorting by name should return apps sorted by name in descending order",
inputCtx: IAMOwnerCtx,
inputRequest: &application.ListApplicationsRequest{
SortingColumn: application.ApplicationSorting_APPLICATION_SORT_BY_NAME,
Pagination: &filter.PaginationRequest{Asc: true},
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: p.GetId()},
},
},
},
},
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 []*application.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: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: p.GetId()},
},
},
},
SortingColumn: application.ApplicationSorting_APPLICATION_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 []*application.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: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: p.GetId()},
},
},
},
SortingColumn: application.ApplicationSorting_APPLICATION_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.GetApplicationId()
}
return ids
},
actualOrderedKeys: func(apps []*application.Application) any {
ids := make([]string, len(apps))
for i, a := range apps {
ids[i] = a.GetApplicationId()
}
return ids
},
},
{
testName: "when sorting by creation date should return apps sorted by creation date in descending order",
inputCtx: IAMOwnerCtx,
inputRequest: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: p.GetId()},
},
},
},
SortingColumn: application.ApplicationSorting_APPLICATION_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 []*application.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: &application.ListApplicationsRequest{
Pagination: &filter.PaginationRequest{Asc: true},
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: p.GetId()},
},
},
{
Filter: &application.ApplicationSearchFilter_StateFilter{
StateFilter: application.ApplicationState_APPLICATION_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 []*application.Application) any {
creationDates := make([]time.Time, len(apps))
for i, a := range apps {
creationDates[i] = a.GetCreationDate().AsTime()
}
return creationDates
},
},
{
testName: "when filtering by application type should return apps of matching type only",
inputCtx: IAMOwnerCtx,
inputRequest: &application.ListApplicationsRequest{
Pagination: &filter.PaginationRequest{Asc: true},
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: p.GetId()},
},
},
{Filter: &application.ApplicationSearchFilter_TypeFilter{
TypeFilter: application.ApplicationType_APPLICATION_TYPE_OIDC,
}},
},
},
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 []*application.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, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
// When
res, err := instance.Client.ApplicationV2.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.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
p, projectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx)
_, otherProjectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx)
appName1, appName2, appName3 := integration.ApplicationName(), integration.ApplicationName(), integration.ApplicationName()
reqForAPIAppCreation := &application.CreateApplicationRequest_ApiConfiguration{
ApiConfiguration: &application.CreateAPIApplicationRequest{AuthMethodType: application.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
}
app1, appAPIConfigChangeErr := instancePermissionV2.Client.ApplicationV2.CreateApplication(iamOwnerCtx, &application.CreateApplicationRequest{
ProjectId: p.GetId(),
Name: appName1,
ApplicationType: reqForAPIAppCreation,
})
require.Nil(t, appAPIConfigChangeErr)
app2, appAPIConfigChangeErr := instancePermissionV2.Client.ApplicationV2.CreateApplication(iamOwnerCtx, &application.CreateApplicationRequest{
ProjectId: p.GetId(),
Name: appName2,
ApplicationType: reqForAPIAppCreation,
})
require.Nil(t, appAPIConfigChangeErr)
app3, appAPIConfigChangeErr := instancePermissionV2.Client.ApplicationV2.CreateApplication(iamOwnerCtx, &application.CreateApplicationRequest{
ProjectId: p.GetId(),
Name: appName3,
ApplicationType: reqForAPIAppCreation,
})
require.Nil(t, appAPIConfigChangeErr)
t.Parallel()
tt := []struct {
testName string
inputRequest *application.ListApplicationsRequest
inputCtx context.Context
expectedCode codes.Code
expectedAppIDs []string
}{
{
testName: "when user has no read permission should return empty set",
inputCtx: instancePermissionV2.WithAuthorizationToken(context.Background(), integration.UserTypeNoPermission),
inputRequest: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: p.GetId()},
},
},
},
},
expectedAppIDs: []string{},
},
{
testName: "when projectOwner should return full application list",
inputCtx: projectOwnerCtx,
inputRequest: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: p.GetId()},
},
},
},
},
expectedCode: codes.OK,
expectedAppIDs: []string{app1.GetApplicationId(), app2.GetApplicationId(), app3.GetApplicationId()},
},
{
testName: "when orgOwner should return full application list",
inputCtx: instancePermissionV2.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
inputRequest: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: p.GetId()},
},
},
},
},
expectedAppIDs: []string{app1.GetApplicationId(), app2.GetApplicationId(), app3.GetApplicationId()},
},
{
testName: "when iamOwner user should return full application list",
inputCtx: iamOwnerCtx,
inputRequest: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{ProjectId: p.GetId()},
},
},
},
},
expectedAppIDs: []string{app1.GetApplicationId(), app2.GetApplicationId(), app3.GetApplicationId()},
},
{
testName: "when other projectOwner user should return empty list",
inputCtx: otherProjectOwnerCtx,
inputRequest: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{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, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
// When
res, err := instancePermissionV2.Client.ApplicationV2.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.GetApplicationId())
}
assert.ElementsMatch(ttt, tc.expectedAppIDs, resAppIDs)
}
}, retryDuration, tick)
})
}
}
func TestGetApplicationKey(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
createdApiApp := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
createdAppKey := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp.GetApplicationId(), time.Now().AddDate(0, 0, 1))
t.Parallel()
tt := []struct {
testName string
inputRequest *application.GetApplicationKeyRequest
inputCtx context.Context
expectedErrorType codes.Code
expectedAppKeyID string
}{
{
testName: "when unknown application ID should return not found error",
inputCtx: IAMOwnerCtx,
inputRequest: &application.GetApplicationKeyRequest{
KeyId: integration.ID(),
},
expectedErrorType: codes.NotFound,
},
{
testName: "when user has no permission should return membership not found error",
inputCtx: NoPermissionCtx,
inputRequest: &application.GetApplicationKeyRequest{
KeyId: createdAppKey.GetKeyId(),
},
expectedErrorType: codes.NotFound,
},
{
testName: "when providing API application ID should return valid API application result",
inputCtx: projectOwnerCtx,
inputRequest: &application.GetApplicationKeyRequest{
KeyId: createdAppKey.GetKeyId(),
},
expectedAppKeyID: createdAppKey.GetKeyId(),
},
{
testName: "when user is OrgOwner should return request key",
inputCtx: OrgOwnerCtx,
inputRequest: &application.GetApplicationKeyRequest{
KeyId: createdAppKey.GetKeyId(),
},
expectedAppKeyID: createdAppKey.GetKeyId(),
},
{
testName: "when user is IAMOwner should return request key",
inputCtx: OrgOwnerCtx,
inputRequest: &application.GetApplicationKeyRequest{
KeyId: createdAppKey.GetKeyId(),
},
expectedAppKeyID: createdAppKey.GetKeyId(),
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
// When
res, err := instance.Client.ApplicationV2.GetApplicationKey(tc.inputCtx, tc.inputRequest)
// Then
require.Equal(t, tc.expectedErrorType, status.Code(err))
if tc.expectedErrorType == codes.OK {
assert.Equal(t, tc.expectedAppKeyID, res.GetKeyId())
assert.NotEmpty(t, res.GetCreationDate())
assert.NotEmpty(t, res.GetExpirationDate())
}
}, retryDuration, tick)
})
}
}
func TestListApplicationKeys(t *testing.T) {
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
createdApiApp1 := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
createdApiApp2 := createAPIApp(t, IAMOwnerCtx, instance, p.GetId())
tomorrow := time.Now().AddDate(0, 0, 1)
in2Days := tomorrow.AddDate(0, 0, 1)
in3Days := in2Days.AddDate(0, 0, 1)
appKey1 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp1.GetApplicationId(), in2Days)
appKey2 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp1.GetApplicationId(), in3Days)
appKey3 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp1.GetApplicationId(), tomorrow)
appKey4 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp2.GetApplicationId(), tomorrow)
t.Parallel()
tt := []struct {
testName string
inputRequest *application.ListApplicationKeysRequest
deps func() (projectID, applicationID, organizationID string)
inputCtx context.Context
expectedErrorType codes.Code
expectedAppKeysIDs []string
}{
{
testName: "when sorting by expiration date should return keys sorted by expiration date ascending",
inputCtx: LoginUserCtx,
inputRequest: &application.ListApplicationKeysRequest{
Pagination: &filter.PaginationRequest{Asc: true},
SortingColumn: application.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_EXPIRATION,
Filters: []*application.ApplicationKeySearchFilter{
{Filter: &application.ApplicationKeySearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ApplicationKeyProjectIDFilter{ProjectId: p.GetId()},
}},
},
},
expectedAppKeysIDs: []string{appKey3.GetKeyId(), appKey4.GetKeyId(), appKey1.GetKeyId(), appKey2.GetKeyId()},
},
{
testName: "when sorting by creation date should return keys sorted by creation date descending",
inputCtx: IAMOwnerCtx,
inputRequest: &application.ListApplicationKeysRequest{
SortingColumn: application.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_CREATION_DATE,
Filters: []*application.ApplicationKeySearchFilter{
{Filter: &application.ApplicationKeySearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ApplicationKeyProjectIDFilter{ProjectId: p.GetId()},
}},
},
},
expectedAppKeysIDs: []string{appKey4.GetKeyId(), appKey3.GetKeyId(), appKey2.GetKeyId(), appKey1.GetKeyId()},
},
{
testName: "when filtering by application ID should return keys matching application ID sorted by ID",
inputCtx: projectOwnerCtx,
inputRequest: &application.ListApplicationKeysRequest{
Pagination: &filter.PaginationRequest{Asc: true},
Filters: []*application.ApplicationKeySearchFilter{
{Filter: &application.ApplicationKeySearchFilter_ApplicationIdFilter{
ApplicationIdFilter: &application.ApplicationKeyApplicationIDFilter{ApplicationId: createdApiApp1.GetApplicationId()},
}},
},
},
expectedAppKeysIDs: []string{appKey1.GetKeyId(), appKey2.GetKeyId(), appKey3.GetKeyId()},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
// When
res, err := instance.Client.ApplicationV2.ListApplicationKeys(tc.inputCtx, tc.inputRequest)
// Then
require.Equal(ttt, tc.expectedErrorType, status.Code(err))
if tc.expectedErrorType == codes.OK {
require.Len(ttt, res.GetKeys(), len(tc.expectedAppKeysIDs))
for i, k := range res.GetKeys() {
assert.Equal(ttt, tc.expectedAppKeysIDs[i], k.GetKeyId())
}
}
}, retryDuration, tick)
})
}
}
func TestListApplicationKeys_WithPermissionV2(t *testing.T) {
ensureFeaturePermissionV2Enabled(t, instancePermissionV2)
iamOwnerCtx := instancePermissionV2.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
loginUserCtx := instancePermissionV2.WithAuthorizationToken(context.Background(), integration.UserTypeLogin)
p, projectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx)
createdApiApp1 := createAPIApp(t, iamOwnerCtx, instancePermissionV2, p.GetId())
createdApiApp2 := createAPIApp(t, iamOwnerCtx, instancePermissionV2, p.GetId())
tomorrow := time.Now().AddDate(0, 0, 1)
in2Days := tomorrow.AddDate(0, 0, 1)
in3Days := in2Days.AddDate(0, 0, 1)
appKey1 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp1.GetApplicationId(), in2Days)
appKey2 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp1.GetApplicationId(), in3Days)
appKey3 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp1.GetApplicationId(), tomorrow)
appKey4 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp2.GetApplicationId(), tomorrow)
t.Parallel()
tt := []struct {
testName string
inputRequest *application.ListApplicationKeysRequest
deps func() (projectID, applicationID, organizationID string)
inputCtx context.Context
expectedErrorType codes.Code
expectedAppKeysIDs []string
}{
{
testName: "when sorting by expiration date should return keys sorted by expiration date ascending",
inputCtx: loginUserCtx,
inputRequest: &application.ListApplicationKeysRequest{
Pagination: &filter.PaginationRequest{Asc: true},
SortingColumn: application.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_EXPIRATION,
},
expectedAppKeysIDs: []string{appKey3.GetKeyId(), appKey4.GetKeyId(), appKey1.GetKeyId(), appKey2.GetKeyId()},
},
{
testName: "when sorting by creation date should return keys sorted by creation date descending",
inputCtx: iamOwnerCtx,
inputRequest: &application.ListApplicationKeysRequest{
SortingColumn: application.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_CREATION_DATE,
},
expectedAppKeysIDs: []string{appKey4.GetKeyId(), appKey3.GetKeyId(), appKey2.GetKeyId(), appKey1.GetKeyId()},
},
{
testName: "when filtering by application ID should return keys matching application ID sorted by ID",
inputCtx: projectOwnerCtx,
inputRequest: &application.ListApplicationKeysRequest{
Pagination: &filter.PaginationRequest{Asc: true},
Filters: []*application.ApplicationKeySearchFilter{
{Filter: &application.ApplicationKeySearchFilter_ApplicationIdFilter{
ApplicationIdFilter: &application.ApplicationKeyApplicationIDFilter{ApplicationId: createdApiApp1.GetApplicationId()},
}},
},
},
expectedAppKeysIDs: []string{appKey1.GetKeyId(), appKey2.GetKeyId(), appKey3.GetKeyId()},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// t.Parallel()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
// When
res, err := instancePermissionV2.Client.ApplicationV2.ListApplicationKeys(tc.inputCtx, tc.inputRequest)
// Then
require.Equal(ttt, tc.expectedErrorType, status.Code(err))
if tc.expectedErrorType == codes.OK {
require.Len(ttt, res.GetKeys(), len(tc.expectedAppKeysIDs))
for i, k := range res.GetKeys() {
assert.Equal(ttt, tc.expectedAppKeysIDs[i], k.GetKeyId())
}
}
}, retryDuration, tick)
})
}
}

View File

@@ -0,0 +1,219 @@
//go:build integration
package app_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
"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(), integration.ProjectName(), 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(`<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
validUntil="2022-08-26T14:08:16Z"
cacheDuration="PT604800S"
entityID="%s">
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://test.com/saml/acs"
index="1" />
</md:SPSSODescriptor>
</md:EntityDescriptor>
`,
entityID)
return []byte(str)
}
func createSAMLAppWithName(t *testing.T, baseURI, projectID string) ([]byte, *application.CreateApplicationResponse, string) {
samlMetas := samlMetadataGen(integration.URL())
appName := integration.ApplicationName()
appForSAMLConfigChange, appSAMLConfigChangeErr := instance.Client.ApplicationV2.CreateApplication(IAMOwnerCtx, &application.CreateApplicationRequest{
ProjectId: projectID,
Name: appName,
ApplicationType: &application.CreateApplicationRequest_SamlConfiguration{
SamlConfiguration: &application.CreateSAMLApplicationRequest{
Metadata: &application.CreateSAMLApplicationRequest_MetadataXml{
MetadataXml: samlMetas,
},
LoginVersion: &application.LoginVersion{
Version: &application.LoginVersion_LoginV2{
LoginV2: &application.LoginV2{
BaseUri: &baseURI,
},
},
},
},
},
})
require.Nil(t, appSAMLConfigChangeErr)
return samlMetas, appForSAMLConfigChange, appName
}
func createSAMLApp(t *testing.T, baseURI, projectID string) ([]byte, *application.CreateApplicationResponse) {
metas, app, _ := createSAMLAppWithName(t, baseURI, projectID)
return metas, app
}
func createOIDCAppWithName(t *testing.T, baseURI, projectID string) (*application.CreateApplicationResponse, string) {
appName := integration.ApplicationName()
appForOIDCConfigChange, appOIDCConfigChangeErr := instance.Client.ApplicationV2.CreateApplication(IAMOwnerCtx, &application.CreateApplicationRequest{
ProjectId: projectID,
Name: appName,
ApplicationType: &application.CreateApplicationRequest_OidcConfiguration{
OidcConfiguration: &application.CreateOIDCApplicationRequest{
RedirectUris: []string{"http://example.com"},
ResponseTypes: []application.OIDCResponseType{application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
GrantTypes: []application.OIDCGrantType{application.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
ApplicationType: application.OIDCApplicationType_OIDC_APP_TYPE_WEB,
AuthMethodType: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
PostLogoutRedirectUris: []string{"http://example.com/home"},
Version: application.OIDCVersion_OIDC_VERSION_1_0,
AccessTokenType: application.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
BackChannelLogoutUri: "http://example.com/logout",
LoginVersion: &application.LoginVersion{
Version: &application.LoginVersion_LoginV2{
LoginV2: &application.LoginV2{
BaseUri: &baseURI,
},
},
},
},
},
})
require.Nil(t, appOIDCConfigChangeErr)
return appForOIDCConfigChange, appName
}
func createOIDCApp(t *testing.T, baseURI, projctID string) *application.CreateApplicationResponse {
app, _ := createOIDCAppWithName(t, baseURI, projctID)
return app
}
func createAPIAppWithName(t *testing.T, ctx context.Context, inst *integration.Instance, projectID string) (*application.CreateApplicationResponse, string) {
appName := integration.ApplicationName()
reqForAPIAppCreation := &application.CreateApplicationRequest_ApiConfiguration{
ApiConfiguration: &application.CreateAPIApplicationRequest{AuthMethodType: application.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
}
appForAPIConfigChange, appAPIConfigChangeErr := inst.Client.ApplicationV2.CreateApplication(ctx, &application.CreateApplicationRequest{
ProjectId: projectID,
Name: appName,
ApplicationType: reqForAPIAppCreation,
})
require.Nil(t, appAPIConfigChangeErr)
return appForAPIConfigChange, appName
}
func createAPIApp(t *testing.T, ctx context.Context, inst *integration.Instance, projectID string) *application.CreateApplicationResponse {
res, _ := createAPIAppWithName(t, ctx, inst, projectID)
return res
}
func deactivateApp(t *testing.T, appToDeactivate *application.CreateApplicationResponse, projectID string) {
_, appDeactivateErr := instance.Client.ApplicationV2.DeactivateApplication(IAMOwnerCtx, &application.DeactivateApplicationRequest{
ProjectId: projectID,
ApplicationId: appToDeactivate.GetApplicationId(),
})
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")
}
func createAppKey(t *testing.T, ctx context.Context, inst *integration.Instance, projectID, appID string, expirationDate time.Time) *application.CreateApplicationKeyResponse {
res, err := inst.Client.ApplicationV2.CreateApplicationKey(ctx,
&application.CreateApplicationKeyRequest{
ApplicationId: appID,
ProjectId: projectID,
ExpirationDate: timestamppb.New(expirationDate.UTC()),
},
)
require.Nil(t, err)
return res
}

View File

@@ -0,0 +1,72 @@
package app
import (
"context"
"strings"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/application/v2/convert"
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
)
func (s *Server) GetApplication(ctx context.Context, req *connect.Request[application.GetApplicationRequest]) (*connect.Response[application.GetApplicationResponse], error) {
res, err := s.query.AppByIDWithPermission(ctx, req.Msg.GetApplicationId(), false, s.checkPermission)
if err != nil {
return nil, err
}
return connect.NewResponse(&application.GetApplicationResponse{
Application: convert.AppToPb(res),
}), nil
}
func (s *Server) ListApplications(ctx context.Context, req *connect.Request[application.ListApplicationsRequest]) (*connect.Response[application.ListApplicationsResponse], error) {
queries, err := convert.ListApplicationsRequestToModel(s.systemDefaults, req.Msg)
if err != nil {
return nil, err
}
res, err := s.query.SearchApps(ctx, queries, s.checkPermission)
if err != nil {
return nil, err
}
return connect.NewResponse(&application.ListApplicationsResponse{
Applications: convert.AppsToPb(res.Apps),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, res.SearchResponse),
}), nil
}
func (s *Server) GetApplicationKey(ctx context.Context, req *connect.Request[application.GetApplicationKeyRequest]) (*connect.Response[application.GetApplicationKeyResponse], error) {
key, err := s.query.GetAuthNKeyByIDWithPermission(ctx, true, strings.TrimSpace(req.Msg.GetKeyId()), s.checkPermission)
if err != nil {
return nil, err
}
return connect.NewResponse(&application.GetApplicationKeyResponse{
KeyId: key.ID,
CreationDate: timestamppb.New(key.CreationDate),
ExpirationDate: timestamppb.New(key.Expiration),
}), nil
}
func (s *Server) ListApplicationKeys(ctx context.Context, req *connect.Request[application.ListApplicationKeysRequest]) (*connect.Response[application.ListApplicationKeysResponse], error) {
queries, err := convert.ListApplicationKeysRequestToDomain(s.systemDefaults, req.Msg)
if err != nil {
return nil, err
}
res, err := s.query.SearchAuthNKeys(ctx, queries, query.JoinFilterUnspecified, s.checkPermission)
if err != nil {
return nil, err
}
return connect.NewResponse(&application.ListApplicationKeysResponse{
Keys: convert.ApplicationKeysToPb(res.AuthNKeys),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, res.SearchResponse),
}), nil
}

View File

@@ -0,0 +1,59 @@
package app
import (
"net/http"
"connectrpc.com/connect"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/zitadel/zitadel/internal/api/authz"
"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"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
"github.com/zitadel/zitadel/pkg/grpc/application/v2/applicationconnect"
)
var _ applicationconnect.ApplicationServiceHandler = (*Server)(nil)
type Server struct {
command *command.Commands
query *query.Queries
systemDefaults systemdefaults.SystemDefaults
checkPermission domain.PermissionCheck
}
func CreateServer(
systemDefaults systemdefaults.SystemDefaults,
command *command.Commands,
query *query.Queries,
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
command: command,
query: query,
checkPermission: checkPermission,
systemDefaults: systemDefaults,
}
}
func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) {
return applicationconnect.NewApplicationServiceHandler(s, connect.WithInterceptors(interceptors...))
}
func (s *Server) FileDescriptor() protoreflect.FileDescriptor {
return application.File_zitadel_application_v2_application_service_proto
}
func (s *Server) AppName() string {
return application.ApplicationService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return application.ApplicationService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return application.ApplicationService_AuthMethods
}

View File

@@ -212,6 +212,7 @@ func (c *Commands) UpdateAPIApplication(ctx context.Context, apiApp *domain.APIA
return apiWriteModelToAPIConfig(existingAPI), nil
}
// Deprecated: use [ChangeApplicationSecret], which supports both OIDC and API applications.
func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, appID, resourceOwner string) (*domain.APIApp, error) {
if projectID == "" || appID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-99i83", "Errors.IDMissing")

View File

@@ -318,6 +318,7 @@ func (c *Commands) UpdateOIDCApplication(ctx context.Context, oidc *domain.OIDCA
return result, nil
}
// Deprecated: use [ChangeApplicationSecret], which supports both OIDC and API applications.
func (c *Commands) ChangeOIDCApplicationSecret(ctx context.Context, projectID, appID, resourceOwner string) (*domain.OIDCApp, error) {
if projectID == "" || appID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-99i83", "Errors.IDMissing")

View File

@@ -0,0 +1,59 @@
package command
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/eventstore"
project_repo "github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) ChangeApplicationSecret(ctx context.Context, projectID, applicationID, resourceOwner string) (secret string, changeDate time.Time, err error) {
if projectID == "" || applicationID == "" {
return "", time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-KJ29c", "Errors.IDMissing")
}
existingApplication, err := c.getApplicationSecretWriteModel(ctx, projectID, applicationID, resourceOwner)
if err != nil {
return "", time.Time{}, err
}
if !existingApplication.State.Exists() {
return "", time.Time{}, zerrors.ThrowNotFound(nil, "COMMAND-Kd92s", "Errors.Project.App.NotExisting")
}
if err := c.checkPermissionUpdateApplication(ctx, existingApplication.ResourceOwner, existingApplication.AggregateID); err != nil {
return "", time.Time{}, err
}
encodedHash, plain, err := c.newHashedSecret(ctx, c.eventstore.Filter) //nolint:staticcheck
if err != nil {
return "", time.Time{}, err
}
projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingApplication.WriteModel)
var command eventstore.Command
command = project_repo.NewOIDCConfigSecretChangedEvent(ctx, projectAgg, applicationID, encodedHash)
if existingApplication.IsAPI {
command = project_repo.NewAPIConfigSecretChangedEvent(ctx, projectAgg, applicationID, encodedHash)
}
if err = c.pushAppendAndReduce(ctx, existingApplication, command); err != nil {
return "", time.Time{}, err
}
return plain, existingApplication.ChangeDate, nil
}
func (c *Commands) getApplicationSecretWriteModel(ctx context.Context, projectID, applicationID, resourceOwner string) (_ *ApplicationSecretWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
appWriteModel := NewApplicationSecretWriteModel(projectID, applicationID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, appWriteModel)
if err != nil {
return nil, err
}
return appWriteModel, nil
}

View File

@@ -0,0 +1,158 @@
package command
import (
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/project"
)
type ApplicationSecretWriteModel struct {
eventstore.WriteModel
ApplicationID string
ClientID string
HashedSecret string
State domain.AppState
SecretAllowed bool
IsAPI bool
}
func NewApplicationSecretWriteModel(projectID, applicationID, resourceOwner string) *ApplicationSecretWriteModel {
return &ApplicationSecretWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: projectID,
ResourceOwner: resourceOwner,
},
ApplicationID: applicationID,
}
}
func (wm *ApplicationSecretWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *project.ApplicationRemovedEvent:
if e.AppID != wm.ApplicationID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.OIDCConfigAddedEvent:
if e.AppID != wm.ApplicationID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.OIDCConfigChangedEvent:
if e.AppID != wm.ApplicationID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.APIConfigAddedEvent:
if e.AppID != wm.ApplicationID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.APIConfigChangedEvent:
if e.AppID != wm.ApplicationID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.OIDCConfigSecretChangedEvent:
if e.AppID != wm.ApplicationID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.OIDCConfigSecretHashUpdatedEvent:
if e.AppID != wm.ApplicationID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.APIConfigSecretChangedEvent:
if e.AppID != wm.ApplicationID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.APIConfigSecretHashUpdatedEvent:
if e.AppID != wm.ApplicationID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.ProjectRemovedEvent:
wm.WriteModel.AppendEvents(e)
}
}
}
func (wm *ApplicationSecretWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *project.ApplicationRemovedEvent:
wm.State = domain.AppStateRemoved
case *project.OIDCConfigAddedEvent:
wm.appendAddOIDCEvent(e)
case *project.OIDCConfigChangedEvent:
wm.appendChangeOIDCEvent(e)
case *project.APIConfigAddedEvent:
wm.appendAddAPIEvent(e)
case *project.APIConfigChangedEvent:
wm.appendChangeAPIEvent(e)
case *project.OIDCConfigSecretChangedEvent:
wm.HashedSecret = crypto.SecretOrEncodedHash(e.ClientSecret, e.HashedSecret)
case *project.OIDCConfigSecretHashUpdatedEvent:
wm.HashedSecret = e.HashedSecret
case *project.APIConfigSecretChangedEvent:
wm.HashedSecret = crypto.SecretOrEncodedHash(e.ClientSecret, e.HashedSecret)
case *project.APIConfigSecretHashUpdatedEvent:
wm.HashedSecret = e.HashedSecret
case *project.ProjectRemovedEvent:
wm.State = domain.AppStateRemoved
}
}
return wm.WriteModel.Reduce()
}
func (wm *ApplicationSecretWriteModel) appendAddOIDCEvent(e *project.OIDCConfigAddedEvent) {
wm.State = domain.AppStateActive
wm.ClientID = e.ClientID
wm.SecretAllowed = e.AuthMethodType == domain.OIDCAuthMethodTypeBasic || e.AuthMethodType == domain.OIDCAuthMethodTypePost
wm.IsAPI = false
}
func (wm *ApplicationSecretWriteModel) appendChangeOIDCEvent(e *project.OIDCConfigChangedEvent) {
if e.AuthMethodType != nil {
wm.SecretAllowed = *e.AuthMethodType == domain.OIDCAuthMethodTypeBasic || *e.AuthMethodType == domain.OIDCAuthMethodTypePost
}
}
func (wm *ApplicationSecretWriteModel) appendAddAPIEvent(e *project.APIConfigAddedEvent) {
wm.State = domain.AppStateActive
wm.ClientID = e.ClientID
wm.SecretAllowed = e.AuthMethodType == domain.APIAuthMethodTypeBasic
wm.IsAPI = true
}
func (wm *ApplicationSecretWriteModel) appendChangeAPIEvent(e *project.APIConfigChangedEvent) {
if e.AuthMethodType != nil {
wm.SecretAllowed = *e.AuthMethodType == domain.APIAuthMethodTypeBasic
}
}
func (wm *ApplicationSecretWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(project.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
project.ApplicationRemovedType,
project.OIDCConfigAddedType,
project.OIDCConfigChangedType,
project.APIConfigAddedType,
project.APIConfigChangedType,
project.OIDCConfigSecretChangedType,
project.OIDCConfigSecretHashUpdatedType,
project.APIConfigSecretChangedType,
project.APIConfigSecretHashUpdatedType,
project.ProjectRemovedType).
Builder()
}

View File

@@ -0,0 +1,206 @@
package command
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestCommandSide_ChangeApplicationSecret(t *testing.T) {
t.Parallel()
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
appID string
projectID string
resourceOwner string
}
type res struct {
wantSecret string
wantChangeDate time.Time
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "no projectid, invalid argument error",
fields: fields{
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
appID: "app1",
resourceOwner: "org1",
},
res: res{
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-KJ29c", "Errors.IDMissing"),
},
},
{
name: "no appid, invalid argument error",
fields: fields{
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
projectID: "project1",
appID: "",
resourceOwner: "org1",
},
res: res{
err: zerrors.ThrowInvalidArgumentf(nil, "COMMAND-KJ29c", "Errors.IDMissing"),
},
},
{
name: "app not existing, not found error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
appID: "app1",
resourceOwner: "org1",
},
res: res{
err: zerrors.ThrowNotFound(nil, "COMMAND-Kd92s", "Errors.Project.App.NotExisting"),
},
},
{
name: "change secret (OIDC), ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewOIDCConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
domain.OIDCVersionV1,
"app1",
"client1@project",
"secret",
[]string{"https://test.ch"},
[]domain.OIDCResponseType{domain.OIDCResponseTypeCode},
[]domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
domain.OIDCApplicationTypeWeb,
domain.OIDCAuthMethodTypePost,
[]string{"https://test.ch/logout"},
true,
domain.OIDCTokenTypeBearer,
true,
true,
true,
time.Second*1,
[]string{"https://sub.test.ch"},
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
),
expectPush(
project.NewOIDCConfigSecretChangedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"secret",
),
),
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
appID: "app1",
resourceOwner: "org1",
},
res: res{
wantSecret: "secret",
},
},
{
name: "change secret (API), ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewAPIConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"client1@project",
"secret",
domain.APIAuthMethodTypeBasic,
),
),
),
expectPush(
project.NewAPIConfigSecretChangedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"secret",
),
),
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
appID: "app1",
resourceOwner: "org1",
},
res: res{
wantSecret: "secret",
},
},
}
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(),
}
now := time.Now()
gotSecret, gotChangeDate, err := r.ChangeApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.wantSecret, gotSecret)
if tt.res.wantChangeDate != (time.Time{}) {
assert.WithinRange(t, gotChangeDate, now, time.Now())
}
})
}
}

View File

@@ -124,6 +124,7 @@ func isProjectStateExists(state domain.ProjectState) bool {
return !slices.Contains([]domain.ProjectState{domain.ProjectStateRemoved, domain.ProjectStateUnspecified}, state)
}
// Deprecated: use ProjectAggregateFromWriteModelWithCTX
func ProjectAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return eventstore.AggregateFromWriteModel(wm, project.AggregateType, project.AggregateVersion)
}

View File

@@ -24,7 +24,8 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/action/v2"
action_v2beta "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/admin"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
app_v2beta "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
"github.com/zitadel/zitadel/pkg/grpc/auth"
authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
@@ -85,7 +86,8 @@ type Client struct {
SCIM *scim.Client
Projectv2Beta project_v2beta.ProjectServiceClient
InstanceV2Beta instance.InstanceServiceClient
AppV2Beta app.AppServiceClient
AppV2Beta app_v2beta.AppServiceClient
ApplicationV2 application.ApplicationServiceClient
InternalPermissionv2Beta internal_permission_v2beta.InternalPermissionServiceClient
InternalPermissionV2 internal_permission_v2.InternalPermissionServiceClient
AuthorizationV2Beta authorization.AuthorizationServiceClient
@@ -131,7 +133,8 @@ 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),
AppV2Beta: app_v2beta.NewAppServiceClient(cc),
ApplicationV2: application.NewApplicationServiceClient(cc),
InternalPermissionv2Beta: internal_permission_v2beta.NewInternalPermissionServiceClient(cc),
InternalPermissionV2: internal_permission_v2.NewInternalPermissionServiceClient(cc),
AuthorizationV2Beta: authorization.NewAuthorizationServiceClient(cc),

View File

@@ -0,0 +1,5 @@
package application
type IsApplicationConfiguration = isApplication_Configuration
type MetaType = isUpdateSAMLApplicationConfigurationRequest_Metadata

View File

@@ -112,12 +112,15 @@ service AppService {
// Create Application
//
// Deprecated: use [application service v2 CreateApplication](apis/resources/application_service_v2/application-service-create-application.api.mdx) instead.
//
// Create an application. The application can be OIDC, API or SAML type, based on the input.
//
// Required permissions:
// - project.app.write
rpc CreateApplication(CreateApplicationRequest) returns (CreateApplicationResponse) {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {
@@ -140,6 +143,8 @@ service AppService {
// Update Application
//
// Deprecated: use [application service v2 UpdateApplication](apis/resources/application_service_v2/zitadel-app-v-2-application-service-update-application.api.mdx) instead.
//
// Changes the configuration of an OIDC, API or SAML type application, as well as
// the application name, based on the input provided.
//
@@ -147,6 +152,7 @@ service AppService {
// - project.app.write
rpc UpdateApplication(UpdateApplicationRequest) returns (UpdateApplicationResponse) {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {
@@ -169,12 +175,15 @@ service AppService {
// Get Application
//
// Deprecated: use [application service v2 GetApplication](apis/resources/application_service_v2/application-service-get-application.api.mdx) instead.
//
// Retrieves the application matching the provided ID.
//
// Required permissions:
// - project.app.read
rpc GetApplication(GetApplicationRequest) returns (GetApplicationResponse) {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {
@@ -196,6 +205,8 @@ service AppService {
// Delete Application
//
// Deprecated: use [application service v2 DeleteApplication](apis/resources/application_service_v2/application-service-delete-application.api.mdx) instead.
//
// Deletes the application belonging to the input project and matching the provided
// application ID.
//
@@ -207,6 +218,7 @@ service AppService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {
@@ -224,6 +236,8 @@ service AppService {
// Deactivate Application
//
// Deprecated: use [application service v2 DeactivateApplication](apis/resources/application_service_v2/application-service-deactivate-application.api.mdx) instead.
//
// Deactivates the application belonging to the input project and matching the provided
// application ID.
//
@@ -236,6 +250,7 @@ service AppService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {
@@ -253,6 +268,8 @@ service AppService {
// Reactivate Application
//
// Deprecated: use [application service v2 ReactivateApplication](apis/resources/application_service_v2/application-service-reactivate-application.api.mdx) instead.
//
// Reactivates the application belonging to the input project and matching the provided
// application ID.
//
@@ -265,6 +282,7 @@ service AppService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {
@@ -283,6 +301,8 @@ service AppService {
// Regenerate Client Secret
//
// Deprecated: use [application service v2 GenerateClientSecret](apis/resources/application_service_v2/application-service-generate-client-secret.api.mdx) instead.
//
// Regenerates the client secret of an API or OIDC application that belongs to the input project.
//
// Required permissions:
@@ -294,6 +314,7 @@ service AppService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {
@@ -311,6 +332,8 @@ service AppService {
// List Applications
//
// Deprecated: use [application service v2 ListApplications](apis/resources/application_service_v2/application-service-list-applications.api.mdx) instead.
//
// Returns a list of applications matching the input parameters that belong to the provided
// project.
//
@@ -326,6 +349,7 @@ service AppService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {
@@ -344,6 +368,8 @@ service AppService {
// Create Application Key
//
// Deprecated: use [application service v2 CreateApplicationKey](apis/resources/application_service_v2/application-service-create-application-key.api.mdx) instead.
//
// Create a new application key, which is used to authorize an API application.
//
// Key details are returned in the response. They must be stored safely, as it will not
@@ -364,6 +390,7 @@ service AppService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {
@@ -375,6 +402,8 @@ service AppService {
// Delete Application Key
//
// Deprecated: use [application service v2 DeleteApplicationKey](apis/resources/application_service_v2/application-service-delete-application-key.api.mdx) instead.
//
// Deletes an application key matching the provided ID.
//
// Organization ID is not mandatory, but helps with filtering/performance.
@@ -395,6 +424,7 @@ service AppService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {
@@ -406,6 +436,8 @@ service AppService {
// Get Application Key
//
// Deprecated: use [application service v2 GetApplicationKey](apis/resources/application_service_v2/application-service-get-application-key.api.mdx) instead.
//
// Retrieves the application key matching the provided ID.
//
// Specifying a project, organization and app ID is optional but help with filtering/performance.
@@ -414,6 +446,7 @@ service AppService {
// - project.app.read
rpc GetApplicationKey(GetApplicationKeyRequest) returns (GetApplicationKeyResponse) {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {
@@ -435,6 +468,8 @@ service AppService {
// List Application Keys
//
// Deprecated: use [application service v2 ListApplicationKeys](apis/resources/application_service_v2/application-service-list-application-keys.api.mdx) instead.
//
// Returns a list of application keys matching the input parameters.
//
// The result can be sorted by id, aggregate, creation date, expiration date, resource owner or type.
@@ -449,6 +484,7 @@ service AppService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200";
value: {

View File

@@ -0,0 +1,21 @@
syntax = "proto3";
package zitadel.application.v2;
import "protoc-gen-openapiv2/options/annotations.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/application/v2;application";
enum APIAuthMethodType {
API_AUTH_METHOD_TYPE_BASIC = 0;
API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT = 1;
}
message APIConfiguration {
// The unique OAuth2 client_id used for authentication of the API,
// e.g. at the introspection endpoint.
string client_id = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629023906488334@ZITADEL\""}];
// The authentication method type used by the API to authenticate at the introspection endpoint.
APIAuthMethodType auth_method_type = 2;
}

View File

@@ -0,0 +1,181 @@
syntax = "proto3";
package zitadel.application.v2;
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/application/v2/api.proto";
import "zitadel/application/v2/oidc.proto";
import "zitadel/application/v2/saml.proto";
import "zitadel/filter/v2/filter.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/application/v2;application";
message Application {
// The unique identifier of the application.
string application_id = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629023906488334\""}];
// The timestamp of the application 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 last update to the application.
google.protobuf.Timestamp change_date = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2024-12-18T07:50:47.492Z\""}];
// The current state of the application.
ApplicationState state = 4;
// The name of the application. This can be displayed to users.
string name = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"Console\""}];
// Depending on the application type, this field contains the specific configuration.
// Only one of the configurations will be set.
oneof configuration {
OIDCConfiguration oidc_configuration = 6;
APIConfiguration api_configuration = 7;
SAMLConfiguration saml_configuration = 8;
}
}
enum ApplicationState {
APPLICATION_STATE_UNSPECIFIED = 0;
APPLICATION_STATE_ACTIVE = 1;
APPLICATION_STATE_INACTIVE = 2;
APPLICATION_STATE_REMOVED = 3;
}
enum ApplicationSorting {
APPLICATION_SORT_BY_ID = 0;
APPLICATION_SORT_BY_NAME = 1;
APPLICATION_SORT_BY_STATE = 2;
APPLICATION_SORT_BY_CREATION_DATE = 3;
APPLICATION_SORT_BY_CHANGE_DATE = 4;
}
message ApplicationSearchFilter {
oneof filter {
option (validate.required) = true;
// Filter the projectID the application has to belong to.
ProjectIDFilter project_id_filter = 1;
// Filter the applications by their name.
ApplicationNameFilter name_filter = 2;
// Filter the applications by their state.
ApplicationState state_filter = 3;
// Filter for specific application types.
ApplicationType type_filter = 4 [(validate.rules).enum = {defined_only: true, not_in:[0]}];
}
}
message ProjectIDFilter {
// Search for application belonging to the project with this ID.
string project_id = 1 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629023906488334\""},
(google.api.field_behavior) = REQUIRED
];
}
message ApplicationNameFilter {
// The name of the application to search for.
string name = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"Conso\""}
];
// The method to use for text comparison. If not set, defaults to EQUALS.
zitadel.filter.v2.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true];
}
enum ApplicationType {
APPLICATION_TYPE_UNSPECIFIED = 0;
APPLICATION_TYPE_OIDC = 1;
APPLICATION_TYPE_API = 2;
APPLICATION_TYPE_SAML = 3;
}
enum ApplicationKeysSorting {
APPLICATION_KEYS_SORT_BY_ID = 0;
APPLICATION_KEYS_SORT_BY_PROJECT_ID = 1;
APPLICATION_KEYS_SORT_BY_APPLICATION_ID = 2;
APPLICATION_KEYS_SORT_BY_CREATION_DATE = 3;
APPLICATION_KEYS_SORT_BY_ORGANIZATION_ID = 4;
APPLICATION_KEYS_SORT_BY_EXPIRATION = 5;
APPLICATION_KEYS_SORT_BY_TYPE = 6;
}
message ApplicationKeySearchFilter {
oneof filter {
option (validate.required) = true;
// Filter the application keys by the application ID they belong to.
ApplicationKeyApplicationIDFilter application_id_filter = 1;
// Filter the application keys by the project ID the corresponding application belong to.
ApplicationKeyProjectIDFilter project_id_filter = 2;
// Filter the application keys by the organization ID the corresponding application belong to.
ApplicationKeyOrganizationIDFilter organization_id_filter = 3;
}
}
message ApplicationKeyApplicationIDFilter {
// Search for application keys belonging to the application with this ID.
string application_id = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629023906488334\""},
(google.api.field_behavior) = REQUIRED
];
}
message ApplicationKeyProjectIDFilter {
// Search for application keys belonging to applications in the project with this ID.
string project_id = 1 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629023906488334\""},
(google.api.field_behavior) = REQUIRED
];
}
message ApplicationKeyOrganizationIDFilter {
// Search for application keys belonging to applications in the organization with this ID.
string organization_id = 1 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629023906488334\""},
(google.api.field_behavior) = REQUIRED
];
}
message ApplicationKey {
// The unique identifier of the application key.
string key_id = 1;
// The identifier of the application this key belongs to.
string application_id = 2;
// The identifier of the project this application belongs to.
string project_id = 3;
// The timestamp of the key creation.
google.protobuf.Timestamp creation_date = 4 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2024-12-18T07:50:47.492Z\""}];
// The identifier of the organization this application belongs to.
string organization_id = 5;
// The timestamp the key expires.
google.protobuf.Timestamp expiration_date = 6 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2024-12-18T07:50:47.492Z\""}];
}

View File

@@ -0,0 +1,849 @@
syntax = "proto3";
package zitadel.application.v2;
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/application/v2/api.proto";
import "zitadel/application/v2/application.proto";
import "zitadel/application/v2/login.proto";
import "zitadel/application/v2/oidc.proto";
import "zitadel/filter/v2/filter.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/application/v2;application";
// Service to manage applications.
// This service provides methods to create, update, delete and list application and application keys.
service ApplicationService {
// Create Application
//
// Create an application. The application can be OIDC, API or SAML type, based on the input.
//
// Required permissions:
// - project.app.write
rpc CreateApplication(CreateApplicationRequest) returns (CreateApplicationResponse) {
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.
//
// Required permissions:
// - project.app.write
rpc UpdateApplication(UpdateApplicationRequest) returns (UpdateApplicationResponse) {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {permission: "authenticated"}
};
}
// Get Application
//
// Retrieves the application matching the provided ID.
//
// Required permissions:
// - project.app.read
rpc GetApplication(GetApplicationRequest) returns (GetApplicationResponse) {
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.
//
// Required permissions:
// - project.app.delete
rpc DeleteApplication(DeleteApplicationRequest) returns (DeleteApplicationResponse) {
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.
//
// Required permissions:
// - project.app.write
rpc DeactivateApplication(DeactivateApplicationRequest) returns (DeactivateApplicationResponse) {
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.
//
// Required permissions:
// - project.app.write
rpc ReactivateApplication(ReactivateApplicationRequest) returns (ReactivateApplicationResponse) {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {permission: "authenticated"}
};
}
// Generate Client Secret
//
// Generates the client secret of an API or OIDC application that belongs to the input project.
//
// Required permissions:
// - project.app.write
rpc GenerateClientSecret(GenerateClientSecretRequest) returns (GenerateClientSecretResponse) {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {permission: "authenticated"}
};
}
// List Applications
//
// Returns a list of applications matching the input parameters. The results can be filtered
// by project, state, type and name. It can be sorted by id, name, creation date, change date or state.
//
// Required permissions:
// - project.app.read
rpc ListApplications(ListApplicationsRequest) returns (ListApplicationsResponse) {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {permission: "authenticated"}
};
}
// Create Application Key
//
// Create a new application key, which is used to authorize an API application.
//
// Key details are returned in the response. They must be stored safely, as it will not
// be possible to retrieve them again.
//
// Required permissions:
// - `project.app.write`
rpc CreateApplicationKey(CreateApplicationKeyRequest) returns (CreateApplicationKeyResponse) {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {permission: "authenticated"}
};
}
// Delete Application Key
//
// Deletes an application key matching the provided ID.
//
// Organization ID is not mandatory, but helps with filtering/performance.
//
// The deletion time is returned in response message.
//
// Required permissions:
// - `project.app.write`
rpc DeleteApplicationKey(DeleteApplicationKeyRequest) returns (DeleteApplicationKeyResponse) {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {permission: "authenticated"}
};
}
// Get Application Key
//
// Retrieves the application key matching the provided ID.
//
// Specifying a project, organization and application ID is optional but help with filtering/performance.
//
// Required permissions:
// - project.app.read
rpc GetApplicationKey(GetApplicationKeyRequest) returns (GetApplicationKeyResponse) {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {permission: "authenticated"}
};
}
// List Application Keys
//
// Returns a list of application keys matching the input parameters.
//
// The result can be sorted by id, aggregate, creation date, expiration date, resource owner or type.
// It can also be filtered by application, project or organization ID.
//
// Required permissions:
// - project.app.read
rpc ListApplicationKeys(ListApplicationKeysRequest) returns (ListApplicationKeysResponse) {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {permission: "authenticated"}
};
}
}
message CreateApplicationRequest {
// The ID of the project the application will be created in.
string project_id = 1 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
// Optionally, provide the unique ID of the new application. If omitted, the system will generate one for you,
// which is the recommended way. The generated ID will be returned in the response.
string application_id = 2 [(validate.rules).string = {max_len: 200}];
// Publicly visible name of the application. This might be presented to users if they sign in.
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\""
}
];
// Defines the type of application to be created: OIDC, SAML or API and allows you
// to provide the configuration for the respective application type.
oneof application_type {
option (validate.required) = true;
CreateOIDCApplicationRequest oidc_configuration = 4;
CreateSAMLApplicationRequest saml_configuration = 5;
CreateAPIApplicationRequest api_configuration = 6;
}
}
message CreateApplicationResponse {
// The unique ID of the newly created application.
string application_id = 1;
// The timestamp of the application creation.
google.protobuf.Timestamp creation_date = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2024-12-18T07:50:47.492Z\""}];
// Depending on the application type, the response contains the relevant configuration details.
// This can include client IDs, secrets, or other generated information necessary for the application to function.
oneof application_type {
CreateOIDCApplicationResponse oidc_configuration = 3;
CreateSAMLApplicationResponse saml_configuration = 4;
CreateAPIApplicationResponse api_configuration = 5;
}
}
message CreateOIDCApplicationRequest {
// RedirectURIs are the allowed callback URIs for the OAuth2 / OIDC flows,
// where the authorization code or tokens will be sent to.
// The redirect_uri parameter in the authorization request must exactly match one of these URIs.
repeated string redirect_uris = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"http://localhost:4200/auth/callback\"]"}];
// ResponseTypes define whether a code, id_token token or just id_token will be returned.
// The response_type parameter in the authorization request must exactly match one of these values.
repeated OIDCResponseType response_types = 2 [(validate.rules).repeated = {
items: {
enum: {
defined_only: true
not_in: [0] /* OIDC_RESPONSE_TYPE_UNSPECIFIED is not allowed */
}
}
}];
// GrantTypes define the flow type the application is allowed to use.
// The grant_type parameter in the token request must exactly match one of these values.
// Minimum one grant type must be provided, but multiple grant types can be provided to allow
// different flows, e.g. authorization code flow and refresh token flow.
repeated OIDCGrantType grant_types = 3 [(validate.rules).repeated = {
items: {
enum: {defined_only: true}
}
}];
// ApplicationType defines the OAuth2/OIDC client type and their ability to maintain
// confidentiality of their credentials.
// This influences the allowed grant types and the required authentication method.
OIDCApplicationType application_type = 4 [(validate.rules).enum = {defined_only: true}];
// The authentication method type used by the application to authenticate at the token endpoint.
OIDCAuthMethodType auth_method_type = 5 [(validate.rules).enum = {defined_only: true}];
// PostLogoutRedirectURIs are the allowed URIs to redirect to after a logout.
// The post_logout_redirect_uri parameter in the logout request must exactly match one of these URIs.
repeated string post_logout_redirect_uris = 6 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"http://localhost:4200/signedout\"]"}];
// Version defines the OIDC version used by the application.
// Currently, only version 1.0 is supported.
// Future versions might introduce breaking changes.
OIDCVersion version = 7 [(validate.rules).enum = {defined_only: true}];
// DevelopmentMode can be enabled for development purposes. This allows the use of
// OIDC non-compliant and potentially insecure settings, such as the use of
// HTTP redirect URIs or wildcard redirect URIs.
bool development_mode = 8;
// The AccessTokenType defines the type of the access token returned from ZITADEL.
// Bearer tokens are opaque to clients. JWT tokens are self-contained and can be validated by the client.
// Bearer tokens must be introspected at the ZITADEL token endpoint.
OIDCTokenType access_token_type = 9 [(validate.rules).enum = {defined_only: true}];
// If AccessTokenRoleAssertion is enabled, the roles of the user are added to the access token.
// Ensure that the access token is a JWT token and not a bearer token. And either request the roles
// by scope or enable the user role assertion on the project.
bool access_token_role_assertion = 10;
// If IDTokenRoleAssertion is enabled, the roles of the user are added to the id token.
// Ensure that either the roles are requested by scope or enable the user role assertion on the
// project.
bool id_token_role_assertion = 11;
// If IDTokenUserinfoAssertion is enabled, the claims of profile, email, address and phone scopes
// are added to the id token even if an access token is issued. This can be required by some applications
// that do not call the userinfo endpoint after authentication or directly use the id_token for retrieving
// user information.
// Attention: this violates the OIDC specification, which states that these claims must only be
// requested from the userinfo endpoint if an access token is issued. This is to prevent
// leaking of personal information in the id token, which is often stored in the browser and
// therefore more vulnerable.
bool id_token_userinfo_assertion = 12;
// ClockSkew is used to compensate time differences between the servers of ZITADEL and the application.
// It is added to the "exp" claim and subtracted from "iat", "auth_time" and "nbf" claims.
// The default is 0s, the maximum is 5s.
google.protobuf.Duration clock_skew = 13 [
(validate.rules).duration = {
gte: {}
lte: {seconds: 5}
},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"1s\""}
];
// AdditionalOrigins are HTTP origins (scheme + host + port) from where the API can be used
// additional to the redirect_uris.
// This is useful if the application is used from an origin different to the redirect_uris,
// e.g. if the application is a SPA served in a native app, where the redirect_uri is a custom scheme,
// but the application is served from a https origin.
repeated string additional_origins = 14 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"scheme://localhost:8080\"]"}];
// For native apps a successful login usually shows a success page with a link to open the application again.
// SkipNativeAppSuccessPage can be used to skip this page and open the application directly.
bool skip_native_app_success_page = 15;
// BackChannelLogoutURI is used to notify the application about terminated sessions according
// to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html).
string back_channel_logout_uri = 16 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"https://example.com/auth/backchannel\"]"}];
// LoginVersion specifies the login UI, where the user is redirected to for authentication.
// It can be used to select a specific login UI, e.g. for embedded UIs or for custom login pages
// hosted on any other domain.
// If unset, the login UI is chosen by the instance default.
LoginVersion login_version = 17;
}
message CreateOIDCApplicationResponse {
// The unique OAuth2/OIDC client_id used for authentication of the application,
// e.g. at the token endpoint.
string client_id = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"1035496534033449\""}];
// In case of using the OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_CLIENT_SECRET_BASIC
// or OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_CLIENT_SECRET_POST the client_secret is generated and returned.
// It must be stored safely, as it will not be possible to retrieve it again.
// A new client_secret can be generated using the GenerateClientSecret endpoint.
string client_secret = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"gjoq34589uasgh\""}];
// NonCompliant specifies whether the config is OIDC compliant. A production configuration SHOULD be compliant.
// Non-compliant configurations can run into interoperability issues with OIDC libraries and tools.
// Compliance problems are listed in the compliance_problems field.
bool non_compliant = 3;
// ComplianceProblems lists the problems for non-compliant configurations.
// In case of a compliant configuration, this list is empty.
repeated OIDCLocalizedMessage compliance_problems = 4;
}
message CreateSAMLApplicationRequest {
// The SAML metadata can be provided either as raw XML or as a URL where the metadata can be fetched from.
// Either metadata_xml or metadata_url must be provided.
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: 2048
uri_ref: true
}];
}
// LoginVersion specifies the login UI, where the user is redirected to for authentication.
// It can be used to select a specific login UI, e.g. for embedded UIs or for custom login pages
// hosted on any other domain.
// If unset, the login UI is chosen by the instance default.
LoginVersion login_version = 3;
}
message CreateSAMLApplicationResponse {}
message CreateAPIApplicationRequest {
// The authentication method type used by the API to authenticate at the introspection endpoint.
APIAuthMethodType auth_method_type = 1 [(validate.rules).enum = {defined_only: true}];
}
message CreateAPIApplicationResponse {
// The unique OAuth2 client_id used for authentication of the API, e.g. at the introspection endpoint.
string client_id = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"3950723409029374\""}];
// In case of using the APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC the client_secret is generated and returned.
// It must be stored safely, as it will not be possible to retrieve it again.
// A new client_secret can be generated using the GenerateClientSecret endpoint.
string client_secret = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"gjoq34589uasgh\""}];
}
message UpdateApplicationRequest {
// The unique ID of the application to be updated.
string application_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\""
}
];
// The ID of the project the application belongs to.
string project_id = 2 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
// Publicly visible name of the application. This might be presented to users if they sign in.
// If not set, the name will not be changed.
string name = 3 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"MyApplicationName\""
max_length: 200
}
];
oneof application_type {
UpdateSAMLApplicationConfigurationRequest saml_configuration = 4;
UpdateOIDCApplicationConfigurationRequest oidc_configuration = 5;
UpdateAPIApplicationConfigurationRequest api_configuration = 6;
}
}
message UpdateApplicationResponse {
// The timestamp of the application update. If no changes were made, the previous change date is returned.
// This can be used to check if the application was actually updated.
google.protobuf.Timestamp change_date = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2024-12-18T07:50:47.492Z\""}];
}
message UpdateSAMLApplicationConfigurationRequest {
// The SAML metadata can be provided either as raw XML or as a URL where the metadata can be fetched from.
// Either metadata_xml or metadata_url must be provided.
// If omitted, the metadata will not be changed.
oneof metadata {
bytes metadata_xml = 1 [(validate.rules).bytes.max_len = 500000];
string metadata_url = 2 [(validate.rules).string.max_len = 200];
}
// LoginVersion specifies the login UI, where the user is redirected to for authentication.
// It can be used to select a specific login UI, e.g. for embedded UIs or for custom login pages
// hosted on any other domain.
// If unset, the login UI is chosen by the instance default.
optional LoginVersion login_version = 3;
}
message UpdateOIDCApplicationConfigurationRequest {
// RedirectURIs are the allowed callback URIs for the OAuth2 / OIDC flows,
// where the authorization code or tokens will be sent to.
// The redirect_uri parameter in the authorization request must exactly match one of these URIs.
// Any existing redirect URIs not included in this list will be removed.
// If not set, the redirect URIs will not be changed.
repeated string redirect_uris = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"http://localhost:4200/auth/callback\"]"}];
// ResponseTypes define whether a code, id_token token or just id_token will be returned.
// The response_type parameter in the authorization request must exactly match one of these values.
// Any existing response types not included in this list will be removed.
// If not set, the response types will not be changed.
repeated OIDCResponseType response_types = 2;
// GrantTypes define the flow type the application is allowed to use.
// The grant_type parameter in the token request must exactly match one of these values.
// Minimum one grant type must be provided, but multiple grant types can be provided to allow
// different flows, e.g. authorization code flow and refresh token flow.
// Any existing grant types not included in this list will be removed.
// If not set, the grant types will not be changed.
repeated OIDCGrantType grant_types = 3;
// ApplicationType defines the OAuth2/OIDC client type and their ability to maintain
// confidentiality of their credentials.
// This influences the allowed grant types and the required authentication method.
// If not set, the application type will not be changed.
optional OIDCApplicationType application_type = 4 [(validate.rules).enum = {defined_only: true}];
// The authentication method type used by the application to authenticate at the token endpoint.
// If not set, the authentication method type will not be changed.
optional OIDCAuthMethodType auth_method_type = 5 [(validate.rules).enum = {defined_only: true}];
// PostLogoutRedirectURIs are the allowed URIs to redirect to after a logout.
// The post_logout_redirect_uri parameter in the logout request must exactly match one of these URIs.
// Any existing post logout redirect URIs not included in this list will be removed.
// If not set, the post logout redirect URIs will not be changed.
repeated string post_logout_redirect_uris = 6 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"http://localhost:4200/signedout\"]"}];
// Version defines the OIDC version used by the application.
// Currently, only version 1.0 is supported.
// Future versions might introduce breaking changes.
// If not set, the version will not be changed.
optional OIDCVersion version = 7 [(validate.rules).enum = {defined_only: true}];
// DevelopmentMode can be enabled for development purposes. This allows the use of
// OIDC non-compliant and potentially insecure settings, such as the use of
// HTTP redirect URIs or wildcard redirect URIs.
// If not set, the dev mode will not be changed.
optional bool development_mode = 8;
// The AccessTokenType defines the type of the access token returned from ZITADEL.
// Bearer tokens are opaque to clients. JWT tokens are self-contained and can be validated by the client.
// Bearer tokens must be introspected at the ZITADEL token endpoint.
// If not set, the access token type will not be changed.
optional OIDCTokenType access_token_type = 9 [(validate.rules).enum = {defined_only: true}];
// If AccessTokenRoleAssertion is enabled, the roles of the user are added to the access token.
// Ensure that the access token is a JWT token and not a bearer token. And either request the roles
// by scope or enable the user role assertion on the project.
// If not set, the access token role assertion will not be changed.
optional bool access_token_role_assertion = 10;
// If IDTokenRoleAssertion is enabled, the roles of the user are added to the id token.
// Ensure that either the roles are requested by scope or enable the user role assertion on the
// project.
// If not set, the id token role assertion will not be changed.
optional bool id_token_role_assertion = 11;
// If IDTokenUserinfoAssertion is enabled, the claims of profile, email, address and phone scopes
// are added to the id token even if an access token is issued. This can be required by some applications
// that do not call the userinfo endpoint after authentication or directly use the id_token for retrieving
// user information.
// Attention: this violates the OIDC specification, which states that these claims must only be
// requested from the userinfo endpoint if an access token is issued. This is to prevent
// leaking of personal information in the id token, which is often stored in the browser and
// therefore more vulnerable.
// If not set, the id token userinfo assertion will not be changed.
optional bool id_token_userinfo_assertion = 12;
// ClockSkew is used to compensate time differences between the servers of ZITADEL and the application.
// It is added to the "exp" claim and subtracted from "iat", "auth_time" and "nbf" claims.
// The default is 0s, the maximum is 5s.
// If not set, the clock skew will not be changed.
optional google.protobuf.Duration clock_skew = 13 [
(validate.rules).duration = {
gte: {}
lte: {seconds: 5}
},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"1s\""}
];
// AdditionalOrigins are HTTP origins (scheme + host + port) from where the API can be used
// additional to the redirect_uris.
// This is useful if the application is used from an origin different to the redirect_uris,
// e.g. if the application is a SPA served in a native app, where the redirect_uri is a custom scheme,
// but the application is served from a https origin.
// Any existing additional origins not included in this list will be removed.
// If not set, the additional origins will not be changed.
repeated string additional_origins = 14 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"scheme://localhost:8080\"]"}];
// For native apps a successful login usually shows a success page with a link to open the application again.
// SkipNativeAppSuccessPage can be used to skip this page and open the application directly.
// If not set, the skip native app success page will not be changed.
optional bool skip_native_app_success_page = 15;
// BackChannelLogoutURI is used to notify the application about terminated sessions according
// to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html).
// If not set, the back channel logout URI will not be changed.
optional string back_channel_logout_uri = 16 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"https://example.com/auth/backchannel\"]"}];
// LoginVersion specifies the login UI, where the user is redirected to for authentication.
// It can be used to select a specific login UI, e.g. for embedded UIs or for custom login pages
// hosted on any other domain.
// If unset, the login UI is chosen by the instance default.
optional LoginVersion login_version = 17;
}
message UpdateAPIApplicationConfigurationRequest {
// The authentication method type used by the API to authenticate at the introspection endpoint.
APIAuthMethodType auth_method_type = 1 [(validate.rules).enum = {defined_only: true}];
}
message GetApplicationRequest {
// The unique ID of the application to be retrieved.
string application_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 application = 1;
}
message DeleteApplicationRequest {
// The unique ID of the application to be deleted.
string application_id = 1 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
// The ID of the project the application belongs to.
string project_id = 2 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
}
message DeleteApplicationResponse {
// The timestamp of the application deletion. In case the application was already deleted,
// the previous deletion date is returned. This can be used to check if the application was
// actually deleted.
google.protobuf.Timestamp deletion_date = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2025-01-23T10:34:18.051Z\""}];
}
message DeactivateApplicationRequest {
// The unique ID of the application to be deactivated.
string application_id = 1 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
// The ID of the project the application belongs to.
string project_id = 2 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
}
message DeactivateApplicationResponse {
// The timestamp of the application deactivation.
google.protobuf.Timestamp deactivation_date = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2025-01-23T10:34:18.051Z\""}];
}
message ReactivateApplicationRequest {
// The unique ID of the application to be reactivated.
string application_id = 1 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
// The ID of the project the application belongs to.
string project_id = 2 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
}
message ReactivateApplicationResponse {
// The timestamp of the application reactivation.
google.protobuf.Timestamp reactivation_date = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2025-01-23T10:34:18.051Z\""}];
}
message GenerateClientSecretRequest {
// The unique ID of the application to generate a new client secret for.
string application_id = 1 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
// The ID of the project the application belongs to.
string project_id = 2 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
}
message GenerateClientSecretResponse {
// The newly generated client secret. It must be stored safely, as it will not be possible to retrieve it again.
// A new client secret can be generated using this endpoint.
string client_secret = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"gjoq34589uasgh\""}];
// 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 {
// Pagination and sorting.
zitadel.filter.v2.PaginationRequest pagination = 1;
ApplicationSorting sorting_column = 3;
// Criteria to filter the applications.
// All provided filters are combined with a logical AND.
repeated ApplicationSearchFilter filters = 2;
}
message ListApplicationsResponse {
// The list of applications matching the query. Depending on the applied limit,
// there might be more applications available than included in this list.
// Use the returned pagination information to request further applications.
repeated Application applications = 1;
// Contains the total number of apps matching the query and the applied limit.
zitadel.filter.v2.PaginationResponse pagination = 2;
}
message CreateApplicationKeyRequest {
// The ID of the application the key will be created for.
string application_id = 1 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
// The ID of the project the application belongs to.
string project_id = 2 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
// The timestamp the key will expire.
google.protobuf.Timestamp expiration_date = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2519-04-01T08:45:00.000000Z\""}];
}
message CreateApplicationKeyResponse {
// The unique ID of the newly created application key.
string key_id = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"28746028909593987\""}];
// The timestamp of the application key creation.
google.protobuf.Timestamp creation_date = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2024-12-18T07:50:47.492Z\""}];
// KeyDetails contains the serialized private key and additional information depending on the key type.
// It must be stored safely, as it will not be possible to retrieve it again.
// A new key can be generated using the CreateApplicationKey endpoint.
bytes key_details = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"eyJ0eXBlIjoiYXBwbGljYXRpb24iLCJrZXlJZCI6IjIwMjcxMDE4NjYyMjcxNDExMyIsImtleSI6Ii0tLS0tQkVHSU4gUlNBIFBSSVZBVEUgS0VZLS0tLS1cbk1JSUVvd0lCQUFLQ0FRRUFuMUxyNStTV0pGRllURU1kaXQ2U0dNY0E2Yks5dG0xMmhlcm55V0wrZm9PWnA3eEVcbk9wcmsvWE81QVplSU5NY0x0ZVhxckJlK1NPdVVNMFpLU2xCMHFTNzNjVStDVTVMTGoycVB0UzhNOFI0N3BGdFhcbjJXRTFJNjNhZHB1N01TejA2SXduQ2lyNnJYOTVPQ2ZneHA3VU1Dd0pSTUZmYXJqdjVBRXY3NXpsSS9lYUV6bUJcbkxKWU1xanZFRmZoN2x3M2lPT3VsWW9kNjNpN3RDNWl5czNlYjNLZW4yWU0rN1FSbXB2dE5qcTJMVmlIMnkrUGJcbk9ESlI3MU9ib05TYVJDNTZDUFpWVytoWDByYXI3VzMwUjI2eGtIQ09oSytQbUpSeGtOY0g1VTdja0xXMEw0WEVcbnNNZkVUSmszeDR3Q0psbisxbElXUzkrNmw0R1E2TWRzWURyOU5RSURBUUFCQW9JQkFCSkx6WGQxMHFBZEQwekNcbnNGUFFOMnJNLzVmV3hONThONDR0YWF6QXg0VHp5K050UlZDTmxScGQvYkxuR2VjbHJIeVpDSmYycWcxcHNEMHJcbkowRGRlR2d0VXBFYWxsYk9scjNEZVBsUGkrYnNsK0RKOUk2c0VSUWwxTjZtQjVzZ0ZJZllBR3UwZjlFSXdIem9cblozR25yNnBRaEVmM0JPUVdsTVhVTlJNSksyOHp3M2E1L01nRmtKVUZUSTUzeXFwbGRtZ2hLajRZR1hLRk1LUGhcbkV3RkxrRncwK2s3K0xuSjFQNGp1ZVd1RXo3WlAyaFpvUWxCcXdSajVyTG9QZ05RbUU4UytFVDRuczlUYzByOFFcbnFyaHlacDZBczJrTDhGTytCZnF3SVpDZnpnWHN2cC9PLzRaSHIzVTB2Ymp3UW1sSzdVSm42U0J6T2hpWFpNU0lcbk5Wc0V5VUVDZ1lFQTFEaktkRGo3NTM1MWQzdlRNQlRFd2JSQ3hoUVZOdENFMnMwVUw4ckJQZ1I0K1dlblNUWmFcbnprWUprcEV0bE54VGxzYnN1Y0RTUXZqeWRYYk5nSHFBeDYzMm1vdTVkak9lR0VTUDFWVGtUdElsZFZQZWszQWxcbjVYbkpQa1dqWGVyVVJZNm5KeUQ5UWhlREx3MVp4NEFYVzNHWURiTFkrT05XV0VKUlJaQUloNjBDZ1lFQXdEQ2xcbnc1MHc4dkcvbEJ4RzNSYW9FaHdLOWNna1VXOHk2T25DekNwcEtjOEZUUmY1VE5iWjl5TzNXUmdYajhkeHRCakFcbkl5VGlzYk9NQk1VaFZKUUtGZHRQaDhoVDBwRkRjeE9ndzY0aHBtYzhyY2RTbXVKNzlYSVRTaHUySjA0N0UvNFZcbnJOTThpWVk5ZGR3VGdGUUlsdFNZL0l0RnFxWERmdjhqK1dVY25La0NnWUVBaENOUU80bDNuNjRucWR2WnBTaHBcblVrclJBTkJrWFJyOGZkZ1BaNnFSSS9KWStNSEhjVmg4dGM3NkN0NkdTUmZlbkJVRU5LeVF2czZPK1FDZCtBOU9cbnZBWGZkRjduZldlcVdtWG1RT2g0dDNNMWk1WkxFZlpVUWt2UU9BdllLcFFhMDZ4OCsyb1pCdHZvL0pVTmY2Q0xcbjZvNFNKUVZrLzZOZGtkckpDODBnNG9rQ2dZQkZsNWYrbkVYa1F0dWZVeG5wNXRGWE5XWldsM0ZuTjMvVXpRaW5cbmkxZm5OcnB4cnhPcjJrUzA4KzdwU1FzSEdpNDNDNXRQWG9UajJlTUN1eXNWaUVHYXBuNUc2YWhJb0NjdlhWVWlcblprUnpFQUR0NERZdU5ZS3pYdXBUTkhPaUNmYmtoMlhyM2RXVzZ0QUloSGRmU1k2T3AwNzZhNmYvWWVUSGNMWGpcbkVkVHBlUUtCZ0FPdnBqcDQ4TzRUWEZkU0JLSnYya005OHVhUjlSQURtdGxTWHd2cTlyQkhTV084NFk4bzE0L1Bcbkl1UmxUOHhROGRYKzhMR21UUCtjcUtiOFFRQ1grQk1YUWxMSEVtWnpnb0xFa0pGMUVIMm4vZEZ5bngxS3prdFNcbm9UZUdsRzZhbXhVOVh4eW9RVFlEVGJCbERwc2FZUlFBZ2FUQzM3UVZRUjhmK1ZoRzFHSFFcbi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tXG4iLCJhcHBJZCI6IjIwMjcwNjM5ODgxMzg4MDU3NyIsImNsaWVudElkIjoiMjAyNzA2Mzk4ODEzOTQ2MTEzQG15dGVzdHByb2plY3QifQ==\""}];
}
message DeleteApplicationKeyRequest {
// The unique ID of the application key to be deleted.
string key_id = 1 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
// The ID of the application the key belongs to.
string application_id = 2 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
// The ID of the project the application belongs to.
string project_id = 3 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
}
message DeleteApplicationKeyResponse {
// The timestamp of the application key deletion. In case the key was already deleted,
// the previous deletion date is returned. This can be used to check if the key was
// actually deleted.
google.protobuf.Timestamp deletion_date = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2025-01-23T10:34:18.051Z\""}];
}
message GetApplicationKeyRequest {
// The unique ID of the application key to be retrieved.
string key_id = 1 [
(validate.rules).string = {
min_len: 1
max_len: 200
},
(google.api.field_behavior) = REQUIRED
];
}
message GetApplicationKeyResponse {
// Unique ID of the application key.
string key_id = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629023906488334\""}];
// The timestamp of the key creation.
google.protobuf.Timestamp creation_date = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2025-01-23T10:34:18.051Z\""}];
// The timestamp when the key will expire.
google.protobuf.Timestamp expiration_date = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"3019-04-01T08:45:00.000000Z\""}];
}
message ListApplicationKeysRequest {
// Pagination and sorting.
zitadel.filter.v2.PaginationRequest pagination = 1;
// The column to sort by. If not provided, the default is 'KEY_ID'.
ApplicationKeysSorting sorting_column = 2;
// Criteria to filter the application keys.
// All provided filters are combined with a logical AND.
repeated ApplicationKeySearchFilter filters = 3;
}
message ListApplicationKeysResponse {
// The list of application keys matching the query. Depending on the applied limit,
// there might be more keys available than returned in this list.
// Use the returned pagination information to request further keys.
repeated ApplicationKey keys = 1;
// Contains the total number of application keys matching the query and the applied limit.
zitadel.filter.v2.PaginationResponse pagination = 2;
}

View File

@@ -0,0 +1,21 @@
syntax = "proto3";
package zitadel.application.v2;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/application/v2;application";
message LoginVersion {
oneof version {
// Allow the user to sign in through the ZITADEL hosted login UI.
LoginV1 login_v1 = 1;
// Allow the user to sign in though the new login UI or even your own UI by specifying a base_uri.
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;
}

View File

@@ -0,0 +1,160 @@
syntax = "proto3";
package zitadel.application.v2;
import "google/protobuf/duration.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/application/v2/login.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/application/v2;application";
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 OIDCApplicationType {
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 OIDCConfiguration {
// RedirectURIs are the allowed callback URIs for the OAuth2 / OIDC flows,
// where the authorization code or tokens will be sent to.
// The redirect_uri parameter in the authorization request must exactly match one of these URIs.
repeated string redirect_uris = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"https://console.zitadel.ch/auth/callback\"]"}];
// ResponseTypes define whether a code, id_token token or just id_token will be returned.
// The response_type parameter in the authorization request must exactly match one of these values.
repeated OIDCResponseType response_types = 2;
// GrantTypes define the flow type the application is allowed to use.
// The grant_type parameter in the token request must exactly match one of these values.
repeated OIDCGrantType grant_types = 3;
// ApplicationType defines the OAuth2/OIDC client type and their ability to maintain
// confidentiality of their credentials.
// This influences the allowed grant types and the required authentication method.
OIDCApplicationType application_type = 4 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "determines the paradigm of the application"}];
// The unique OAuth2/OIDC client_id used for authentication of the application,
// e.g. at the token endpoint.
string client_id = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629023906488334@ZITADEL\""}];
// The authentication method type used by the application to authenticate at the token endpoint.
OIDCAuthMethodType auth_method_type = 6;
// PostLogoutRedirectURIs are the allowed URIs to redirect to after a logout.
// The post_logout_redirect_uri parameter in the logout request must exactly match one of these URIs.
repeated string post_logout_redirect_uris = 7 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"https://console.zitadel.ch/logout\"]"}];
// Version defines the OIDC version used by the application.
// Currently, only version 1.0 is supported.
// Future versions might introduce breaking changes.
OIDCVersion version = 8;
// NonCompliant specifies whether the config is OIDC compliant. A production configuration SHOULD be compliant.
// Non-compliant configurations can run into interoperability issues with OIDC libraries and tools.
// Compliance problems are listed in the compliance_problems field.
bool non_compliant = 9;
// ComplianceProblems lists the problems for non-compliant configurations.
// In case of a compliant configuration, this list is empty.
repeated OIDCLocalizedMessage compliance_problems = 10;
// DevelopmentMode can be enabled for development purposes. This allows the use of
// OIDC non-compliant and potentially insecure settings, such as the use of
// HTTP redirect URIs or wildcard redirect URIs.
bool development_mode = 11;
// The AccessTokenType defines the type of the access token returned from ZITADEL.
// Bearer tokens are opaque to clients. JWT tokens are self-contained and can be validated by the client.
// Bearer tokens must be introspected at the ZITADEL token endpoint.
OIDCTokenType access_token_type = 12;
// If AccessTokenRoleAssertion is enabled, the roles of the user are added to the access token.
// Ensure that the access token is a JWT token and not a bearer token. And either request the roles
// by scope or enable the user role assertion on the project.
bool access_token_role_assertion = 13;
// If IDTokenRoleAssertion is enabled, the roles of the user are added to the id token.
// Ensure that either the roles are requested by scope or enable the user role assertion on the
// project.
bool id_token_role_assertion = 14;
// If IDTokenUserinfoAssertion is enabled, the claims of profile, email, address and phone scopes
// are added to the id token even if an access token is issued. This can be required by some applications
// that do not call the userinfo endpoint after authentication or directly use the id_token for retrieving
// user information.
// Attention: this violates the OIDC specification, which states that these claims must only be
// requested from the userinfo endpoint if an access token is issued. This is to prevent
// leaking of personal information in the id token, which is often stored in the browser and
// therefore more vulnerable.
bool id_token_userinfo_assertion = 15;
// ClockSkew is used to compensate time differences between the servers of ZITADEL and the application.
// It is added to the "exp" claim and subtracted from "iat", "auth_time" and "nbf" claims.
// The default is 0s, the maximum is 5s.
google.protobuf.Duration clock_skew = 16;
// AdditionalOrigins are HTTP origins (scheme + host + port) from where the API can be used
// additional to the redirect_uris.
// This is useful if the application is used from an origin different to the redirect_uris,
// e.g. if the application is a SPA served in a native app, where the redirect_uri is a custom scheme,
// but the application is served from a https origin.
repeated string additional_origins = 17 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"https://console.zitadel.ch/auth/callback\"]"}];
// AllowedOrigins are all HTTP origins where the application is allowed to be used from.
// This is used to prevent CORS issues in browsers.
// If the origin of the request is not in this list, the request will be rejected.
// This is especially important for SPAs.
// Note that this is a generated list from the redirect_uris and additional_origins.
// If you use the application from another origin, you have to add it to the additional_origins.
repeated string allowed_origins = 18 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"https://console.zitadel.ch\"]"}];
// For native apps a successful login usually shows a success page with a link to open the application again.
// SkipNativeAppSuccessPage can be used to skip this page and open the application directly.
bool skip_native_app_success_page = 19;
// BackChannelLogoutURI is used to notify the application about terminated sessions according
// to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html).
string back_channel_logout_uri = 20 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"https://example.com/auth/backchannel\"]"}];
// LoginVersion specifies the login UI, where the user is redirected to for authentication.
// It can be used to select a specific login UI, e.g. for embedded UIs or for custom login pages
// hosted on any other domain.
// If unset, the login UI is chosen by the instance default.
LoginVersion login_version = 21;
}

View File

@@ -0,0 +1,24 @@
syntax = "proto3";
package zitadel.application.v2;
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/application/v2/login.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/application/v2;application";
message SAMLConfiguration {
// The Metadata XML is the provided or fetched metadata stored at Zitadel.
// If either the metadata was provided as XML or when Zitadel fetched it at the provided URL,
// it is stored here.
bytes metadata_xml = 1;
// The Metadata URL is the URL where the metadata was fetched from.
// In case the metadata was provided as raw XML, this field is empty.
string metadata_url = 2;
// LoginVersion specifies the login UI, where the user is redirected to for authentication.
// It can be used to select a specific login UI, e.g. for embedded UIs or for custom login pages
// hosted on any other domain.
// If unset, the login UI is chosen by the instance default.
LoginVersion login_version = 3;
}

View File

@@ -3542,7 +3542,7 @@ service ManagementService {
// Get Application By ID
//
// Deprecated: Use [GetApplication](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-get-application.api.mdx) instead to fetch an app
// Deprecated: Use [GetApplication](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-get-application.api.mdx) instead to fetch an app
//
// Get an application of any type (OIDC, API, SAML).
rpc GetAppByID(GetAppByIDRequest) returns (GetAppByIDResponse) {
@@ -3571,7 +3571,7 @@ service ManagementService {
// Search Applications
//
// Deprecated: Use [ListApplications](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-list-applications.api.mdx) instead to list applications
// Deprecated: Use [ListApplications](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-list-applications.api.mdx) instead to list applications
//
// Returns all applications within a project, that match the query.
rpc ListApps(ListAppsRequest) returns (ListAppsResponse) {
@@ -3626,7 +3626,7 @@ service ManagementService {
// Create Application (OIDC)
//
// Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-create-application.api.mdx) instead to create an OIDC application.
// Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-create-application.api.mdx) instead to create an OIDC application.
//
// Create a new OIDC client. The client id will be generated and returned in the response. Depending on the chosen configuration also a secret will be returned.
rpc AddOIDCApp(AddOIDCAppRequest) returns (AddOIDCAppResponse) {
@@ -3656,7 +3656,7 @@ service ManagementService {
// Create Application (SAML)
//
// Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-create-application.api.mdx) instead to create a SAML application.
// Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-create-application.api.mdx) instead to create a SAML application.
//
// Create a new SAML client. Returns an entity ID.
rpc AddSAMLApp(AddSAMLAppRequest) returns (AddSAMLAppResponse) {
@@ -3686,7 +3686,7 @@ service ManagementService {
// Create Application (API)
//
// Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-create-application.api.mdx) instead to create an API application
// Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-create-application.api.mdx) instead to create an API application
//
// 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.
@@ -3717,7 +3717,7 @@ service ManagementService {
// Update Application
//
// Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-update-application.api.mdx) instead to update the generic params of an app.
// Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-update-application.api.mdx) instead to update the generic params of an app.
//
// Update the basic information of an application. This doesn't include information that are dependent on the application type (OIDC, API, SAML)
rpc UpdateApp(UpdateAppRequest) returns (UpdateAppResponse) {
@@ -3747,7 +3747,7 @@ service ManagementService {
// Update OIDC Application Config
//
// Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-update-application.api.mdx) instead to update the config of an OIDC app.
// Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-update-application.api.mdx) instead to update the config of an OIDC app.
//
// Update the OIDC specific configuration of an application.
rpc UpdateOIDCAppConfig(UpdateOIDCAppConfigRequest) returns (UpdateOIDCAppConfigResponse) {
@@ -3777,7 +3777,7 @@ service ManagementService {
// Update SAML Application Config
//
// Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-update-application.api.mdx) instead to update the config of a SAML app.
// Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-update-application.api.mdx) instead to update the config of a SAML app.
//
// Update the SAML specific configuration of an application.
rpc UpdateSAMLAppConfig(UpdateSAMLAppConfigRequest) returns (UpdateSAMLAppConfigResponse) {
@@ -3807,7 +3807,7 @@ service ManagementService {
// Update API Application Config
//
// Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-update-application.api.mdx) instead to update the config of an API app.
// Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-update-application.api.mdx) instead to update the config of an API app.
//
// Update the OIDC-specific configuration of an application.
rpc UpdateAPIAppConfig(UpdateAPIAppConfigRequest) returns (UpdateAPIAppConfigResponse) {
@@ -3837,7 +3837,7 @@ service ManagementService {
// Deactivate Application
//
// Deprecated: Use [DeactivateApplication](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-deactivate-application.api.mdx) instead to deactivate an app.
// Deprecated: Use [DeactivateApplication](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-deactivate-application.api.mdx) instead to deactivate an app.
//
// Set the state of an application to deactivated. It is not possible to request tokens for deactivated apps. Request returns an error if the application is already deactivated.
rpc DeactivateApp(DeactivateAppRequest) returns (DeactivateAppResponse) {
@@ -3867,7 +3867,7 @@ service ManagementService {
// Reactivate Application
//
// Deprecated: Use [ReactivateApplication](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-reactivate-application.api.mdx) instead to reactivate an app.
// Deprecated: Use [ReactivateApplication](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-reactivate-application.api.mdx) instead to reactivate an app.
//
// Set the state of an application to active. Request returns an error if the application is not deactivated.
rpc ReactivateApp(ReactivateAppRequest) returns (ReactivateAppResponse) {
@@ -3897,7 +3897,7 @@ service ManagementService {
// Remove Application
//
// Deprecated: Use [DeleteApplication](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-delete-application.api.mdx) instead to delete an app.
// Deprecated: Use [DeleteApplication](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-delete-application.api.mdx) instead to delete an app.
//
// Remove an application. It is not possible to request tokens for removed apps. Request returns an error if the application is already deactivated.
rpc RemoveApp(RemoveAppRequest) returns (RemoveAppResponse) {
@@ -3926,7 +3926,7 @@ service ManagementService {
// Generate New OIDC Client Secret
//
// Deprecated: Use [RegenerateClientSecret](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-regenerate-client-secret.api.mdx) instead to regenerate an OIDC app client secret.
// Deprecated: Use [GenerateClientSecret](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-generate-client-secret.api.mdx) instead to (re-)generate an OIDC app client secret.
//
// Generates a new client secret for the OIDC application, make sure to save the response.
rpc RegenerateOIDCClientSecret(RegenerateOIDCClientSecretRequest) returns (RegenerateOIDCClientSecretResponse) {
@@ -3956,7 +3956,7 @@ service ManagementService {
// Generate New API Client Secret
//
// Deprecated: Use [RegenerateClientSecret](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-regenerate-client-secret.api.mdx) instead to regenerate an API app client secret
// Deprecated: Use [GenerateClientSecret](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-generate-client-secret.api.mdx) instead to (re-)generate an API app client secret
//
// Generates a new client secret for the API application, make sure to save the response.
rpc RegenerateAPIClientSecret(RegenerateAPIClientSecretRequest) returns (RegenerateAPIClientSecretResponse) {
@@ -3986,7 +3986,7 @@ service ManagementService {
// Get Application Key By ID
//
// Deprecated: Use [GetApplicationKey](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-get-application-key.api.mdx) instead to get an application key.
// Deprecated: Use [GetApplicationKey](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-get-application-key.api.mdx) instead to get an application key.
//
// Returns an application key. Keys are used for authorizing API Applications.
rpc GetAppKey(GetAppKeyRequest) returns (GetAppKeyResponse) {
@@ -4015,7 +4015,7 @@ service ManagementService {
// List Application Keys
//
// Deprecated: Use [ListApplicationKeys](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-list-application-keys.api.mdx) instead to list application keys.
// Deprecated: Use [ListApplicationKeys](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-list-application-keys.api.mdx) instead to list application keys.
//
// Search application keys. Keys are used for authorizing API Applications.
rpc ListAppKeys(ListAppKeysRequest) returns (ListAppKeysResponse) {
@@ -4045,7 +4045,7 @@ service ManagementService {
// Create Application Key
//
// Deprecated: Use [CreateApplicationKey](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-create-application-key.api.mdx) instead to create an application key.
// Deprecated: Use [CreateApplicationKey](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-create-application-key.api.mdx) instead to create an application key.
//
// Create a new application key, they are used for authorizing API Applications. Key details will be returned in the response, make sure to save it.
rpc AddAppKey(AddAppKeyRequest) returns (AddAppKeyResponse){
@@ -4075,7 +4075,7 @@ service ManagementService {
// Delete Application Key
//
// Deprecated: Use [DeleteApplicationKey](/apis/resources/application_service_v2/zitadel-app-v-2-beta-app-service-delete-application-key.api.mdx) instead to delete an application key.
// Deprecated: Use [DeleteApplicationKey](/apis/resources/application_service_v2/zitadel-application-v-2-application-service-delete-application-key.api.mdx) instead to delete an application key.
//
// Remove an application key. The API application will not be able to authorize with the key anymore.
rpc RemoveAppKey(RemoveAppKeyRequest) returns (RemoveAppKeyResponse) {