mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 18:07:31 +00:00
feat: App API v2 (#10077)
# Which Problems Are Solved This PR *partially* addresses #9450 . Specifically, it implements the resource based API for the apps. APIs for app keys ARE not part of this PR. # How the Problems Are Solved - `CreateApplication`, `PatchApplication` (update) and `RegenerateClientSecret` endpoints are now unique for all app types: API, SAML and OIDC apps. - All new endpoints have integration tests - All new endpoints are using permission checks V2 # Additional Changes - The `ListApplications` endpoint allows to do sorting (see protobuf for details) and filtering by app type (see protobuf). - SAML and OIDC update endpoint can now receive requests for partial updates # Additional Context Partially addresses #9450
This commit is contained in:
@@ -783,7 +783,7 @@ func (s *Server) getProjectsAndApps(ctx context.Context, org string) ([]*v1_pb.D
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
apps, err := s.query.SearchApps(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{appSearch}}, false)
|
||||
apps, err := s.query.SearchApps(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{appSearch}}, nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
208
internal/api/grpc/app/v2beta/app.go
Normal file
208
internal/api/grpc/app/v2beta/app.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/app/v2beta/convert"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) CreateApplication(ctx context.Context, req *app.CreateApplicationRequest) (*app.CreateApplicationResponse, error) {
|
||||
switch t := req.GetCreationRequestType().(type) {
|
||||
case *app.CreateApplicationRequest_ApiRequest:
|
||||
apiApp, err := s.command.AddAPIApplication(ctx, convert.CreateAPIApplicationRequestToDomain(req.GetName(), req.GetProjectId(), req.GetId(), t.ApiRequest), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &app.CreateApplicationResponse{
|
||||
AppId: apiApp.AppID,
|
||||
CreationDate: timestamppb.New(apiApp.ChangeDate),
|
||||
CreationResponseType: &app.CreateApplicationResponse_ApiResponse{
|
||||
ApiResponse: &app.CreateAPIApplicationResponse{
|
||||
ClientId: apiApp.ClientID,
|
||||
ClientSecret: apiApp.ClientSecretString,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
case *app.CreateApplicationRequest_OidcRequest:
|
||||
oidcAppRequest, err := convert.CreateOIDCAppRequestToDomain(req.GetName(), req.GetProjectId(), req.GetOidcRequest())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oidcApp, err := s.command.AddOIDCApplication(ctx, oidcAppRequest, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &app.CreateApplicationResponse{
|
||||
AppId: oidcApp.AppID,
|
||||
CreationDate: timestamppb.New(oidcApp.ChangeDate),
|
||||
CreationResponseType: &app.CreateApplicationResponse_OidcResponse{
|
||||
OidcResponse: &app.CreateOIDCApplicationResponse{
|
||||
ClientId: oidcApp.ClientID,
|
||||
ClientSecret: oidcApp.ClientSecretString,
|
||||
NoneCompliant: oidcApp.Compliance.NoneCompliant,
|
||||
ComplianceProblems: convert.ComplianceProblemsToLocalizedMessages(oidcApp.Compliance.Problems),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
case *app.CreateApplicationRequest_SamlRequest:
|
||||
samlAppRequest, err := convert.CreateSAMLAppRequestToDomain(req.GetName(), req.GetProjectId(), req.GetSamlRequest())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
samlApp, err := s.command.AddSAMLApplication(ctx, samlAppRequest, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &app.CreateApplicationResponse{
|
||||
AppId: samlApp.AppID,
|
||||
CreationDate: timestamppb.New(samlApp.ChangeDate),
|
||||
CreationResponseType: &app.CreateApplicationResponse_SamlResponse{
|
||||
SamlResponse: &app.CreateSAMLApplicationResponse{},
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "APP-0iiN46", "unknown app type")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) UpdateApplication(ctx context.Context, req *app.UpdateApplicationRequest) (*app.UpdateApplicationResponse, error) {
|
||||
var changedTime time.Time
|
||||
|
||||
if name := strings.TrimSpace(req.GetName()); name != "" {
|
||||
updatedDetails, err := s.command.UpdateApplicationName(
|
||||
ctx,
|
||||
req.GetProjectId(),
|
||||
&domain.ChangeApp{
|
||||
AppID: req.GetId(),
|
||||
AppName: name,
|
||||
},
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changedTime = updatedDetails.EventDate
|
||||
}
|
||||
|
||||
switch t := req.GetUpdateRequestType().(type) {
|
||||
case *app.UpdateApplicationRequest_ApiConfigurationRequest:
|
||||
updatedAPIApp, err := s.command.UpdateAPIApplication(ctx, convert.UpdateAPIApplicationConfigurationRequestToDomain(req.GetId(), req.GetProjectId(), t.ApiConfigurationRequest), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changedTime = updatedAPIApp.ChangeDate
|
||||
|
||||
case *app.UpdateApplicationRequest_OidcConfigurationRequest:
|
||||
oidcApp, err := convert.UpdateOIDCAppConfigRequestToDomain(req.GetId(), req.GetProjectId(), t.OidcConfigurationRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedOIDCApp, err := s.command.UpdateOIDCApplication(ctx, oidcApp, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changedTime = updatedOIDCApp.ChangeDate
|
||||
|
||||
case *app.UpdateApplicationRequest_SamlConfigurationRequest:
|
||||
samlApp, err := convert.UpdateSAMLAppConfigRequestToDomain(req.GetId(), req.GetProjectId(), t.SamlConfigurationRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedSAMLApp, err := s.command.UpdateSAMLApplication(ctx, samlApp, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changedTime = updatedSAMLApp.ChangeDate
|
||||
}
|
||||
|
||||
return &app.UpdateApplicationResponse{
|
||||
ChangeDate: timestamppb.New(changedTime),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteApplication(ctx context.Context, req *app.DeleteApplicationRequest) (*app.DeleteApplicationResponse, error) {
|
||||
details, err := s.command.RemoveApplication(ctx, req.GetProjectId(), req.GetId(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &app.DeleteApplicationResponse{
|
||||
DeletionDate: timestamppb.New(details.EventDate),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeactivateApplication(ctx context.Context, req *app.DeactivateApplicationRequest) (*app.DeactivateApplicationResponse, error) {
|
||||
details, err := s.command.DeactivateApplication(ctx, req.GetProjectId(), req.GetId(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &app.DeactivateApplicationResponse{
|
||||
DeactivationDate: timestamppb.New(details.EventDate),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) ReactivateApplication(ctx context.Context, req *app.ReactivateApplicationRequest) (*app.ReactivateApplicationResponse, error) {
|
||||
details, err := s.command.ReactivateApplication(ctx, req.GetProjectId(), req.GetId(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &app.ReactivateApplicationResponse{
|
||||
ReactivationDate: timestamppb.New(details.EventDate),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) RegenerateClientSecret(ctx context.Context, req *app.RegenerateClientSecretRequest) (*app.RegenerateClientSecretResponse, error) {
|
||||
var secret string
|
||||
var changeDate time.Time
|
||||
|
||||
switch req.GetAppType().(type) {
|
||||
case *app.RegenerateClientSecretRequest_IsApi:
|
||||
config, err := s.command.ChangeAPIApplicationSecret(ctx, req.GetProjectId(), req.GetApplicationId(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secret = config.ClientSecretString
|
||||
changeDate = config.ChangeDate
|
||||
|
||||
case *app.RegenerateClientSecretRequest_IsOidc:
|
||||
config, err := s.command.ChangeOIDCApplicationSecret(ctx, req.GetProjectId(), req.GetApplicationId(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secret = config.ClientSecretString
|
||||
changeDate = config.ChangeDate
|
||||
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "APP-aLWIzw", "unknown app type")
|
||||
}
|
||||
|
||||
return &app.RegenerateClientSecretResponse{
|
||||
ClientSecret: secret,
|
||||
CreationDate: timestamppb.New(changeDate),
|
||||
}, nil
|
||||
}
|
60
internal/api/grpc/app/v2beta/convert/api_app.go
Normal file
60
internal/api/grpc/app/v2beta/convert/api_app.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
)
|
||||
|
||||
func CreateAPIApplicationRequestToDomain(name, projectID, appID string, app *app.CreateAPIApplicationRequest) *domain.APIApp {
|
||||
return &domain.APIApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: projectID,
|
||||
},
|
||||
AppName: name,
|
||||
AppID: appID,
|
||||
AuthMethodType: apiAuthMethodTypeToDomain(app.GetAuthMethodType()),
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateAPIApplicationConfigurationRequestToDomain(appID, projectID string, app *app.UpdateAPIApplicationConfigurationRequest) *domain.APIApp {
|
||||
return &domain.APIApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: projectID,
|
||||
},
|
||||
AppID: appID,
|
||||
AuthMethodType: apiAuthMethodTypeToDomain(app.GetAuthMethodType()),
|
||||
}
|
||||
}
|
||||
|
||||
func appAPIConfigToPb(apiApp *query.APIApp) app.ApplicationConfig {
|
||||
return &app.Application_ApiConfig{
|
||||
ApiConfig: &app.APIConfig{
|
||||
ClientId: apiApp.ClientID,
|
||||
AuthMethodType: apiAuthMethodTypeToPb(apiApp.AuthMethodType),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func apiAuthMethodTypeToDomain(authType app.APIAuthMethodType) domain.APIAuthMethodType {
|
||||
switch authType {
|
||||
case app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC:
|
||||
return domain.APIAuthMethodTypeBasic
|
||||
case app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT:
|
||||
return domain.APIAuthMethodTypePrivateKeyJWT
|
||||
default:
|
||||
return domain.APIAuthMethodTypeBasic
|
||||
}
|
||||
}
|
||||
|
||||
func apiAuthMethodTypeToPb(methodType domain.APIAuthMethodType) app.APIAuthMethodType {
|
||||
switch methodType {
|
||||
case domain.APIAuthMethodTypeBasic:
|
||||
return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC
|
||||
case domain.APIAuthMethodTypePrivateKeyJWT:
|
||||
return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT
|
||||
default:
|
||||
return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC
|
||||
}
|
||||
}
|
149
internal/api/grpc/app/v2beta/convert/api_app_test.go
Normal file
149
internal/api/grpc/app/v2beta/convert/api_app_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
)
|
||||
|
||||
func TestCreateAPIApplicationRequestToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
appName string
|
||||
projectID string
|
||||
appID string
|
||||
req *app.CreateAPIApplicationRequest
|
||||
want *domain.APIApp
|
||||
}{
|
||||
{
|
||||
name: "basic auth method",
|
||||
appName: "my-app",
|
||||
projectID: "proj-1",
|
||||
appID: "someID",
|
||||
req: &app.CreateAPIApplicationRequest{
|
||||
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
|
||||
},
|
||||
want: &domain.APIApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
|
||||
AppName: "my-app",
|
||||
AuthMethodType: domain.APIAuthMethodTypeBasic,
|
||||
AppID: "someID",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "private key jwt",
|
||||
appName: "jwt-app",
|
||||
projectID: "proj-2",
|
||||
req: &app.CreateAPIApplicationRequest{
|
||||
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
|
||||
},
|
||||
want: &domain.APIApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"},
|
||||
AppName: "jwt-app",
|
||||
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
got := CreateAPIApplicationRequestToDomain(tt.appName, tt.projectID, tt.appID, tt.req)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAPIApplicationConfigurationRequestToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
appID string
|
||||
projectID string
|
||||
req *app.UpdateAPIApplicationConfigurationRequest
|
||||
want *domain.APIApp
|
||||
}{
|
||||
{
|
||||
name: "basic auth method",
|
||||
appID: "app-1",
|
||||
projectID: "proj-1",
|
||||
req: &app.UpdateAPIApplicationConfigurationRequest{
|
||||
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
|
||||
},
|
||||
want: &domain.APIApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
|
||||
AppID: "app-1",
|
||||
AuthMethodType: domain.APIAuthMethodTypeBasic,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "private key jwt",
|
||||
appID: "app-2",
|
||||
projectID: "proj-2",
|
||||
req: &app.UpdateAPIApplicationConfigurationRequest{
|
||||
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
|
||||
},
|
||||
want: &domain.APIApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"},
|
||||
AppID: "app-2",
|
||||
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
got := UpdateAPIApplicationConfigurationRequestToDomain(tt.appID, tt.projectID, tt.req)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_apiAuthMethodTypeToPb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
methodType domain.APIAuthMethodType
|
||||
expectedResult app.APIAuthMethodType
|
||||
}{
|
||||
{
|
||||
name: "basic auth method",
|
||||
methodType: domain.APIAuthMethodTypeBasic,
|
||||
expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
|
||||
},
|
||||
{
|
||||
name: "private key jwt",
|
||||
methodType: domain.APIAuthMethodTypePrivateKeyJWT,
|
||||
expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
|
||||
},
|
||||
{
|
||||
name: "unknown auth method defaults to basic",
|
||||
expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
res := apiAuthMethodTypeToPb(tc.methodType)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedResult, res)
|
||||
})
|
||||
}
|
||||
}
|
165
internal/api/grpc/app/v2beta/convert/convert.go
Normal file
165
internal/api/grpc/app/v2beta/convert/convert.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
)
|
||||
|
||||
func AppToPb(query_app *query.App) *app.Application {
|
||||
if query_app == nil {
|
||||
return &app.Application{}
|
||||
}
|
||||
|
||||
return &app.Application{
|
||||
Id: query_app.ID,
|
||||
CreationDate: timestamppb.New(query_app.CreationDate),
|
||||
ChangeDate: timestamppb.New(query_app.ChangeDate),
|
||||
State: appStateToPb(query_app.State),
|
||||
Name: query_app.Name,
|
||||
Config: appConfigToPb(query_app),
|
||||
}
|
||||
}
|
||||
|
||||
func AppsToPb(queryApps []*query.App) []*app.Application {
|
||||
pbApps := make([]*app.Application, len(queryApps))
|
||||
|
||||
for i, queryApp := range queryApps {
|
||||
pbApps[i] = AppToPb(queryApp)
|
||||
}
|
||||
|
||||
return pbApps
|
||||
}
|
||||
|
||||
func ListApplicationsRequestToModel(sysDefaults systemdefaults.SystemDefaults, req *app.ListApplicationsRequest) (*query.AppSearchQueries, error) {
|
||||
offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queries, err := appQueriesToModel(req.GetFilters())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projectQuery, err := query.NewAppProjectIDSearchQuery(req.GetProjectId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queries = append(queries, projectQuery)
|
||||
return &query.AppSearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Asc: asc,
|
||||
SortingColumn: appSortingToColumn(req.GetSortingColumn()),
|
||||
},
|
||||
|
||||
Queries: queries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appSortingToColumn(sortingCriteria app.AppSorting) query.Column {
|
||||
switch sortingCriteria {
|
||||
case app.AppSorting_APP_SORT_BY_CHANGE_DATE:
|
||||
return query.AppColumnChangeDate
|
||||
case app.AppSorting_APP_SORT_BY_CREATION_DATE:
|
||||
return query.AppColumnCreationDate
|
||||
case app.AppSorting_APP_SORT_BY_NAME:
|
||||
return query.AppColumnName
|
||||
case app.AppSorting_APP_SORT_BY_STATE:
|
||||
return query.AppColumnState
|
||||
case app.AppSorting_APP_SORT_BY_ID:
|
||||
fallthrough
|
||||
default:
|
||||
return query.AppColumnID
|
||||
}
|
||||
}
|
||||
|
||||
func appStateToPb(state domain.AppState) app.AppState {
|
||||
switch state {
|
||||
case domain.AppStateActive:
|
||||
return app.AppState_APP_STATE_ACTIVE
|
||||
case domain.AppStateInactive:
|
||||
return app.AppState_APP_STATE_INACTIVE
|
||||
case domain.AppStateRemoved:
|
||||
return app.AppState_APP_STATE_REMOVED
|
||||
case domain.AppStateUnspecified:
|
||||
fallthrough
|
||||
default:
|
||||
return app.AppState_APP_STATE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func appConfigToPb(app *query.App) app.ApplicationConfig {
|
||||
if app.OIDCConfig != nil {
|
||||
return appOIDCConfigToPb(app.OIDCConfig)
|
||||
}
|
||||
if app.SAMLConfig != nil {
|
||||
return appSAMLConfigToPb(app.SAMLConfig)
|
||||
}
|
||||
return appAPIConfigToPb(app.APIConfig)
|
||||
}
|
||||
|
||||
func loginVersionToDomain(version *app.LoginVersion) (*domain.LoginVersion, *string, error) {
|
||||
switch v := version.GetVersion().(type) {
|
||||
case nil:
|
||||
return gu.Ptr(domain.LoginVersionUnspecified), gu.Ptr(""), nil
|
||||
case *app.LoginVersion_LoginV1:
|
||||
return gu.Ptr(domain.LoginVersion1), gu.Ptr(""), nil
|
||||
case *app.LoginVersion_LoginV2:
|
||||
_, err := url.Parse(v.LoginV2.GetBaseUri())
|
||||
return gu.Ptr(domain.LoginVersion2), gu.Ptr(v.LoginV2.GetBaseUri()), err
|
||||
default:
|
||||
return gu.Ptr(domain.LoginVersionUnspecified), gu.Ptr(""), nil
|
||||
}
|
||||
}
|
||||
|
||||
func loginVersionToPb(version domain.LoginVersion, baseURI *string) *app.LoginVersion {
|
||||
switch version {
|
||||
case domain.LoginVersionUnspecified:
|
||||
return nil
|
||||
case domain.LoginVersion1:
|
||||
return &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}
|
||||
case domain.LoginVersion2:
|
||||
return &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: baseURI}}}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func appQueriesToModel(queries []*app.ApplicationSearchFilter) (toReturn []query.SearchQuery, err error) {
|
||||
toReturn = make([]query.SearchQuery, len(queries))
|
||||
for i, query := range queries {
|
||||
toReturn[i], err = appQueryToModel(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return toReturn, nil
|
||||
}
|
||||
|
||||
func appQueryToModel(appQuery *app.ApplicationSearchFilter) (query.SearchQuery, error) {
|
||||
switch q := appQuery.GetFilter().(type) {
|
||||
case *app.ApplicationSearchFilter_NameFilter:
|
||||
return query.NewAppNameSearchQuery(filter.TextMethodPbToQuery(q.NameFilter.GetMethod()), q.NameFilter.Name)
|
||||
case *app.ApplicationSearchFilter_StateFilter:
|
||||
return query.NewAppStateSearchQuery(domain.AppState(q.StateFilter))
|
||||
case *app.ApplicationSearchFilter_ApiAppOnly:
|
||||
return query.NewNotNullQuery(query.AppAPIConfigColumnAppID)
|
||||
case *app.ApplicationSearchFilter_OidcAppOnly:
|
||||
return query.NewNotNullQuery(query.AppOIDCConfigColumnAppID)
|
||||
case *app.ApplicationSearchFilter_SamlAppOnly:
|
||||
return query.NewNotNullQuery(query.AppSAMLConfigColumnAppID)
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid")
|
||||
}
|
||||
}
|
520
internal/api/grpc/app/v2beta/convert/convert_test.go
Normal file
520
internal/api/grpc/app/v2beta/convert/convert_test.go
Normal file
@@ -0,0 +1,520 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
filter_pb_v2 "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
|
||||
filter_pb_v2_beta "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
|
||||
)
|
||||
|
||||
func TestAppToPb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
inputQueryApp *query.App
|
||||
expectedPbApp *app.Application
|
||||
}{
|
||||
{
|
||||
testName: "full app conversion",
|
||||
inputQueryApp: &query.App{
|
||||
ID: "id",
|
||||
CreationDate: now,
|
||||
ChangeDate: now,
|
||||
State: domain.AppStateActive,
|
||||
Name: "test-app",
|
||||
APIConfig: &query.APIApp{},
|
||||
},
|
||||
expectedPbApp: &app.Application{
|
||||
Id: "id",
|
||||
CreationDate: timestamppb.New(now),
|
||||
ChangeDate: timestamppb.New(now),
|
||||
State: app.AppState_APP_STATE_ACTIVE,
|
||||
Name: "test-app",
|
||||
Config: &app.Application_ApiConfig{
|
||||
ApiConfig: &app.APIConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "nil app",
|
||||
inputQueryApp: nil,
|
||||
expectedPbApp: &app.Application{},
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
res := AppToPb(tc.inputQueryApp)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedPbApp, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListApplicationsRequestToModel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validSearchByNameQuery, err := query.NewAppNameSearchQuery(filter.TextMethodPbToQuery(filter_pb_v2_beta.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS), "test")
|
||||
require.NoError(t, err)
|
||||
|
||||
validSearchByProjectQuery, err := query.NewAppProjectIDSearchQuery("project1")
|
||||
require.NoError(t, err)
|
||||
|
||||
sysDefaults := systemdefaults.SystemDefaults{DefaultQueryLimit: 100, MaxQueryLimit: 150}
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
req *app.ListApplicationsRequest
|
||||
|
||||
expectedResponse *query.AppSearchQueries
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
testName: "invalid pagination limit",
|
||||
req: &app.ListApplicationsRequest{
|
||||
Pagination: &filter_pb_v2.PaginationRequest{Asc: true, Limit: uint32(sysDefaults.MaxQueryLimit + 1)},
|
||||
},
|
||||
expectedResponse: nil,
|
||||
expectedError: zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", sysDefaults.MaxQueryLimit+1, sysDefaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded"),
|
||||
},
|
||||
{
|
||||
testName: "empty request",
|
||||
req: &app.ListApplicationsRequest{
|
||||
ProjectId: "project1",
|
||||
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
|
||||
},
|
||||
expectedResponse: &query.AppSearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Asc: true,
|
||||
SortingColumn: query.AppColumnID,
|
||||
},
|
||||
Queries: []query.SearchQuery{
|
||||
validSearchByProjectQuery,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "valid request",
|
||||
req: &app.ListApplicationsRequest{
|
||||
ProjectId: "project1",
|
||||
Filters: []*app.ApplicationSearchFilter{
|
||||
{
|
||||
Filter: &app.ApplicationSearchFilter_NameFilter{NameFilter: &app.ApplicationNameQuery{Name: "test"}},
|
||||
},
|
||||
},
|
||||
SortingColumn: app.AppSorting_APP_SORT_BY_NAME,
|
||||
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
|
||||
},
|
||||
|
||||
expectedResponse: &query.AppSearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Asc: true,
|
||||
SortingColumn: query.AppColumnName,
|
||||
},
|
||||
Queries: []query.SearchQuery{
|
||||
validSearchByNameQuery,
|
||||
validSearchByProjectQuery,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
got, err := ListApplicationsRequestToModel(sysDefaults, tc.req)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
assert.Equal(t, tc.expectedResponse, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppSortingToColumn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
sorting app.AppSorting
|
||||
expected query.Column
|
||||
}{
|
||||
{
|
||||
name: "sort by change date",
|
||||
sorting: app.AppSorting_APP_SORT_BY_CHANGE_DATE,
|
||||
expected: query.AppColumnChangeDate,
|
||||
},
|
||||
{
|
||||
name: "sort by creation date",
|
||||
sorting: app.AppSorting_APP_SORT_BY_CREATION_DATE,
|
||||
expected: query.AppColumnCreationDate,
|
||||
},
|
||||
{
|
||||
name: "sort by name",
|
||||
sorting: app.AppSorting_APP_SORT_BY_NAME,
|
||||
expected: query.AppColumnName,
|
||||
},
|
||||
{
|
||||
name: "sort by state",
|
||||
sorting: app.AppSorting_APP_SORT_BY_STATE,
|
||||
expected: query.AppColumnState,
|
||||
},
|
||||
{
|
||||
name: "sort by ID",
|
||||
sorting: app.AppSorting_APP_SORT_BY_ID,
|
||||
expected: query.AppColumnID,
|
||||
},
|
||||
{
|
||||
name: "unknown sorting defaults to ID",
|
||||
sorting: app.AppSorting(99),
|
||||
expected: query.AppColumnID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := appSortingToColumn(tc.sorting)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppStateToPb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
state domain.AppState
|
||||
expected app.AppState
|
||||
}{
|
||||
{
|
||||
name: "active state",
|
||||
state: domain.AppStateActive,
|
||||
expected: app.AppState_APP_STATE_ACTIVE,
|
||||
},
|
||||
{
|
||||
name: "inactive state",
|
||||
state: domain.AppStateInactive,
|
||||
expected: app.AppState_APP_STATE_INACTIVE,
|
||||
},
|
||||
{
|
||||
name: "removed state",
|
||||
state: domain.AppStateRemoved,
|
||||
expected: app.AppState_APP_STATE_REMOVED,
|
||||
},
|
||||
{
|
||||
name: "unspecified state",
|
||||
state: domain.AppStateUnspecified,
|
||||
expected: app.AppState_APP_STATE_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
name: "unknown state defaults to unspecified",
|
||||
state: domain.AppState(99),
|
||||
expected: app.AppState_APP_STATE_UNSPECIFIED,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := appStateToPb(tc.state)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppConfigToPb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
app *query.App
|
||||
expected app.ApplicationConfig
|
||||
}{
|
||||
{
|
||||
name: "OIDC config",
|
||||
app: &query.App{
|
||||
OIDCConfig: &query.OIDCApp{},
|
||||
},
|
||||
expected: &app.Application_OidcConfig{
|
||||
OidcConfig: &app.OIDCConfig{
|
||||
ResponseTypes: []app.OIDCResponseType{},
|
||||
GrantTypes: []app.OIDCGrantType{},
|
||||
ComplianceProblems: []*app.OIDCLocalizedMessage{},
|
||||
ClockSkew: &durationpb.Duration{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SAML config",
|
||||
app: &query.App{
|
||||
SAMLConfig: &query.SAMLApp{},
|
||||
},
|
||||
expected: &app.Application_SamlConfig{
|
||||
SamlConfig: &app.SAMLConfig{
|
||||
Metadata: &app.SAMLConfig_MetadataXml{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "API config",
|
||||
app: &query.App{
|
||||
APIConfig: &query.APIApp{},
|
||||
},
|
||||
expected: &app.Application_ApiConfig{
|
||||
ApiConfig: &app.APIConfig{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := appConfigToPb(tc.app)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginVersionToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
version *app.LoginVersion
|
||||
expectedVer *domain.LoginVersion
|
||||
expectedURI *string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "nil version",
|
||||
version: nil,
|
||||
expectedVer: gu.Ptr(domain.LoginVersionUnspecified),
|
||||
expectedURI: gu.Ptr(""),
|
||||
},
|
||||
{
|
||||
name: "login v1",
|
||||
version: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}},
|
||||
expectedVer: gu.Ptr(domain.LoginVersion1),
|
||||
expectedURI: gu.Ptr(""),
|
||||
},
|
||||
{
|
||||
name: "login v2 valid URI",
|
||||
version: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://valid.url")}}},
|
||||
expectedVer: gu.Ptr(domain.LoginVersion2),
|
||||
expectedURI: gu.Ptr("https://valid.url"),
|
||||
},
|
||||
{
|
||||
name: "login v2 invalid URI",
|
||||
version: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: gu.Ptr("://invalid")}}},
|
||||
expectedVer: gu.Ptr(domain.LoginVersion2),
|
||||
expectedURI: gu.Ptr("://invalid"),
|
||||
expectedError: &url.Error{Op: "parse", URL: "://invalid", Err: errors.New("missing protocol scheme")},
|
||||
},
|
||||
{
|
||||
name: "unknown version type",
|
||||
version: &app.LoginVersion{},
|
||||
expectedVer: gu.Ptr(domain.LoginVersionUnspecified),
|
||||
expectedURI: gu.Ptr(""),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
version, uri, err := loginVersionToDomain(tc.version)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedVer, version)
|
||||
assert.Equal(t, tc.expectedURI, uri)
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginVersionToPb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
version domain.LoginVersion
|
||||
baseURI *string
|
||||
expected *app.LoginVersion
|
||||
}{
|
||||
{
|
||||
name: "unspecified version",
|
||||
version: domain.LoginVersionUnspecified,
|
||||
baseURI: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "login v1",
|
||||
version: domain.LoginVersion1,
|
||||
baseURI: nil,
|
||||
expected: &app.LoginVersion{
|
||||
Version: &app.LoginVersion_LoginV1{
|
||||
LoginV1: &app.LoginV1{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "login v2",
|
||||
version: domain.LoginVersion2,
|
||||
baseURI: gu.Ptr("https://example.com"),
|
||||
expected: &app.LoginVersion{
|
||||
Version: &app.LoginVersion_LoginV2{
|
||||
LoginV2: &app.LoginV2{
|
||||
BaseUri: gu.Ptr("https://example.com"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown version",
|
||||
version: domain.LoginVersion(99),
|
||||
baseURI: nil,
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := loginVersionToPb(tc.version, tc.baseURI)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppQueryToModel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validAppNameSearchQuery, err := query.NewAppNameSearchQuery(query.TextEquals, "test")
|
||||
require.NoError(t, err)
|
||||
|
||||
validAppStateSearchQuery, err := query.NewAppStateSearchQuery(domain.AppStateActive)
|
||||
require.NoError(t, err)
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
query *app.ApplicationSearchFilter
|
||||
|
||||
expectedQuery query.SearchQuery
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "name query",
|
||||
query: &app.ApplicationSearchFilter{
|
||||
Filter: &app.ApplicationSearchFilter_NameFilter{
|
||||
NameFilter: &app.ApplicationNameQuery{
|
||||
Name: "test",
|
||||
Method: filter_pb_v2.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedQuery: validAppNameSearchQuery,
|
||||
},
|
||||
{
|
||||
name: "state query",
|
||||
query: &app.ApplicationSearchFilter{
|
||||
Filter: &app.ApplicationSearchFilter_StateFilter{
|
||||
StateFilter: app.AppState_APP_STATE_ACTIVE,
|
||||
},
|
||||
},
|
||||
expectedQuery: validAppStateSearchQuery,
|
||||
},
|
||||
{
|
||||
name: "api app only query",
|
||||
query: &app.ApplicationSearchFilter{
|
||||
Filter: &app.ApplicationSearchFilter_ApiAppOnly{},
|
||||
},
|
||||
expectedQuery: &query.NotNullQuery{
|
||||
Column: query.AppAPIConfigColumnAppID,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "oidc app only query",
|
||||
query: &app.ApplicationSearchFilter{
|
||||
Filter: &app.ApplicationSearchFilter_OidcAppOnly{},
|
||||
},
|
||||
expectedQuery: &query.NotNullQuery{
|
||||
Column: query.AppOIDCConfigColumnAppID,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "saml app only query",
|
||||
query: &app.ApplicationSearchFilter{
|
||||
Filter: &app.ApplicationSearchFilter_SamlAppOnly{},
|
||||
},
|
||||
expectedQuery: &query.NotNullQuery{
|
||||
Column: query.AppSAMLConfigColumnAppID,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid query type",
|
||||
query: &app.ApplicationSearchFilter{},
|
||||
expectedQuery: nil,
|
||||
expectedError: zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result, err := appQueryToModel(tc.query)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
assert.Equal(t, tc.expectedQuery, result)
|
||||
})
|
||||
}
|
||||
}
|
291
internal/api/grpc/app/v2beta/convert/oidc_app.go
Normal file
291
internal/api/grpc/app/v2beta/convert/oidc_app.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/muhlemmer/gu"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
)
|
||||
|
||||
func CreateOIDCAppRequestToDomain(name, projectID string, req *app.CreateOIDCApplicationRequest) (*domain.OIDCApp, error) {
|
||||
loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain.OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: projectID,
|
||||
},
|
||||
AppName: name,
|
||||
OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
|
||||
RedirectUris: req.GetRedirectUris(),
|
||||
ResponseTypes: oidcResponseTypesToDomain(req.GetResponseTypes()),
|
||||
GrantTypes: oidcGrantTypesToDomain(req.GetGrantTypes()),
|
||||
ApplicationType: gu.Ptr(oidcApplicationTypeToDomain(req.GetAppType())),
|
||||
AuthMethodType: gu.Ptr(oidcAuthMethodTypeToDomain(req.GetAuthMethodType())),
|
||||
PostLogoutRedirectUris: req.GetPostLogoutRedirectUris(),
|
||||
DevMode: &req.DevMode,
|
||||
AccessTokenType: gu.Ptr(oidcTokenTypeToDomain(req.GetAccessTokenType())),
|
||||
AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()),
|
||||
IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()),
|
||||
IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()),
|
||||
ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()),
|
||||
AdditionalOrigins: req.GetAdditionalOrigins(),
|
||||
SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()),
|
||||
BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()),
|
||||
LoginVersion: loginVersion,
|
||||
LoginBaseURI: loginBaseURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UpdateOIDCAppConfigRequestToDomain(appID, projectID string, app *app.UpdateOIDCApplicationConfigurationRequest) (*domain.OIDCApp, error) {
|
||||
loginVersion, loginBaseURI, err := loginVersionToDomain(app.GetLoginVersion())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain.OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: projectID,
|
||||
},
|
||||
AppID: appID,
|
||||
RedirectUris: app.RedirectUris,
|
||||
ResponseTypes: oidcResponseTypesToDomain(app.ResponseTypes),
|
||||
GrantTypes: oidcGrantTypesToDomain(app.GrantTypes),
|
||||
ApplicationType: oidcApplicationTypeToDomainPtr(app.AppType),
|
||||
AuthMethodType: oidcAuthMethodTypeToDomainPtr(app.AuthMethodType),
|
||||
PostLogoutRedirectUris: app.PostLogoutRedirectUris,
|
||||
DevMode: app.DevMode,
|
||||
AccessTokenType: oidcTokenTypeToDomainPtr(app.AccessTokenType),
|
||||
AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
|
||||
IDTokenRoleAssertion: app.IdTokenRoleAssertion,
|
||||
IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion,
|
||||
ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()),
|
||||
AdditionalOrigins: app.AdditionalOrigins,
|
||||
SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage,
|
||||
BackChannelLogoutURI: app.BackChannelLogoutUri,
|
||||
LoginVersion: loginVersion,
|
||||
LoginBaseURI: loginBaseURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func oidcResponseTypesToDomain(responseTypes []app.OIDCResponseType) []domain.OIDCResponseType {
|
||||
if len(responseTypes) == 0 {
|
||||
return []domain.OIDCResponseType{domain.OIDCResponseTypeCode}
|
||||
}
|
||||
oidcResponseTypes := make([]domain.OIDCResponseType, len(responseTypes))
|
||||
for i, responseType := range responseTypes {
|
||||
switch responseType {
|
||||
case app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED:
|
||||
oidcResponseTypes[i] = domain.OIDCResponseTypeUnspecified
|
||||
case app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE:
|
||||
oidcResponseTypes[i] = domain.OIDCResponseTypeCode
|
||||
case app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN:
|
||||
oidcResponseTypes[i] = domain.OIDCResponseTypeIDToken
|
||||
case app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN:
|
||||
oidcResponseTypes[i] = domain.OIDCResponseTypeIDTokenToken
|
||||
}
|
||||
}
|
||||
return oidcResponseTypes
|
||||
}
|
||||
|
||||
func oidcGrantTypesToDomain(grantTypes []app.OIDCGrantType) []domain.OIDCGrantType {
|
||||
if len(grantTypes) == 0 {
|
||||
return []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}
|
||||
}
|
||||
oidcGrantTypes := make([]domain.OIDCGrantType, len(grantTypes))
|
||||
for i, grantType := range grantTypes {
|
||||
switch grantType {
|
||||
case app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE:
|
||||
oidcGrantTypes[i] = domain.OIDCGrantTypeAuthorizationCode
|
||||
case app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT:
|
||||
oidcGrantTypes[i] = domain.OIDCGrantTypeImplicit
|
||||
case app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN:
|
||||
oidcGrantTypes[i] = domain.OIDCGrantTypeRefreshToken
|
||||
case app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE:
|
||||
oidcGrantTypes[i] = domain.OIDCGrantTypeDeviceCode
|
||||
case app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE:
|
||||
oidcGrantTypes[i] = domain.OIDCGrantTypeTokenExchange
|
||||
}
|
||||
}
|
||||
return oidcGrantTypes
|
||||
}
|
||||
|
||||
func oidcApplicationTypeToDomainPtr(appType *app.OIDCAppType) *domain.OIDCApplicationType {
|
||||
if appType == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := oidcApplicationTypeToDomain(*appType)
|
||||
return &res
|
||||
}
|
||||
|
||||
func oidcApplicationTypeToDomain(appType app.OIDCAppType) domain.OIDCApplicationType {
|
||||
switch appType {
|
||||
case app.OIDCAppType_OIDC_APP_TYPE_WEB:
|
||||
return domain.OIDCApplicationTypeWeb
|
||||
case app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT:
|
||||
return domain.OIDCApplicationTypeUserAgent
|
||||
case app.OIDCAppType_OIDC_APP_TYPE_NATIVE:
|
||||
return domain.OIDCApplicationTypeNative
|
||||
}
|
||||
return domain.OIDCApplicationTypeWeb
|
||||
}
|
||||
|
||||
func oidcAuthMethodTypeToDomainPtr(authType *app.OIDCAuthMethodType) *domain.OIDCAuthMethodType {
|
||||
if authType == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := oidcAuthMethodTypeToDomain(*authType)
|
||||
return &res
|
||||
}
|
||||
|
||||
func oidcAuthMethodTypeToDomain(authType app.OIDCAuthMethodType) domain.OIDCAuthMethodType {
|
||||
switch authType {
|
||||
case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC:
|
||||
return domain.OIDCAuthMethodTypeBasic
|
||||
case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST:
|
||||
return domain.OIDCAuthMethodTypePost
|
||||
case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE:
|
||||
return domain.OIDCAuthMethodTypeNone
|
||||
case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT:
|
||||
return domain.OIDCAuthMethodTypePrivateKeyJWT
|
||||
default:
|
||||
return domain.OIDCAuthMethodTypeBasic
|
||||
}
|
||||
}
|
||||
|
||||
func oidcTokenTypeToDomainPtr(tokenType *app.OIDCTokenType) *domain.OIDCTokenType {
|
||||
if tokenType == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := oidcTokenTypeToDomain(*tokenType)
|
||||
return &res
|
||||
}
|
||||
|
||||
func oidcTokenTypeToDomain(tokenType app.OIDCTokenType) domain.OIDCTokenType {
|
||||
switch tokenType {
|
||||
case app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER:
|
||||
return domain.OIDCTokenTypeBearer
|
||||
case app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT:
|
||||
return domain.OIDCTokenTypeJWT
|
||||
default:
|
||||
return domain.OIDCTokenTypeBearer
|
||||
}
|
||||
}
|
||||
|
||||
func ComplianceProblemsToLocalizedMessages(complianceProblems []string) []*app.OIDCLocalizedMessage {
|
||||
converted := make([]*app.OIDCLocalizedMessage, len(complianceProblems))
|
||||
for i, p := range complianceProblems {
|
||||
converted[i] = &app.OIDCLocalizedMessage{Key: p}
|
||||
}
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
func appOIDCConfigToPb(oidcApp *query.OIDCApp) *app.Application_OidcConfig {
|
||||
return &app.Application_OidcConfig{
|
||||
OidcConfig: &app.OIDCConfig{
|
||||
RedirectUris: oidcApp.RedirectURIs,
|
||||
ResponseTypes: oidcResponseTypesFromModel(oidcApp.ResponseTypes),
|
||||
GrantTypes: oidcGrantTypesFromModel(oidcApp.GrantTypes),
|
||||
AppType: oidcApplicationTypeToPb(oidcApp.AppType),
|
||||
ClientId: oidcApp.ClientID,
|
||||
AuthMethodType: oidcAuthMethodTypeToPb(oidcApp.AuthMethodType),
|
||||
PostLogoutRedirectUris: oidcApp.PostLogoutRedirectURIs,
|
||||
Version: app.OIDCVersion_OIDC_VERSION_1_0,
|
||||
NoneCompliant: len(oidcApp.ComplianceProblems) != 0,
|
||||
ComplianceProblems: ComplianceProblemsToLocalizedMessages(oidcApp.ComplianceProblems),
|
||||
DevMode: oidcApp.IsDevMode,
|
||||
AccessTokenType: oidcTokenTypeToPb(oidcApp.AccessTokenType),
|
||||
AccessTokenRoleAssertion: oidcApp.AssertAccessTokenRole,
|
||||
IdTokenRoleAssertion: oidcApp.AssertIDTokenRole,
|
||||
IdTokenUserinfoAssertion: oidcApp.AssertIDTokenUserinfo,
|
||||
ClockSkew: durationpb.New(oidcApp.ClockSkew),
|
||||
AdditionalOrigins: oidcApp.AdditionalOrigins,
|
||||
AllowedOrigins: oidcApp.AllowedOrigins,
|
||||
SkipNativeAppSuccessPage: oidcApp.SkipNativeAppSuccessPage,
|
||||
BackChannelLogoutUri: oidcApp.BackChannelLogoutURI,
|
||||
LoginVersion: loginVersionToPb(oidcApp.LoginVersion, oidcApp.LoginBaseURI),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func oidcResponseTypesFromModel(responseTypes []domain.OIDCResponseType) []app.OIDCResponseType {
|
||||
oidcResponseTypes := make([]app.OIDCResponseType, len(responseTypes))
|
||||
for i, responseType := range responseTypes {
|
||||
switch responseType {
|
||||
case domain.OIDCResponseTypeUnspecified:
|
||||
oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED
|
||||
case domain.OIDCResponseTypeCode:
|
||||
oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE
|
||||
case domain.OIDCResponseTypeIDToken:
|
||||
oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN
|
||||
case domain.OIDCResponseTypeIDTokenToken:
|
||||
oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN
|
||||
}
|
||||
}
|
||||
return oidcResponseTypes
|
||||
}
|
||||
|
||||
func oidcGrantTypesFromModel(grantTypes []domain.OIDCGrantType) []app.OIDCGrantType {
|
||||
oidcGrantTypes := make([]app.OIDCGrantType, len(grantTypes))
|
||||
for i, grantType := range grantTypes {
|
||||
switch grantType {
|
||||
case domain.OIDCGrantTypeAuthorizationCode:
|
||||
oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE
|
||||
case domain.OIDCGrantTypeImplicit:
|
||||
oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT
|
||||
case domain.OIDCGrantTypeRefreshToken:
|
||||
oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN
|
||||
case domain.OIDCGrantTypeDeviceCode:
|
||||
oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE
|
||||
case domain.OIDCGrantTypeTokenExchange:
|
||||
oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE
|
||||
}
|
||||
}
|
||||
return oidcGrantTypes
|
||||
}
|
||||
|
||||
func oidcApplicationTypeToPb(appType domain.OIDCApplicationType) app.OIDCAppType {
|
||||
switch appType {
|
||||
case domain.OIDCApplicationTypeWeb:
|
||||
return app.OIDCAppType_OIDC_APP_TYPE_WEB
|
||||
case domain.OIDCApplicationTypeUserAgent:
|
||||
return app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT
|
||||
case domain.OIDCApplicationTypeNative:
|
||||
return app.OIDCAppType_OIDC_APP_TYPE_NATIVE
|
||||
default:
|
||||
return app.OIDCAppType_OIDC_APP_TYPE_WEB
|
||||
}
|
||||
}
|
||||
|
||||
func oidcAuthMethodTypeToPb(authType domain.OIDCAuthMethodType) app.OIDCAuthMethodType {
|
||||
switch authType {
|
||||
case domain.OIDCAuthMethodTypeBasic:
|
||||
return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC
|
||||
case domain.OIDCAuthMethodTypePost:
|
||||
return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST
|
||||
case domain.OIDCAuthMethodTypeNone:
|
||||
return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE
|
||||
case domain.OIDCAuthMethodTypePrivateKeyJWT:
|
||||
return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT
|
||||
default:
|
||||
return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC
|
||||
}
|
||||
}
|
||||
|
||||
func oidcTokenTypeToPb(tokenType domain.OIDCTokenType) app.OIDCTokenType {
|
||||
switch tokenType {
|
||||
case domain.OIDCTokenTypeBearer:
|
||||
return app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER
|
||||
case domain.OIDCTokenTypeJWT:
|
||||
return app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT
|
||||
default:
|
||||
return app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER
|
||||
}
|
||||
}
|
755
internal/api/grpc/app/v2beta/convert/oidc_app_test.go
Normal file
755
internal/api/grpc/app/v2beta/convert/oidc_app_test.go
Normal file
@@ -0,0 +1,755 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
)
|
||||
|
||||
func TestCreateOIDCAppRequestToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
projectID string
|
||||
req *app.CreateOIDCApplicationRequest
|
||||
|
||||
expectedModel *domain.OIDCApp
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
testName: "unparsable login version 2 URL",
|
||||
projectID: "pid",
|
||||
req: &app.CreateOIDCApplicationRequest{
|
||||
LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{
|
||||
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}},
|
||||
},
|
||||
},
|
||||
expectedModel: nil,
|
||||
expectedError: &url.Error{
|
||||
URL: "%+o",
|
||||
Op: "parse",
|
||||
Err: url.EscapeError("%+o"),
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "all fields set",
|
||||
projectID: "project1",
|
||||
req: &app.CreateOIDCApplicationRequest{
|
||||
RedirectUris: []string{"https://redirect"},
|
||||
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
|
||||
GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
|
||||
AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
|
||||
AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
|
||||
PostLogoutRedirectUris: []string{"https://logout"},
|
||||
DevMode: true,
|
||||
AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
|
||||
AccessTokenRoleAssertion: true,
|
||||
IdTokenRoleAssertion: true,
|
||||
IdTokenUserinfoAssertion: true,
|
||||
ClockSkew: durationpb.New(5 * time.Second),
|
||||
AdditionalOrigins: []string{"https://origin"},
|
||||
SkipNativeAppSuccessPage: true,
|
||||
BackChannelLogoutUri: "https://backchannel",
|
||||
LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{
|
||||
BaseUri: gu.Ptr("https://login"),
|
||||
}}},
|
||||
},
|
||||
expectedModel: &domain.OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "project1"},
|
||||
AppName: "all fields set",
|
||||
OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
|
||||
RedirectUris: []string{"https://redirect"},
|
||||
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
|
||||
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
|
||||
ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
|
||||
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic),
|
||||
PostLogoutRedirectUris: []string{"https://logout"},
|
||||
DevMode: gu.Ptr(true),
|
||||
AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
|
||||
AccessTokenRoleAssertion: gu.Ptr(true),
|
||||
IDTokenRoleAssertion: gu.Ptr(true),
|
||||
IDTokenUserinfoAssertion: gu.Ptr(true),
|
||||
ClockSkew: gu.Ptr(5 * time.Second),
|
||||
AdditionalOrigins: []string{"https://origin"},
|
||||
SkipNativeAppSuccessPage: gu.Ptr(true),
|
||||
BackChannelLogoutURI: gu.Ptr("https://backchannel"),
|
||||
LoginVersion: gu.Ptr(domain.LoginVersion2),
|
||||
LoginBaseURI: gu.Ptr("https://login"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
res, err := CreateOIDCAppRequestToDomain(tc.testName, tc.projectID, tc.req)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
assert.Equal(t, tc.expectedModel, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateOIDCAppConfigRequestToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
|
||||
appID string
|
||||
projectID string
|
||||
req *app.UpdateOIDCApplicationConfigurationRequest
|
||||
|
||||
expectedModel *domain.OIDCApp
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
testName: "unparsable login version 2 URL",
|
||||
appID: "app1",
|
||||
projectID: "pid",
|
||||
req: &app.UpdateOIDCApplicationConfigurationRequest{
|
||||
LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{
|
||||
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")},
|
||||
}},
|
||||
},
|
||||
expectedModel: nil,
|
||||
expectedError: &url.Error{
|
||||
URL: "%+o",
|
||||
Op: "parse",
|
||||
Err: url.EscapeError("%+o"),
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "successful Update",
|
||||
appID: "app1",
|
||||
projectID: "proj1",
|
||||
req: &app.UpdateOIDCApplicationConfigurationRequest{
|
||||
RedirectUris: []string{"https://redirect"},
|
||||
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
|
||||
GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
|
||||
AppType: gu.Ptr(app.OIDCAppType_OIDC_APP_TYPE_WEB),
|
||||
AuthMethodType: gu.Ptr(app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC),
|
||||
PostLogoutRedirectUris: []string{"https://logout"},
|
||||
DevMode: gu.Ptr(true),
|
||||
AccessTokenType: gu.Ptr(app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER),
|
||||
AccessTokenRoleAssertion: gu.Ptr(true),
|
||||
IdTokenRoleAssertion: gu.Ptr(true),
|
||||
IdTokenUserinfoAssertion: gu.Ptr(true),
|
||||
ClockSkew: durationpb.New(5 * time.Second),
|
||||
AdditionalOrigins: []string{"https://origin"},
|
||||
SkipNativeAppSuccessPage: gu.Ptr(true),
|
||||
BackChannelLogoutUri: gu.Ptr("https://backchannel"),
|
||||
LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{
|
||||
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://login")},
|
||||
}},
|
||||
},
|
||||
expectedModel: &domain.OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "proj1"},
|
||||
AppID: "app1",
|
||||
RedirectUris: []string{"https://redirect"},
|
||||
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
|
||||
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
|
||||
ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
|
||||
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic),
|
||||
PostLogoutRedirectUris: []string{"https://logout"},
|
||||
DevMode: gu.Ptr(true),
|
||||
AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
|
||||
AccessTokenRoleAssertion: gu.Ptr(true),
|
||||
IDTokenRoleAssertion: gu.Ptr(true),
|
||||
IDTokenUserinfoAssertion: gu.Ptr(true),
|
||||
ClockSkew: gu.Ptr(5 * time.Second),
|
||||
AdditionalOrigins: []string{"https://origin"},
|
||||
SkipNativeAppSuccessPage: gu.Ptr(true),
|
||||
BackChannelLogoutURI: gu.Ptr("https://backchannel"),
|
||||
LoginVersion: gu.Ptr(domain.LoginVersion2),
|
||||
LoginBaseURI: gu.Ptr("https://login"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
got, err := UpdateOIDCAppConfigRequestToDomain(tc.appID, tc.projectID, tc.req)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
assert.Equal(t, tc.expectedModel, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCResponseTypesToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
inputResponseType []app.OIDCResponseType
|
||||
expectedResponse []domain.OIDCResponseType
|
||||
}{
|
||||
{
|
||||
testName: "empty response types",
|
||||
inputResponseType: []app.OIDCResponseType{},
|
||||
expectedResponse: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
|
||||
},
|
||||
{
|
||||
testName: "all response types",
|
||||
inputResponseType: []app.OIDCResponseType{
|
||||
app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED,
|
||||
app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
|
||||
app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN,
|
||||
app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN,
|
||||
},
|
||||
expectedResponse: []domain.OIDCResponseType{
|
||||
domain.OIDCResponseTypeUnspecified,
|
||||
domain.OIDCResponseTypeCode,
|
||||
domain.OIDCResponseTypeIDToken,
|
||||
domain.OIDCResponseTypeIDTokenToken,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "single response type",
|
||||
inputResponseType: []app.OIDCResponseType{
|
||||
app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
|
||||
},
|
||||
expectedResponse: []domain.OIDCResponseType{
|
||||
domain.OIDCResponseTypeCode,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
res := oidcResponseTypesToDomain(tc.inputResponseType)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedResponse, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCGrantTypesToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
inputGrantType []app.OIDCGrantType
|
||||
expectedGrants []domain.OIDCGrantType
|
||||
}{
|
||||
{
|
||||
testName: "empty grant types",
|
||||
inputGrantType: []app.OIDCGrantType{},
|
||||
expectedGrants: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
|
||||
},
|
||||
{
|
||||
testName: "all grant types",
|
||||
inputGrantType: []app.OIDCGrantType{
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT,
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN,
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE,
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
|
||||
},
|
||||
expectedGrants: []domain.OIDCGrantType{
|
||||
domain.OIDCGrantTypeAuthorizationCode,
|
||||
domain.OIDCGrantTypeImplicit,
|
||||
domain.OIDCGrantTypeRefreshToken,
|
||||
domain.OIDCGrantTypeDeviceCode,
|
||||
domain.OIDCGrantTypeTokenExchange,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "single grant type",
|
||||
inputGrantType: []app.OIDCGrantType{
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
},
|
||||
expectedGrants: []domain.OIDCGrantType{
|
||||
domain.OIDCGrantTypeAuthorizationCode,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
res := oidcGrantTypesToDomain(tc.inputGrantType)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedGrants, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCApplicationTypeToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
appType app.OIDCAppType
|
||||
expected domain.OIDCApplicationType
|
||||
}{
|
||||
{
|
||||
name: "web type",
|
||||
appType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
|
||||
expected: domain.OIDCApplicationTypeWeb,
|
||||
},
|
||||
{
|
||||
name: "user agent type",
|
||||
appType: app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT,
|
||||
expected: domain.OIDCApplicationTypeUserAgent,
|
||||
},
|
||||
{
|
||||
name: "native type",
|
||||
appType: app.OIDCAppType_OIDC_APP_TYPE_NATIVE,
|
||||
expected: domain.OIDCApplicationTypeNative,
|
||||
},
|
||||
{
|
||||
name: "unspecified type defaults to web",
|
||||
expected: domain.OIDCApplicationTypeWeb,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := oidcApplicationTypeToDomain(tc.appType)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCAuthMethodTypeToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
authType app.OIDCAuthMethodType
|
||||
expectedResponse domain.OIDCAuthMethodType
|
||||
}{
|
||||
{
|
||||
name: "basic auth type",
|
||||
authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
|
||||
expectedResponse: domain.OIDCAuthMethodTypeBasic,
|
||||
},
|
||||
{
|
||||
name: "post auth type",
|
||||
authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST,
|
||||
expectedResponse: domain.OIDCAuthMethodTypePost,
|
||||
},
|
||||
{
|
||||
name: "none auth type",
|
||||
authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
|
||||
expectedResponse: domain.OIDCAuthMethodTypeNone,
|
||||
},
|
||||
{
|
||||
name: "private key jwt auth type",
|
||||
authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
|
||||
expectedResponse: domain.OIDCAuthMethodTypePrivateKeyJWT,
|
||||
},
|
||||
{
|
||||
name: "unspecified auth type defaults to basic",
|
||||
expectedResponse: domain.OIDCAuthMethodTypeBasic,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
res := oidcAuthMethodTypeToDomain(tc.authType)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedResponse, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCTokenTypeToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
tokenType app.OIDCTokenType
|
||||
expectedType domain.OIDCTokenType
|
||||
}{
|
||||
{
|
||||
name: "bearer token type",
|
||||
tokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
|
||||
expectedType: domain.OIDCTokenTypeBearer,
|
||||
},
|
||||
{
|
||||
name: "jwt token type",
|
||||
tokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
|
||||
expectedType: domain.OIDCTokenTypeJWT,
|
||||
},
|
||||
{
|
||||
name: "unspecified defaults to bearer",
|
||||
expectedType: domain.OIDCTokenTypeBearer,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := oidcTokenTypeToDomain(tc.tokenType)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedType, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestAppOIDCConfigToPb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
input *query.OIDCApp
|
||||
expected *app.Application_OidcConfig
|
||||
}{
|
||||
{
|
||||
name: "empty config",
|
||||
input: &query.OIDCApp{},
|
||||
expected: &app.Application_OidcConfig{
|
||||
OidcConfig: &app.OIDCConfig{
|
||||
Version: app.OIDCVersion_OIDC_VERSION_1_0,
|
||||
ComplianceProblems: []*app.OIDCLocalizedMessage{},
|
||||
ClockSkew: durationpb.New(0),
|
||||
ResponseTypes: []app.OIDCResponseType{},
|
||||
GrantTypes: []app.OIDCGrantType{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "full config",
|
||||
input: &query.OIDCApp{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
|
||||
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
|
||||
AppType: domain.OIDCApplicationTypeWeb,
|
||||
ClientID: "client123",
|
||||
AuthMethodType: domain.OIDCAuthMethodTypeBasic,
|
||||
PostLogoutRedirectURIs: []string{"https://example.com/logout"},
|
||||
ComplianceProblems: []string{"problem1", "problem2"},
|
||||
IsDevMode: true,
|
||||
AccessTokenType: domain.OIDCTokenTypeBearer,
|
||||
AssertAccessTokenRole: true,
|
||||
AssertIDTokenRole: true,
|
||||
AssertIDTokenUserinfo: true,
|
||||
ClockSkew: 5 * time.Second,
|
||||
AdditionalOrigins: []string{"https://app.example.com"},
|
||||
AllowedOrigins: []string{"https://allowed.example.com"},
|
||||
SkipNativeAppSuccessPage: true,
|
||||
BackChannelLogoutURI: "https://example.com/backchannel",
|
||||
LoginVersion: domain.LoginVersion2,
|
||||
LoginBaseURI: gu.Ptr("https://login.example.com"),
|
||||
},
|
||||
expected: &app.Application_OidcConfig{
|
||||
OidcConfig: &app.OIDCConfig{
|
||||
RedirectUris: []string{"https://example.com/callback"},
|
||||
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
|
||||
GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
|
||||
AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
|
||||
ClientId: "client123",
|
||||
AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
|
||||
PostLogoutRedirectUris: []string{"https://example.com/logout"},
|
||||
Version: app.OIDCVersion_OIDC_VERSION_1_0,
|
||||
NoneCompliant: true,
|
||||
ComplianceProblems: []*app.OIDCLocalizedMessage{
|
||||
{Key: "problem1"},
|
||||
{Key: "problem2"},
|
||||
},
|
||||
DevMode: true,
|
||||
AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
|
||||
AccessTokenRoleAssertion: true,
|
||||
IdTokenRoleAssertion: true,
|
||||
IdTokenUserinfoAssertion: true,
|
||||
ClockSkew: durationpb.New(5 * time.Second),
|
||||
AdditionalOrigins: []string{"https://app.example.com"},
|
||||
AllowedOrigins: []string{"https://allowed.example.com"},
|
||||
SkipNativeAppSuccessPage: true,
|
||||
BackChannelLogoutUri: "https://example.com/backchannel",
|
||||
LoginVersion: &app.LoginVersion{
|
||||
Version: &app.LoginVersion_LoginV2{
|
||||
LoginV2: &app.LoginV2{
|
||||
BaseUri: gu.Ptr("https://login.example.com"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tt {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := appOIDCConfigToPb(tt.input)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCResponseTypesFromModel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
responseTypes []domain.OIDCResponseType
|
||||
expected []app.OIDCResponseType
|
||||
}{
|
||||
{
|
||||
name: "empty response types",
|
||||
responseTypes: []domain.OIDCResponseType{},
|
||||
expected: []app.OIDCResponseType{},
|
||||
},
|
||||
{
|
||||
name: "all response types",
|
||||
responseTypes: []domain.OIDCResponseType{
|
||||
domain.OIDCResponseTypeUnspecified,
|
||||
domain.OIDCResponseTypeCode,
|
||||
domain.OIDCResponseTypeIDToken,
|
||||
domain.OIDCResponseTypeIDTokenToken,
|
||||
},
|
||||
expected: []app.OIDCResponseType{
|
||||
app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED,
|
||||
app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
|
||||
app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN,
|
||||
app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single response type",
|
||||
responseTypes: []domain.OIDCResponseType{
|
||||
domain.OIDCResponseTypeCode,
|
||||
},
|
||||
expected: []app.OIDCResponseType{
|
||||
app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := oidcResponseTypesFromModel(tc.responseTypes)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestOIDCGrantTypesFromModel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
grantTypes []domain.OIDCGrantType
|
||||
expected []app.OIDCGrantType
|
||||
}{
|
||||
{
|
||||
name: "empty grant types",
|
||||
grantTypes: []domain.OIDCGrantType{},
|
||||
expected: []app.OIDCGrantType{},
|
||||
},
|
||||
{
|
||||
name: "all grant types",
|
||||
grantTypes: []domain.OIDCGrantType{
|
||||
domain.OIDCGrantTypeAuthorizationCode,
|
||||
domain.OIDCGrantTypeImplicit,
|
||||
domain.OIDCGrantTypeRefreshToken,
|
||||
domain.OIDCGrantTypeDeviceCode,
|
||||
domain.OIDCGrantTypeTokenExchange,
|
||||
},
|
||||
expected: []app.OIDCGrantType{
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT,
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN,
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE,
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single grant type",
|
||||
grantTypes: []domain.OIDCGrantType{
|
||||
domain.OIDCGrantTypeAuthorizationCode,
|
||||
},
|
||||
expected: []app.OIDCGrantType{
|
||||
app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := oidcGrantTypesFromModel(tc.grantTypes)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCApplicationTypeToPb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
appType domain.OIDCApplicationType
|
||||
expected app.OIDCAppType
|
||||
}{
|
||||
{
|
||||
name: "web type",
|
||||
appType: domain.OIDCApplicationTypeWeb,
|
||||
expected: app.OIDCAppType_OIDC_APP_TYPE_WEB,
|
||||
},
|
||||
{
|
||||
name: "user agent type",
|
||||
appType: domain.OIDCApplicationTypeUserAgent,
|
||||
expected: app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT,
|
||||
},
|
||||
{
|
||||
name: "native type",
|
||||
appType: domain.OIDCApplicationTypeNative,
|
||||
expected: app.OIDCAppType_OIDC_APP_TYPE_NATIVE,
|
||||
},
|
||||
{
|
||||
name: "unspecified type defaults to web",
|
||||
appType: domain.OIDCApplicationType(999), // Invalid value to trigger default case
|
||||
expected: app.OIDCAppType_OIDC_APP_TYPE_WEB,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := oidcApplicationTypeToPb(tc.appType)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCAuthMethodTypeToPb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
authType domain.OIDCAuthMethodType
|
||||
expected app.OIDCAuthMethodType
|
||||
}{
|
||||
{
|
||||
name: "basic auth type",
|
||||
authType: domain.OIDCAuthMethodTypeBasic,
|
||||
expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
|
||||
},
|
||||
{
|
||||
name: "post auth type",
|
||||
authType: domain.OIDCAuthMethodTypePost,
|
||||
expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST,
|
||||
},
|
||||
{
|
||||
name: "none auth type",
|
||||
authType: domain.OIDCAuthMethodTypeNone,
|
||||
expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
|
||||
},
|
||||
{
|
||||
name: "private key jwt auth type",
|
||||
authType: domain.OIDCAuthMethodTypePrivateKeyJWT,
|
||||
expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
|
||||
},
|
||||
{
|
||||
name: "unknown auth type defaults to basic",
|
||||
authType: domain.OIDCAuthMethodType(999),
|
||||
expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := oidcAuthMethodTypeToPb(tc.authType)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCTokenTypeToPb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
tokenType domain.OIDCTokenType
|
||||
expected app.OIDCTokenType
|
||||
}{
|
||||
{
|
||||
name: "bearer token type",
|
||||
tokenType: domain.OIDCTokenTypeBearer,
|
||||
expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
|
||||
},
|
||||
{
|
||||
name: "jwt token type",
|
||||
tokenType: domain.OIDCTokenTypeJWT,
|
||||
expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
|
||||
},
|
||||
{
|
||||
name: "unknown token type defaults to bearer",
|
||||
tokenType: domain.OIDCTokenType(999), // Invalid value to trigger default case
|
||||
expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
result := oidcTokenTypeToPb(tc.tokenType)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
77
internal/api/grpc/app/v2beta/convert/saml_app.go
Normal file
77
internal/api/grpc/app/v2beta/convert/saml_app.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
)
|
||||
|
||||
func CreateSAMLAppRequestToDomain(name, projectID string, req *app.CreateSAMLApplicationRequest) (*domain.SAMLApp, error) {
|
||||
loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: projectID,
|
||||
},
|
||||
AppName: name,
|
||||
Metadata: req.GetMetadataXml(),
|
||||
MetadataURL: gu.Ptr(req.GetMetadataUrl()),
|
||||
LoginVersion: loginVersion,
|
||||
LoginBaseURI: loginBaseURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UpdateSAMLAppConfigRequestToDomain(appID, projectID string, app *app.UpdateSAMLApplicationConfigurationRequest) (*domain.SAMLApp, error) {
|
||||
loginVersion, loginBaseURI, err := loginVersionToDomain(app.GetLoginVersion())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metasXML, metasURL := metasToDomain(app.GetMetadata())
|
||||
return &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: projectID,
|
||||
},
|
||||
AppID: appID,
|
||||
Metadata: metasXML,
|
||||
MetadataURL: metasURL,
|
||||
LoginVersion: loginVersion,
|
||||
LoginBaseURI: loginBaseURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func metasToDomain(metas app.MetaType) ([]byte, *string) {
|
||||
switch t := metas.(type) {
|
||||
case *app.UpdateSAMLApplicationConfigurationRequest_MetadataXml:
|
||||
return t.MetadataXml, nil
|
||||
case *app.UpdateSAMLApplicationConfigurationRequest_MetadataUrl:
|
||||
return nil, &t.MetadataUrl
|
||||
case nil:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func appSAMLConfigToPb(samlApp *query.SAMLApp) app.ApplicationConfig {
|
||||
if samlApp == nil {
|
||||
return &app.Application_SamlConfig{
|
||||
SamlConfig: &app.SAMLConfig{
|
||||
Metadata: &app.SAMLConfig_MetadataXml{},
|
||||
LoginVersion: &app.LoginVersion{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &app.Application_SamlConfig{
|
||||
SamlConfig: &app.SAMLConfig{
|
||||
Metadata: &app.SAMLConfig_MetadataXml{MetadataXml: samlApp.Metadata},
|
||||
LoginVersion: loginVersionToPb(samlApp.LoginVersion, samlApp.LoginBaseURI),
|
||||
},
|
||||
}
|
||||
}
|
256
internal/api/grpc/app/v2beta/convert/saml_app_test.go
Normal file
256
internal/api/grpc/app/v2beta/convert/saml_app_test.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
)
|
||||
|
||||
func samlMetadataGen(entityID string) []byte {
|
||||
str := fmt.Sprintf(`<?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(gofakeit.URL())
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
appName string
|
||||
projectID string
|
||||
req *app.CreateSAMLApplicationRequest
|
||||
|
||||
expectedResponse *domain.SAMLApp
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
testName: "login version error",
|
||||
appName: "test-app",
|
||||
projectID: "proj-1",
|
||||
req: &app.CreateSAMLApplicationRequest{
|
||||
Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
|
||||
MetadataXml: samlMetadataGen(gofakeit.URL()),
|
||||
},
|
||||
LoginVersion: &app.LoginVersion{
|
||||
Version: &app.LoginVersion_LoginV2{
|
||||
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: &url.Error{
|
||||
URL: "%+o",
|
||||
Op: "parse",
|
||||
Err: url.EscapeError("%+o"),
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "valid request",
|
||||
appName: "test-app",
|
||||
projectID: "proj-1",
|
||||
req: &app.CreateSAMLApplicationRequest{
|
||||
Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
|
||||
MetadataXml: genMetaForValidRequest,
|
||||
},
|
||||
LoginVersion: nil,
|
||||
},
|
||||
|
||||
expectedResponse: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
|
||||
AppName: "test-app",
|
||||
Metadata: genMetaForValidRequest,
|
||||
MetadataURL: gu.Ptr(""),
|
||||
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
|
||||
LoginBaseURI: gu.Ptr(""),
|
||||
State: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "nil request",
|
||||
appName: "test-app",
|
||||
projectID: "proj-1",
|
||||
req: nil,
|
||||
|
||||
expectedResponse: &domain.SAMLApp{
|
||||
AppName: "test-app",
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
|
||||
MetadataURL: gu.Ptr(""),
|
||||
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
|
||||
LoginBaseURI: gu.Ptr(""),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
res, err := CreateSAMLAppRequestToDomain(tc.appName, tc.projectID, tc.req)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
assert.Equal(t, tc.expectedResponse, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestUpdateSAMLAppConfigRequestToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
genMetaForValidRequest := samlMetadataGen(gofakeit.URL())
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
appID string
|
||||
projectID string
|
||||
req *app.UpdateSAMLApplicationConfigurationRequest
|
||||
|
||||
expectedResponse *domain.SAMLApp
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
testName: "login version error",
|
||||
appID: "app-1",
|
||||
projectID: "proj-1",
|
||||
req: &app.UpdateSAMLApplicationConfigurationRequest{
|
||||
Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
|
||||
MetadataXml: samlMetadataGen(gofakeit.URL()),
|
||||
},
|
||||
LoginVersion: &app.LoginVersion{
|
||||
Version: &app.LoginVersion_LoginV2{
|
||||
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: &url.Error{
|
||||
URL: "%+o",
|
||||
Op: "parse",
|
||||
Err: url.EscapeError("%+o"),
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "valid request",
|
||||
appID: "app-1",
|
||||
projectID: "proj-1",
|
||||
req: &app.UpdateSAMLApplicationConfigurationRequest{
|
||||
Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
|
||||
MetadataXml: genMetaForValidRequest,
|
||||
},
|
||||
LoginVersion: nil,
|
||||
},
|
||||
expectedResponse: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
|
||||
AppID: "app-1",
|
||||
Metadata: genMetaForValidRequest,
|
||||
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
|
||||
LoginBaseURI: gu.Ptr(""),
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "nil request",
|
||||
appID: "app-1",
|
||||
projectID: "proj-1",
|
||||
req: nil,
|
||||
expectedResponse: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
|
||||
AppID: "app-1",
|
||||
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
|
||||
LoginBaseURI: gu.Ptr(""),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
res, err := UpdateSAMLAppConfigRequestToDomain(tc.appID, tc.projectID, tc.req)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
assert.Equal(t, tc.expectedResponse, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppSAMLConfigToPb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
metadata := samlMetadataGen(gofakeit.URL())
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
inputSAMLApp *query.SAMLApp
|
||||
|
||||
expectedPbApp app.ApplicationConfig
|
||||
}{
|
||||
{
|
||||
name: "valid conversion",
|
||||
inputSAMLApp: &query.SAMLApp{
|
||||
Metadata: metadata,
|
||||
LoginVersion: domain.LoginVersion2,
|
||||
LoginBaseURI: gu.Ptr("https://example.com"),
|
||||
},
|
||||
expectedPbApp: &app.Application_SamlConfig{
|
||||
SamlConfig: &app.SAMLConfig{
|
||||
Metadata: &app.SAMLConfig_MetadataXml{
|
||||
MetadataXml: metadata,
|
||||
},
|
||||
LoginVersion: &app.LoginVersion{
|
||||
Version: &app.LoginVersion_LoginV2{
|
||||
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://example.com")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil saml app",
|
||||
inputSAMLApp: nil,
|
||||
expectedPbApp: &app.Application_SamlConfig{
|
||||
SamlConfig: &app.SAMLConfig{
|
||||
Metadata: &app.SAMLConfig_MetadataXml{},
|
||||
LoginVersion: &app.LoginVersion{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When
|
||||
got := appSAMLConfigToPb(tc.inputSAMLApp)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, tc.expectedPbApp, got)
|
||||
})
|
||||
}
|
||||
}
|
1446
internal/api/grpc/app/v2beta/integration_test/app_test.go
Normal file
1446
internal/api/grpc/app/v2beta/integration_test/app_test.go
Normal file
File diff suppressed because it is too large
Load Diff
575
internal/api/grpc/app/v2beta/integration_test/query_test.go
Normal file
575
internal/api/grpc/app/v2beta/integration_test/query_test.go
Normal file
@@ -0,0 +1,575 @@
|
||||
//go:build integration
|
||||
|
||||
package instance_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
|
||||
)
|
||||
|
||||
func TestGetApplication(t *testing.T) {
|
||||
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
|
||||
|
||||
apiAppName := gofakeit.AppName()
|
||||
createdApiApp, errAPIAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
|
||||
ProjectId: p.GetId(),
|
||||
Name: apiAppName,
|
||||
CreationRequestType: &app.CreateApplicationRequest_ApiRequest{
|
||||
ApiRequest: &app.CreateAPIApplicationRequest{
|
||||
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Nil(t, errAPIAppCreation)
|
||||
|
||||
samlAppName := gofakeit.AppName()
|
||||
createdSAMLApp, errSAMLAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
|
||||
ProjectId: p.GetId(),
|
||||
Name: samlAppName,
|
||||
CreationRequestType: &app.CreateApplicationRequest_SamlRequest{
|
||||
SamlRequest: &app.CreateSAMLApplicationRequest{
|
||||
LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}},
|
||||
Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{MetadataXml: samlMetadataGen(gofakeit.URL())},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Nil(t, errSAMLAppCreation)
|
||||
|
||||
oidcAppName := gofakeit.AppName()
|
||||
createdOIDCApp, errOIDCAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
|
||||
ProjectId: p.GetId(),
|
||||
Name: oidcAppName,
|
||||
CreationRequestType: &app.CreateApplicationRequest_OidcRequest{
|
||||
OidcRequest: &app.CreateOIDCApplicationRequest{
|
||||
RedirectUris: []string{"http://example.com"},
|
||||
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
|
||||
GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
|
||||
AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
|
||||
AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
|
||||
PostLogoutRedirectUris: []string{"http://example.com/home"},
|
||||
Version: app.OIDCVersion_OIDC_VERSION_1_0,
|
||||
AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
|
||||
BackChannelLogoutUri: "http://example.com/logout",
|
||||
LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: &baseURI}}},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Nil(t, errOIDCAppCreation)
|
||||
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
inputRequest *app.GetApplicationRequest
|
||||
inputCtx context.Context
|
||||
|
||||
expectedErrorType codes.Code
|
||||
expectedAppName string
|
||||
expectedAppID string
|
||||
expectedApplicationType string
|
||||
}{
|
||||
{
|
||||
testName: "when unknown app ID should return not found error",
|
||||
inputCtx: IAMOwnerCtx,
|
||||
inputRequest: &app.GetApplicationRequest{
|
||||
Id: gofakeit.Sentence(2),
|
||||
},
|
||||
|
||||
expectedErrorType: codes.NotFound,
|
||||
},
|
||||
{
|
||||
testName: "when user has no permission should return membership not found error",
|
||||
inputCtx: NoPermissionCtx,
|
||||
inputRequest: &app.GetApplicationRequest{
|
||||
Id: createdApiApp.GetAppId(),
|
||||
},
|
||||
|
||||
expectedErrorType: codes.NotFound,
|
||||
},
|
||||
{
|
||||
testName: "when providing API app ID should return valid API app result",
|
||||
inputCtx: projectOwnerCtx,
|
||||
inputRequest: &app.GetApplicationRequest{
|
||||
Id: createdApiApp.GetAppId(),
|
||||
},
|
||||
|
||||
expectedAppName: apiAppName,
|
||||
expectedAppID: createdApiApp.GetAppId(),
|
||||
expectedApplicationType: fmt.Sprintf("%T", &app.Application_ApiConfig{}),
|
||||
},
|
||||
{
|
||||
testName: "when providing SAML app ID should return valid SAML app result",
|
||||
inputCtx: IAMOwnerCtx,
|
||||
inputRequest: &app.GetApplicationRequest{
|
||||
Id: createdSAMLApp.GetAppId(),
|
||||
},
|
||||
|
||||
expectedAppName: samlAppName,
|
||||
expectedAppID: createdSAMLApp.GetAppId(),
|
||||
expectedApplicationType: fmt.Sprintf("%T", &app.Application_SamlConfig{}),
|
||||
},
|
||||
{
|
||||
testName: "when providing OIDC app ID should return valid OIDC app result",
|
||||
inputCtx: IAMOwnerCtx,
|
||||
inputRequest: &app.GetApplicationRequest{
|
||||
Id: createdOIDCApp.GetAppId(),
|
||||
},
|
||||
|
||||
expectedAppName: oidcAppName,
|
||||
expectedAppID: createdOIDCApp.GetAppId(),
|
||||
expectedApplicationType: fmt.Sprintf("%T", &app.Application_OidcConfig{}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 30*time.Second)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
// When
|
||||
res, err := instance.Client.AppV2Beta.GetApplication(tc.inputCtx, tc.inputRequest)
|
||||
|
||||
// Then
|
||||
require.Equal(t, tc.expectedErrorType, status.Code(err))
|
||||
if tc.expectedErrorType == codes.OK {
|
||||
|
||||
assert.Equal(t, tc.expectedAppID, res.GetApp().GetId())
|
||||
assert.Equal(t, tc.expectedAppName, res.GetApp().GetName())
|
||||
assert.NotZero(t, res.GetApp().GetCreationDate())
|
||||
assert.NotZero(t, res.GetApp().GetChangeDate())
|
||||
|
||||
appType := fmt.Sprintf("%T", res.GetApp().GetConfig())
|
||||
assert.Equal(t, tc.expectedApplicationType, appType)
|
||||
}
|
||||
}, retryDuration, tick)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListApplications(t *testing.T) {
|
||||
p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx)
|
||||
|
||||
t.Parallel()
|
||||
|
||||
createdApiApp, apiAppName := createAPIAppWithName(t, p.GetId())
|
||||
|
||||
createdDeactivatedApiApp, deactivatedApiAppName := createAPIAppWithName(t, p.GetId())
|
||||
deactivateApp(t, createdDeactivatedApiApp, p.GetId())
|
||||
|
||||
_, createdSAMLApp, samlAppName := createSAMLAppWithName(t, gofakeit.URL(), p.GetId())
|
||||
|
||||
createdOIDCApp, oidcAppName := createOIDCAppWithName(t, gofakeit.URL(), p.GetId())
|
||||
|
||||
type appWithName struct {
|
||||
app *app.CreateApplicationResponse
|
||||
name string
|
||||
}
|
||||
|
||||
// Sorting
|
||||
appsSortedByName := []appWithName{
|
||||
{name: apiAppName, app: createdApiApp},
|
||||
{name: deactivatedApiAppName, app: createdDeactivatedApiApp},
|
||||
{name: samlAppName, app: createdSAMLApp},
|
||||
{name: oidcAppName, app: createdOIDCApp},
|
||||
}
|
||||
slices.SortFunc(appsSortedByName, func(a, b appWithName) int {
|
||||
if a.name < b.name {
|
||||
return -1
|
||||
}
|
||||
if a.name > b.name {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
appsSortedByID := []appWithName{
|
||||
{name: apiAppName, app: createdApiApp},
|
||||
{name: deactivatedApiAppName, app: createdDeactivatedApiApp},
|
||||
{name: samlAppName, app: createdSAMLApp},
|
||||
{name: oidcAppName, app: createdOIDCApp},
|
||||
}
|
||||
slices.SortFunc(appsSortedByID, func(a, b appWithName) int {
|
||||
if a.app.GetAppId() < b.app.GetAppId() {
|
||||
return -1
|
||||
}
|
||||
if a.app.GetAppId() > b.app.GetAppId() {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
appsSortedByCreationDate := []appWithName{
|
||||
{name: apiAppName, app: createdApiApp},
|
||||
{name: deactivatedApiAppName, app: createdDeactivatedApiApp},
|
||||
{name: samlAppName, app: createdSAMLApp},
|
||||
{name: oidcAppName, app: createdOIDCApp},
|
||||
}
|
||||
slices.SortFunc(appsSortedByCreationDate, func(a, b appWithName) int {
|
||||
aCreationDate := a.app.GetCreationDate().AsTime()
|
||||
bCreationDate := b.app.GetCreationDate().AsTime()
|
||||
|
||||
if aCreationDate.Before(bCreationDate) {
|
||||
return -1
|
||||
}
|
||||
if bCreationDate.Before(aCreationDate) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
inputRequest *app.ListApplicationsRequest
|
||||
inputCtx context.Context
|
||||
|
||||
expectedOrderedList []appWithName
|
||||
expectedOrderedKeys func(keys []appWithName) any
|
||||
actualOrderedKeys func(keys []*app.Application) any
|
||||
}{
|
||||
{
|
||||
testName: "when no apps found should return empty list",
|
||||
inputCtx: IAMOwnerCtx,
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: "another-id",
|
||||
},
|
||||
|
||||
expectedOrderedList: []appWithName{},
|
||||
expectedOrderedKeys: func(keys []appWithName) any { return keys },
|
||||
actualOrderedKeys: func(keys []*app.Application) any { return keys },
|
||||
},
|
||||
{
|
||||
testName: "when user has no read permission should return empty set",
|
||||
inputCtx: NoPermissionCtx,
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
},
|
||||
|
||||
expectedOrderedList: []appWithName{},
|
||||
expectedOrderedKeys: func(keys []appWithName) any { return keys },
|
||||
actualOrderedKeys: func(keys []*app.Application) any { return keys },
|
||||
},
|
||||
{
|
||||
testName: "when sorting by name should return apps sorted by name in descending order",
|
||||
inputCtx: IAMOwnerCtx,
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
SortingColumn: app.AppSorting_APP_SORT_BY_NAME,
|
||||
Pagination: &filter.PaginationRequest{Asc: true},
|
||||
},
|
||||
|
||||
expectedOrderedList: appsSortedByName,
|
||||
expectedOrderedKeys: func(apps []appWithName) any {
|
||||
names := make([]string, len(apps))
|
||||
for i, a := range apps {
|
||||
names[i] = a.name
|
||||
}
|
||||
|
||||
return names
|
||||
},
|
||||
actualOrderedKeys: func(apps []*app.Application) any {
|
||||
names := make([]string, len(apps))
|
||||
for i, a := range apps {
|
||||
names[i] = a.GetName()
|
||||
}
|
||||
|
||||
return names
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
testName: "when user is project owner should return apps sorted by name in ascending order",
|
||||
inputCtx: projectOwnerCtx,
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
SortingColumn: app.AppSorting_APP_SORT_BY_NAME,
|
||||
Pagination: &filter.PaginationRequest{Asc: true},
|
||||
},
|
||||
|
||||
expectedOrderedList: appsSortedByName,
|
||||
expectedOrderedKeys: func(apps []appWithName) any {
|
||||
names := make([]string, len(apps))
|
||||
for i, a := range apps {
|
||||
names[i] = a.name
|
||||
}
|
||||
|
||||
return names
|
||||
},
|
||||
actualOrderedKeys: func(apps []*app.Application) any {
|
||||
names := make([]string, len(apps))
|
||||
for i, a := range apps {
|
||||
names[i] = a.GetName()
|
||||
}
|
||||
|
||||
return names
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
testName: "when sorting by id should return apps sorted by id in descending order",
|
||||
inputCtx: IAMOwnerCtx,
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
SortingColumn: app.AppSorting_APP_SORT_BY_ID,
|
||||
Pagination: &filter.PaginationRequest{Asc: true},
|
||||
},
|
||||
expectedOrderedList: appsSortedByID,
|
||||
expectedOrderedKeys: func(apps []appWithName) any {
|
||||
ids := make([]string, len(apps))
|
||||
for i, a := range apps {
|
||||
ids[i] = a.app.GetAppId()
|
||||
}
|
||||
|
||||
return ids
|
||||
},
|
||||
actualOrderedKeys: func(apps []*app.Application) any {
|
||||
ids := make([]string, len(apps))
|
||||
for i, a := range apps {
|
||||
ids[i] = a.GetId()
|
||||
}
|
||||
|
||||
return ids
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "when sorting by creation date should return apps sorted by creation date in descending order",
|
||||
inputCtx: IAMOwnerCtx,
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
SortingColumn: app.AppSorting_APP_SORT_BY_CREATION_DATE,
|
||||
Pagination: &filter.PaginationRequest{Asc: true},
|
||||
},
|
||||
expectedOrderedList: appsSortedByCreationDate,
|
||||
expectedOrderedKeys: func(apps []appWithName) any {
|
||||
creationDates := make([]time.Time, len(apps))
|
||||
for i, a := range apps {
|
||||
creationDates[i] = a.app.GetCreationDate().AsTime()
|
||||
}
|
||||
|
||||
return creationDates
|
||||
},
|
||||
actualOrderedKeys: func(apps []*app.Application) any {
|
||||
creationDates := make([]time.Time, len(apps))
|
||||
for i, a := range apps {
|
||||
creationDates[i] = a.GetCreationDate().AsTime()
|
||||
}
|
||||
|
||||
return creationDates
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "when filtering by active apps should return active apps only",
|
||||
inputCtx: IAMOwnerCtx,
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
Pagination: &filter.PaginationRequest{Asc: true},
|
||||
Filters: []*app.ApplicationSearchFilter{
|
||||
{Filter: &app.ApplicationSearchFilter_StateFilter{StateFilter: app.AppState_APP_STATE_ACTIVE}},
|
||||
},
|
||||
},
|
||||
expectedOrderedList: slices.DeleteFunc(
|
||||
slices.Clone(appsSortedByID),
|
||||
func(a appWithName) bool { return a.name == deactivatedApiAppName },
|
||||
),
|
||||
expectedOrderedKeys: func(apps []appWithName) any {
|
||||
creationDates := make([]time.Time, len(apps))
|
||||
for i, a := range apps {
|
||||
creationDates[i] = a.app.GetCreationDate().AsTime()
|
||||
}
|
||||
|
||||
return creationDates
|
||||
},
|
||||
actualOrderedKeys: func(apps []*app.Application) any {
|
||||
creationDates := make([]time.Time, len(apps))
|
||||
for i, a := range apps {
|
||||
creationDates[i] = a.GetCreationDate().AsTime()
|
||||
}
|
||||
|
||||
return creationDates
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "when filtering by app type should return apps of matching type only",
|
||||
inputCtx: IAMOwnerCtx,
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
Pagination: &filter.PaginationRequest{Asc: true},
|
||||
Filters: []*app.ApplicationSearchFilter{
|
||||
{Filter: &app.ApplicationSearchFilter_OidcAppOnly{}},
|
||||
},
|
||||
},
|
||||
expectedOrderedList: slices.DeleteFunc(
|
||||
slices.Clone(appsSortedByID),
|
||||
func(a appWithName) bool { return a.name != oidcAppName },
|
||||
),
|
||||
expectedOrderedKeys: func(apps []appWithName) any {
|
||||
creationDates := make([]time.Time, len(apps))
|
||||
for i, a := range apps {
|
||||
creationDates[i] = a.app.GetCreationDate().AsTime()
|
||||
}
|
||||
|
||||
return creationDates
|
||||
},
|
||||
actualOrderedKeys: func(apps []*app.Application) any {
|
||||
creationDates := make([]time.Time, len(apps))
|
||||
for i, a := range apps {
|
||||
creationDates[i] = a.GetCreationDate().AsTime()
|
||||
}
|
||||
|
||||
return creationDates
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 30*time.Second)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
// When
|
||||
res, err := instance.Client.AppV2Beta.ListApplications(tc.inputCtx, tc.inputRequest)
|
||||
|
||||
// Then
|
||||
require.Equal(ttt, codes.OK, status.Code(err))
|
||||
|
||||
if err == nil {
|
||||
assert.Len(ttt, res.GetApplications(), len(tc.expectedOrderedList))
|
||||
actualOrderedKeys := tc.actualOrderedKeys(res.GetApplications())
|
||||
expectedOrderedKeys := tc.expectedOrderedKeys(tc.expectedOrderedList)
|
||||
assert.ElementsMatch(ttt, expectedOrderedKeys, actualOrderedKeys)
|
||||
}
|
||||
}, retryDuration, tick)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListApplications_WithPermissionV2(t *testing.T) {
|
||||
ensureFeaturePermissionV2Enabled(t, instancePermissionV2)
|
||||
iamOwnerCtx := instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeIAMOwner)
|
||||
p, projectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx)
|
||||
_, otherProjectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx)
|
||||
|
||||
appName1, appName2, appName3 := gofakeit.AppName(), gofakeit.AppName(), gofakeit.AppName()
|
||||
reqForAPIAppCreation := &app.CreateApplicationRequest_ApiRequest{
|
||||
ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
|
||||
}
|
||||
|
||||
app1, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{
|
||||
ProjectId: p.GetId(),
|
||||
Name: appName1,
|
||||
CreationRequestType: reqForAPIAppCreation,
|
||||
})
|
||||
require.Nil(t, appAPIConfigChangeErr)
|
||||
|
||||
app2, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{
|
||||
ProjectId: p.GetId(),
|
||||
Name: appName2,
|
||||
CreationRequestType: reqForAPIAppCreation,
|
||||
})
|
||||
require.Nil(t, appAPIConfigChangeErr)
|
||||
|
||||
app3, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{
|
||||
ProjectId: p.GetId(),
|
||||
Name: appName3,
|
||||
CreationRequestType: reqForAPIAppCreation,
|
||||
})
|
||||
require.Nil(t, appAPIConfigChangeErr)
|
||||
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
inputRequest *app.ListApplicationsRequest
|
||||
inputCtx context.Context
|
||||
|
||||
expectedCode codes.Code
|
||||
expectedAppIDs []string
|
||||
}{
|
||||
{
|
||||
testName: "when user has no read permission should return empty set",
|
||||
inputCtx: instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeNoPermission),
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
},
|
||||
|
||||
expectedAppIDs: []string{},
|
||||
},
|
||||
{
|
||||
testName: "when projectOwner should return full app list",
|
||||
inputCtx: projectOwnerCtx,
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
},
|
||||
|
||||
expectedCode: codes.OK,
|
||||
expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()},
|
||||
},
|
||||
{
|
||||
testName: "when orgOwner should return full app list",
|
||||
inputCtx: instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
},
|
||||
|
||||
expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()},
|
||||
},
|
||||
{
|
||||
testName: "when iamOwner user should return full app list",
|
||||
inputCtx: iamOwnerCtx,
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
},
|
||||
|
||||
expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()},
|
||||
},
|
||||
{
|
||||
testName: "when other projectOwner user should return empty list",
|
||||
inputCtx: otherProjectOwnerCtx,
|
||||
inputRequest: &app.ListApplicationsRequest{
|
||||
ProjectId: p.GetId(),
|
||||
},
|
||||
|
||||
expectedAppIDs: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 5*time.Second)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
// When
|
||||
res, err := instancePermissionV2.Client.AppV2Beta.ListApplications(tc.inputCtx, tc.inputRequest)
|
||||
|
||||
// Then
|
||||
require.Equal(ttt, tc.expectedCode, status.Code(err))
|
||||
|
||||
if err == nil {
|
||||
require.Len(ttt, res.GetApplications(), len(tc.expectedAppIDs))
|
||||
|
||||
resAppIDs := []string{}
|
||||
for _, a := range res.GetApplications() {
|
||||
resAppIDs = append(resAppIDs, a.GetId())
|
||||
}
|
||||
|
||||
assert.ElementsMatch(ttt, tc.expectedAppIDs, resAppIDs)
|
||||
}
|
||||
}, retryDuration, tick)
|
||||
})
|
||||
}
|
||||
}
|
205
internal/api/grpc/app/v2beta/integration_test/server_test.go
Normal file
205
internal/api/grpc/app/v2beta/integration_test/server_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
//go:build integration
|
||||
|
||||
package instance_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
project_v2beta "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
|
||||
)
|
||||
|
||||
var (
|
||||
NoPermissionCtx context.Context
|
||||
LoginUserCtx context.Context
|
||||
OrgOwnerCtx context.Context
|
||||
IAMOwnerCtx context.Context
|
||||
|
||||
instance *integration.Instance
|
||||
instancePermissionV2 *integration.Instance
|
||||
|
||||
baseURI = "http://example.com"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
instance = integration.NewInstance(ctx)
|
||||
instancePermissionV2 = integration.NewInstance(ctx)
|
||||
|
||||
IAMOwnerCtx = instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
|
||||
|
||||
LoginUserCtx = instance.WithAuthorization(ctx, integration.UserTypeLogin)
|
||||
OrgOwnerCtx = instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
|
||||
NoPermissionCtx = instance.WithAuthorization(ctx, integration.UserTypeNoPermission)
|
||||
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func getProjectAndProjectContext(t *testing.T, inst *integration.Instance, ctx context.Context) (*project_v2beta.CreateProjectResponse, context.Context) {
|
||||
project := inst.CreateProject(ctx, t, inst.DefaultOrg.GetId(), gofakeit.Name(), false, false)
|
||||
userResp := inst.CreateMachineUser(ctx)
|
||||
patResp := inst.CreatePersonalAccessToken(ctx, userResp.GetUserId())
|
||||
inst.CreateProjectMembership(t, ctx, project.GetId(), userResp.GetUserId())
|
||||
projectOwnerCtx := integration.WithAuthorizationToken(context.Background(), patResp.Token)
|
||||
|
||||
return project, projectOwnerCtx
|
||||
}
|
||||
|
||||
func samlMetadataGen(entityID string) []byte {
|
||||
str := fmt.Sprintf(`<?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, *app.CreateApplicationResponse, string) {
|
||||
samlMetas := samlMetadataGen(gofakeit.URL())
|
||||
appName := gofakeit.AppName()
|
||||
|
||||
appForSAMLConfigChange, appSAMLConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
|
||||
ProjectId: projectID,
|
||||
Name: appName,
|
||||
CreationRequestType: &app.CreateApplicationRequest_SamlRequest{
|
||||
SamlRequest: &app.CreateSAMLApplicationRequest{
|
||||
Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
|
||||
MetadataXml: samlMetas,
|
||||
},
|
||||
LoginVersion: &app.LoginVersion{
|
||||
Version: &app.LoginVersion_LoginV2{
|
||||
LoginV2: &app.LoginV2{
|
||||
BaseUri: &baseURI,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Nil(t, appSAMLConfigChangeErr)
|
||||
|
||||
return samlMetas, appForSAMLConfigChange, appName
|
||||
}
|
||||
|
||||
func createSAMLApp(t *testing.T, baseURI, projectID string) ([]byte, *app.CreateApplicationResponse) {
|
||||
metas, app, _ := createSAMLAppWithName(t, baseURI, projectID)
|
||||
return metas, app
|
||||
}
|
||||
|
||||
func createOIDCAppWithName(t *testing.T, baseURI, projectID string) (*app.CreateApplicationResponse, string) {
|
||||
appName := gofakeit.AppName()
|
||||
|
||||
appForOIDCConfigChange, appOIDCConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
|
||||
ProjectId: projectID,
|
||||
Name: appName,
|
||||
CreationRequestType: &app.CreateApplicationRequest_OidcRequest{
|
||||
OidcRequest: &app.CreateOIDCApplicationRequest{
|
||||
RedirectUris: []string{"http://example.com"},
|
||||
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
|
||||
GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
|
||||
AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
|
||||
AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
|
||||
PostLogoutRedirectUris: []string{"http://example.com/home"},
|
||||
Version: app.OIDCVersion_OIDC_VERSION_1_0,
|
||||
AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
|
||||
BackChannelLogoutUri: "http://example.com/logout",
|
||||
LoginVersion: &app.LoginVersion{
|
||||
Version: &app.LoginVersion_LoginV2{
|
||||
LoginV2: &app.LoginV2{
|
||||
BaseUri: &baseURI,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Nil(t, appOIDCConfigChangeErr)
|
||||
|
||||
return appForOIDCConfigChange, appName
|
||||
}
|
||||
|
||||
func createOIDCApp(t *testing.T, baseURI, projctID string) *app.CreateApplicationResponse {
|
||||
app, _ := createOIDCAppWithName(t, baseURI, projctID)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func createAPIAppWithName(t *testing.T, projectID string) (*app.CreateApplicationResponse, string) {
|
||||
appName := gofakeit.AppName()
|
||||
|
||||
reqForAPIAppCreation := &app.CreateApplicationRequest_ApiRequest{
|
||||
ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT},
|
||||
}
|
||||
|
||||
appForAPIConfigChange, appAPIConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{
|
||||
ProjectId: projectID,
|
||||
Name: appName,
|
||||
CreationRequestType: reqForAPIAppCreation,
|
||||
})
|
||||
require.Nil(t, appAPIConfigChangeErr)
|
||||
|
||||
return appForAPIConfigChange, appName
|
||||
}
|
||||
|
||||
func createAPIApp(t *testing.T, projectID string) *app.CreateApplicationResponse {
|
||||
res, _ := createAPIAppWithName(t, projectID)
|
||||
return res
|
||||
}
|
||||
|
||||
func deactivateApp(t *testing.T, appToDeactivate *app.CreateApplicationResponse, projectID string) {
|
||||
_, appDeactivateErr := instance.Client.AppV2Beta.DeactivateApplication(IAMOwnerCtx, &app.DeactivateApplicationRequest{
|
||||
ProjectId: projectID,
|
||||
Id: appToDeactivate.GetAppId(),
|
||||
})
|
||||
require.Nil(t, appDeactivateErr)
|
||||
}
|
||||
|
||||
func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) {
|
||||
ctx := instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner)
|
||||
f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{
|
||||
Inheritance: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
if f.PermissionCheckV2.GetEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{
|
||||
PermissionCheckV2: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute)
|
||||
require.EventuallyWithT(t, func(tt *assert.CollectT) {
|
||||
f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{Inheritance: true})
|
||||
require.NoError(tt, err)
|
||||
assert.True(tt, f.PermissionCheckV2.GetEnabled())
|
||||
}, retryDuration, tick, "timed out waiting for ensuring instance feature")
|
||||
}
|
37
internal/api/grpc/app/v2beta/query.go
Normal file
37
internal/api/grpc/app/v2beta/query.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/app/v2beta/convert"
|
||||
filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) GetApplication(ctx context.Context, req *app.GetApplicationRequest) (*app.GetApplicationResponse, error) {
|
||||
res, err := s.query.AppByIDWithPermission(ctx, req.GetId(), false, s.checkPermission)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &app.GetApplicationResponse{
|
||||
App: convert.AppToPb(res),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListApplications(ctx context.Context, req *app.ListApplicationsRequest) (*app.ListApplicationsResponse, error) {
|
||||
queries, err := convert.ListApplicationsRequestToModel(s.systemDefaults, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := s.query.SearchApps(ctx, queries, s.checkPermission)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &app.ListApplicationsResponse{
|
||||
Applications: convert.AppsToPb(res.Apps),
|
||||
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, res.SearchResponse),
|
||||
}, nil
|
||||
}
|
57
internal/api/grpc/app/v2beta/server.go
Normal file
57
internal/api/grpc/app/v2beta/server.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
|
||||
)
|
||||
|
||||
var _ app.AppServiceServer = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
app.UnimplementedAppServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
systemDefaults systemdefaults.SystemDefaults
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
checkPermission domain.PermissionCheck,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
checkPermission: checkPermission,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
|
||||
app.RegisterAppServiceServer(grpcServer, s)
|
||||
}
|
||||
|
||||
func (s *Server) AppName() string {
|
||||
return app.AppService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return app.AppService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
return app.AppService_AuthMethods
|
||||
}
|
||||
|
||||
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
|
||||
return app.RegisterAppServiceHandler
|
||||
}
|
@@ -48,3 +48,26 @@ func QueryToPaginationPb(request query.SearchRequest, response query.SearchRespo
|
||||
TotalResult: response.Count,
|
||||
}
|
||||
}
|
||||
|
||||
func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison {
|
||||
switch method {
|
||||
case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS:
|
||||
return query.TextEquals
|
||||
case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE:
|
||||
return query.TextEqualsIgnoreCase
|
||||
case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH:
|
||||
return query.TextStartsWith
|
||||
case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE:
|
||||
return query.TextStartsWithIgnoreCase
|
||||
case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS:
|
||||
return query.TextContains
|
||||
case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE:
|
||||
return query.TextContainsIgnoreCase
|
||||
case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH:
|
||||
return query.TextEndsWith
|
||||
case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE:
|
||||
return query.TextEndsWithIgnoreCase
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ func (s *Server) ListApps(ctx context.Context, req *mgmt_pb.ListAppsRequest) (*m
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apps, err := s.query.SearchApps(ctx, queries, false)
|
||||
apps, err := s.query.SearchApps(ctx, queries, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -125,7 +125,7 @@ func (s *Server) AddAPIApp(ctx context.Context, req *mgmt_pb.AddAPIAppRequest) (
|
||||
}
|
||||
|
||||
func (s *Server) UpdateApp(ctx context.Context, req *mgmt_pb.UpdateAppRequest) (*mgmt_pb.UpdateAppResponse, error) {
|
||||
details, err := s.command.ChangeApplication(ctx, req.ProjectId, UpdateAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
|
||||
details, err := s.command.UpdateApplicationName(ctx, req.ProjectId, UpdateAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func (s *Server) UpdateOIDCAppConfig(ctx context.Context, req *mgmt_pb.UpdateOID
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := s.command.ChangeOIDCApplication(ctx, oidcApp, authz.GetCtxData(ctx).OrgID)
|
||||
config, err := s.command.UpdateOIDCApplication(ctx, oidcApp, authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAM
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := s.command.ChangeSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID)
|
||||
config, err := s.command.UpdateSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAM
|
||||
}
|
||||
|
||||
func (s *Server) UpdateAPIAppConfig(ctx context.Context, req *mgmt_pb.UpdateAPIAppConfigRequest) (*mgmt_pb.UpdateAPIAppConfigResponse, error) {
|
||||
config, err := s.command.ChangeAPIApplication(ctx, UpdateAPIAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
|
||||
config, err := s.command.UpdateAPIApplication(ctx, UpdateAPIAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
authn_grpc "github.com/zitadel/zitadel/internal/api/grpc/authn"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||
@@ -46,24 +48,24 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) (*domain.OIDCApp,
|
||||
AggregateID: req.ProjectId,
|
||||
},
|
||||
AppName: req.Name,
|
||||
OIDCVersion: app_grpc.OIDCVersionToDomain(req.Version),
|
||||
OIDCVersion: gu.Ptr(app_grpc.OIDCVersionToDomain(req.Version)),
|
||||
RedirectUris: req.RedirectUris,
|
||||
ResponseTypes: app_grpc.OIDCResponseTypesToDomain(req.ResponseTypes),
|
||||
GrantTypes: app_grpc.OIDCGrantTypesToDomain(req.GrantTypes),
|
||||
ApplicationType: app_grpc.OIDCApplicationTypeToDomain(req.AppType),
|
||||
AuthMethodType: app_grpc.OIDCAuthMethodTypeToDomain(req.AuthMethodType),
|
||||
ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(req.AppType)),
|
||||
AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(req.AuthMethodType)),
|
||||
PostLogoutRedirectUris: req.PostLogoutRedirectUris,
|
||||
DevMode: req.DevMode,
|
||||
AccessTokenType: app_grpc.OIDCTokenTypeToDomain(req.AccessTokenType),
|
||||
AccessTokenRoleAssertion: req.AccessTokenRoleAssertion,
|
||||
IDTokenRoleAssertion: req.IdTokenRoleAssertion,
|
||||
IDTokenUserinfoAssertion: req.IdTokenUserinfoAssertion,
|
||||
ClockSkew: req.ClockSkew.AsDuration(),
|
||||
DevMode: gu.Ptr(req.GetDevMode()),
|
||||
AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(req.AccessTokenType)),
|
||||
AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()),
|
||||
IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()),
|
||||
IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()),
|
||||
ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()),
|
||||
AdditionalOrigins: req.AdditionalOrigins,
|
||||
SkipNativeAppSuccessPage: req.SkipNativeAppSuccessPage,
|
||||
BackChannelLogoutURI: req.GetBackChannelLogoutUri(),
|
||||
LoginVersion: loginVersion,
|
||||
LoginBaseURI: loginBaseURI,
|
||||
SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()),
|
||||
BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()),
|
||||
LoginVersion: gu.Ptr(loginVersion),
|
||||
LoginBaseURI: gu.Ptr(loginBaseURI),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -78,9 +80,9 @@ func AddSAMLAppRequestToDomain(req *mgmt_pb.AddSAMLAppRequest) (*domain.SAMLApp,
|
||||
},
|
||||
AppName: req.Name,
|
||||
Metadata: req.GetMetadataXml(),
|
||||
MetadataURL: req.GetMetadataUrl(),
|
||||
LoginVersion: loginVersion,
|
||||
LoginBaseURI: loginBaseURI,
|
||||
MetadataURL: gu.Ptr(req.GetMetadataUrl()),
|
||||
LoginVersion: gu.Ptr(loginVersion),
|
||||
LoginBaseURI: gu.Ptr(loginBaseURI),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -114,20 +116,20 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest)
|
||||
RedirectUris: app.RedirectUris,
|
||||
ResponseTypes: app_grpc.OIDCResponseTypesToDomain(app.ResponseTypes),
|
||||
GrantTypes: app_grpc.OIDCGrantTypesToDomain(app.GrantTypes),
|
||||
ApplicationType: app_grpc.OIDCApplicationTypeToDomain(app.AppType),
|
||||
AuthMethodType: app_grpc.OIDCAuthMethodTypeToDomain(app.AuthMethodType),
|
||||
ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(app.AppType)),
|
||||
AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(app.AuthMethodType)),
|
||||
PostLogoutRedirectUris: app.PostLogoutRedirectUris,
|
||||
DevMode: app.DevMode,
|
||||
AccessTokenType: app_grpc.OIDCTokenTypeToDomain(app.AccessTokenType),
|
||||
AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
|
||||
IDTokenRoleAssertion: app.IdTokenRoleAssertion,
|
||||
IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion,
|
||||
ClockSkew: app.ClockSkew.AsDuration(),
|
||||
DevMode: gu.Ptr(app.GetDevMode()),
|
||||
AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(app.AccessTokenType)),
|
||||
AccessTokenRoleAssertion: gu.Ptr(app.GetAccessTokenRoleAssertion()),
|
||||
IDTokenRoleAssertion: gu.Ptr(app.GetIdTokenRoleAssertion()),
|
||||
IDTokenUserinfoAssertion: gu.Ptr(app.GetIdTokenUserinfoAssertion()),
|
||||
ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()),
|
||||
AdditionalOrigins: app.AdditionalOrigins,
|
||||
SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage,
|
||||
BackChannelLogoutURI: app.BackChannelLogoutUri,
|
||||
LoginVersion: loginVersion,
|
||||
LoginBaseURI: loginBaseURI,
|
||||
SkipNativeAppSuccessPage: gu.Ptr(app.GetSkipNativeAppSuccessPage()),
|
||||
BackChannelLogoutURI: gu.Ptr(app.GetBackChannelLogoutUri()),
|
||||
LoginVersion: gu.Ptr(loginVersion),
|
||||
LoginBaseURI: gu.Ptr(loginBaseURI),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -142,9 +144,9 @@ func UpdateSAMLAppConfigRequestToDomain(app *mgmt_pb.UpdateSAMLAppConfigRequest)
|
||||
},
|
||||
AppID: app.AppId,
|
||||
Metadata: app.GetMetadataXml(),
|
||||
MetadataURL: app.GetMetadataUrl(),
|
||||
LoginVersion: loginVersion,
|
||||
LoginBaseURI: loginBaseURI,
|
||||
MetadataURL: gu.Ptr(app.GetMetadataUrl()),
|
||||
LoginVersion: gu.Ptr(loginVersion),
|
||||
LoginBaseURI: gu.Ptr(loginBaseURI),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user