mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:57:33 +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:
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")
|
||||
}
|
Reference in New Issue
Block a user