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
@@ -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
}
@@ -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
}
@@ -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
}
}
@@ -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)
})
}
}
@@ -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
}
@@ -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)
})
}
}
@@ -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
}
}
@@ -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)
})
}
}
@@ -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),
},
}
}
@@ -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)
})
}
}
@@ -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
@@ -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)
})
}
}
@@ -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
}
+72
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
}
@@ -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
}