Files
zitadel/internal/api/grpc/instance/v2/integration_test/domain_test.go
T
7b9d2b7d17 feat: add administrator role permission table (#11817)
This update introduces a new table for managing administrator role
permissions, along with the necessary repository and projection logic to
handle adding and removing permissions. It also includes tests to ensure
the correct functionality of the permission management system.

closes https://github.com/zitadel/zitadel/issues/10219

---------

Co-authored-by: Marco Ardizzone <[email protected]>
2026-04-14 06:01:52 +00:00

461 lines
17 KiB
Go

//go:build integration
package instance_test
import (
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
"github.com/zitadel/zitadel/pkg/grpc/instance/v2"
)
func TestAddCustomDomain(t *testing.T) {
t.Parallel()
// Given
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
iamOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
relationalInst := integration.NewInstance(ctxWithSysAuthZ)
iamOwnerRelationalCtx := relationalInst.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, relationalInst, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
t.Cleanup(func() {
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
relationalInst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: relationalInst.ID()})
})
// ugly fix to wait until instances are projected in all read models.
time.Sleep(10 * time.Second)
type instanceAndCtx struct {
testType string
instance *integration.Instance
ctx context.Context
}
testedInstances := []instanceAndCtx{
{testType: "eventstore", instance: inst, ctx: iamOwnerCtx},
{testType: "relational", instance: relationalInst, ctx: iamOwnerRelationalCtx},
}
for _, instAndCtx := range testedInstances {
tt := []struct {
testName string
inputContext context.Context
inputRequest *instance.AddCustomDomainRequest
expectedErrorMsg string
expectedErrorCode codes.Code
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: instAndCtx.instance.ID(),
CustomDomain: integration.DomainName(),
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: instAndCtx.instance.ID(),
CustomDomain: integration.DomainName(),
},
inputContext: instAndCtx.ctx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when invalid domain should return invalid argument error",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: instAndCtx.instance.ID(),
CustomDomain: " ",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: instAndCtx.instance.ID(),
CustomDomain: " " + integration.DomainName(),
},
inputContext: ctxWithSysAuthZ,
},
}
for _, tc := range tt {
t.Run(fmt.Sprintf("%s - %s", instAndCtx.testType, tc.testName), func(t *testing.T) {
t.Cleanup(func() {
if tc.expectedErrorMsg == "" {
instAndCtx.instance.Client.InstanceV2.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{CustomDomain: strings.TrimSpace(tc.inputRequest.CustomDomain)})
}
})
// Test
res, err := instAndCtx.instance.Client.InstanceV2.AddCustomDomain(tc.inputContext, tc.inputRequest)
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Contains(t, status.Convert(err).Message(), tc.expectedErrorMsg)
if tc.expectedErrorMsg == "" {
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetCreationDate())
}
})
}
}
}
func TestRemoveCustomDomain(t *testing.T) {
t.Parallel()
// Given
ctx, cancel := context.WithTimeout(t.Context(), 15*time.Minute)
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
instES := integration.NewInstance(ctxWithSysAuthZ)
iamOwnerCtxES := instES.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner)
instRelational := integration.NewInstance(ctxWithSysAuthZ)
iamOwnerCtxRelational := instRelational.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner)
integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, instRelational, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
customDomainES := integration.DomainName()
customDomainRelational := integration.DomainName()
_, err := instES.Client.InstanceV2.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instES.ID(), CustomDomain: customDomainES})
require.Nil(t, err)
_, err = instRelational.Client.InstanceV2.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instRelational.ID(), CustomDomain: customDomainRelational})
require.Nil(t, err)
t.Cleanup(func() {
instES.Client.InstanceV2.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instES.ID(), CustomDomain: customDomainES})
instES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instRelational.ID(), CustomDomain: customDomainRelational})
instRelational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
type instanceAndCtx struct {
testType string
instance *integration.Instance
ctx context.Context
customDomain string
}
testedInstances := []instanceAndCtx{
{testType: "eventstore", instance: instES, ctx: iamOwnerCtxES, customDomain: customDomainES},
{testType: "relational", instance: instRelational, ctx: iamOwnerCtxRelational, customDomain: customDomainRelational},
}
for _, instAndCtx := range testedInstances {
tt := []struct {
testName string
inputContext context.Context
inputRequest *instance.RemoveCustomDomainRequest
expectedErrorMsg string
expectedErrorCode codes.Code
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.RemoveCustomDomainRequest{
InstanceId: instAndCtx.instance.ID(),
CustomDomain: "custom1",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.RemoveCustomDomainRequest{
InstanceId: instAndCtx.instance.ID(),
CustomDomain: "custom1",
},
inputContext: instAndCtx.ctx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when invalid domain should return invalid argument error",
inputRequest: &instance.RemoveCustomDomainRequest{
InstanceId: instAndCtx.instance.ID(),
CustomDomain: " ",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.RemoveCustomDomainRequest{
InstanceId: instAndCtx.instance.ID(),
CustomDomain: " " + instAndCtx.customDomain,
},
inputContext: ctxWithSysAuthZ,
},
}
for _, tc := range tt {
t.Run(fmt.Sprintf("%s - %s", instAndCtx.testType, tc.testName), func(t *testing.T) {
// Test
res, err := instAndCtx.instance.Client.InstanceV2.RemoveCustomDomain(tc.inputContext, tc.inputRequest)
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Contains(t, status.Convert(err).Message(), tc.expectedErrorMsg)
if tc.expectedErrorMsg == "" {
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetDeletionDate())
}
})
}
}
}
func TestAddTrustedDomain(t *testing.T) {
t.Parallel()
// Given
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
// Eventstore objects
instES := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
d1ES := integration.DomainName()
// Relational objects
instRelational := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, instRelational, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
orgOwnerCtxRelational := instRelational.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
d1Relational := integration.DomainName()
t.Cleanup(func() {
instES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
type instAndCtx struct {
testType string
inst *integration.Instance
orgOwnerCtx context.Context
domain string
}
testedInstanceAndCtxs := []instAndCtx{
{testType: "eventstore", inst: instES, orgOwnerCtx: orgOwnerCtxES, domain: d1ES},
{testType: "relational", inst: instRelational, orgOwnerCtx: orgOwnerCtxRelational, domain: d1Relational},
}
for _, stateCase := range testedInstanceAndCtxs {
tt := []struct {
testName string
inputContext context.Context
inputRequest *instance.AddTrustedDomainRequest
expectedErrorMsg string
expectedErrorCode codes.Code
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
TrustedDomain: stateCase.domain,
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
// TODO(IAM-Marco): Fix this test for relational case when permission checks are in place (see https://github.com/zitadel/zitadel/issues/10917)
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
TrustedDomain: stateCase.domain,
},
inputContext: stateCase.orgOwnerCtx,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
testName: "when invalid domain should return invalid argument error",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
TrustedDomain: " ",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
TrustedDomain: " " + stateCase.domain,
},
inputContext: ctxWithSysAuthZ,
},
}
for _, tc := range tt {
// TODO(IAM-Marco): Fix this test for relational case when permission checks are in place (see https://github.com/zitadel/zitadel/issues/10917)
if tc.testName == "when unauthZ context should return unauthZ error" && stateCase.testType == "relational" {
continue
}
t.Run(fmt.Sprintf("%s - %s", stateCase.testType, tc.testName), func(t *testing.T) {
t.Cleanup(func() {
if tc.expectedErrorMsg == "" {
stateCase.inst.Client.InstanceV2.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{TrustedDomain: strings.TrimSpace(tc.inputRequest.TrustedDomain)})
}
})
// Test
res, err := stateCase.inst.Client.InstanceV2.AddTrustedDomain(tc.inputContext, tc.inputRequest)
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Contains(t, status.Convert(err).Message(), tc.expectedErrorMsg)
if tc.expectedErrorMsg == "" {
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetCreationDate())
}
})
}
}
}
func TestRemoveTrustedDomain(t *testing.T) {
t.Parallel()
// Given
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
// Eventstore objects
instES := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
trustedDomainES := integration.DomainName()
_, err := instES.Client.InstanceV2.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instES.ID(), TrustedDomain: trustedDomainES})
require.Nil(t, err)
// Relational objects
instRelational := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, instRelational, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
orgOwnerCtxRelational := instRelational.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
trustedDomainRelational := integration.DomainName()
_, err = instRelational.Client.InstanceV2.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instRelational.ID(), TrustedDomain: trustedDomainRelational})
require.Nil(t, err)
t.Cleanup(func() {
instES.Client.InstanceV2.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instES.ID(), TrustedDomain: trustedDomainES})
instES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instRelational.ID(), TrustedDomain: trustedDomainRelational})
instRelational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
type instAndCtx struct {
testType string
inst *integration.Instance
orgOwnerCtx context.Context
domain string
}
testedInstanceAndCtxs := []instAndCtx{
{testType: "eventstore", inst: instES, orgOwnerCtx: orgOwnerCtxES, domain: trustedDomainES},
{testType: "relational", inst: instRelational, orgOwnerCtx: orgOwnerCtxRelational, domain: trustedDomainRelational},
}
for _, stateCase := range testedInstanceAndCtxs {
tt := []struct {
testName string
inputContext context.Context
inputRequest *instance.RemoveTrustedDomainRequest
expectedErrorMsg string
expectedErrorCode codes.Code
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.RemoveTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
TrustedDomain: "trusted1",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
// TODO(IAM-Marco): Fix this test for relational case when permission checks are in place (see https://github.com/zitadel/zitadel/issues/10917) testName: "when unauthZ context should return unauthZ error",
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.RemoveTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
TrustedDomain: "trusted1",
},
inputContext: stateCase.orgOwnerCtx,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.RemoveTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
TrustedDomain: " " + stateCase.domain,
},
inputContext: ctxWithSysAuthZ,
},
}
for _, tc := range tt {
// TODO(IAM-Marco): Fix this test for relational case when permission checks are in place (see https://github.com/zitadel/zitadel/issues/10917)
if tc.testName == "when unauthZ context should return unauthZ error" && stateCase.testType == "relational" {
continue
}
t.Run(fmt.Sprintf("%s - %s", stateCase.testType, tc.testName), func(t *testing.T) {
// Test
res, err := stateCase.inst.Client.InstanceV2.RemoveTrustedDomain(tc.inputContext, tc.inputRequest)
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message())
if tc.expectedErrorMsg == "" {
require.NotNil(t, res)
require.NotEmpty(t, res.GetDeletionDate())
}
})
}
}
}