feat: instance domain APIs with relational tables (#11051)

# Which Problems Are Solved

This PR implements the endpoints listed in
https://github.com/zitadel/zitadel/issues/10444 .

# How the Problems Are Solved

The implementation follows the same pattern as the Instance one. The
`Add` and `Remove` are implementing the `Commander` interface, while
`List` Domains endpoints are implementing the `Querier` interace.

The PR adds also some forgotten calls to the relational logic for the
Instance API. The commits are identifiable by the label `instance:`
prepended to them. This is the complete list: bafce01b0 , 6fe7257ab ,
e27bc9c34 , 8133ede73 , fde667953 , a3e1135ed , ee1a6f9c1

# Additional Context

- Closes #10444

---------

Co-authored-by: Elio Bischof <[email protected]>
Co-authored-by: Lebogang Phoshoko <[email protected]>
Co-authored-by: Wim Van Laer <[email protected]>
Co-authored-by: Max Peintner <[email protected]>
Co-authored-by: Gayathri Vijayan <[email protected]>
This commit is contained in:
Marco A.
2025-12-17 12:01:09 +01:00
committed by GitHub
co-authored by Elio Bischof Lebogang Phoshoko Wim Van Laer Max Peintner Gayathri Vijayan
parent d79bfe6eba
commit b339a3e696
49 changed files with 5670 additions and 1466 deletions
+32 -4
View File
@@ -6,11 +6,17 @@ import (
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
instancev2 "github.com/zitadel/zitadel/backend/v3/api/instance/v2"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/instance/v2"
)
func (s *Server) AddCustomDomain(ctx context.Context, req *connect.Request[instance.AddCustomDomainRequest]) (*connect.Response[instance.AddCustomDomainResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.AddCustomDomain(ctx, req)
}
// Adding a custom domain is currently only allowed with system permissions,
// so we directly check for them in the auth interceptor and do not check here again.
details, err := s.command.AddInstanceDomain(ctx, req.Msg.GetCustomDomain())
@@ -23,18 +29,32 @@ func (s *Server) AddCustomDomain(ctx context.Context, req *connect.Request[insta
}
func (s *Server) RemoveCustomDomain(ctx context.Context, req *connect.Request[instance.RemoveCustomDomainRequest]) (*connect.Response[instance.RemoveCustomDomainResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.RemoveCustomDomain(ctx, req)
}
// Removing a custom domain is currently only allowed with system permissions,
// so we directly check for them in the auth interceptor and do not check here again.
details, err := s.command.RemoveInstanceDomain(ctx, req.Msg.GetCustomDomain())
details, err := s.command.RemoveInstanceDomain(ctx, req.Msg.GetCustomDomain(), false)
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if details != nil {
deletionDate = timestamppb.New(details.EventDate)
}
return connect.NewResponse(&instance.RemoveCustomDomainResponse{
DeletionDate: timestamppb.New(details.EventDate),
DeletionDate: deletionDate,
}), nil
}
func (s *Server) AddTrustedDomain(ctx context.Context, req *connect.Request[instance.AddTrustedDomainRequest]) (*connect.Response[instance.AddTrustedDomainResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.AddTrustedDomain(ctx, req)
}
if err := s.checkPermission(ctx, domain.PermissionSystemInstanceWrite, domain.PermissionInstanceWrite); err != nil {
return nil, err
}
@@ -48,15 +68,23 @@ func (s *Server) AddTrustedDomain(ctx context.Context, req *connect.Request[inst
}
func (s *Server) RemoveTrustedDomain(ctx context.Context, req *connect.Request[instance.RemoveTrustedDomainRequest]) (*connect.Response[instance.RemoveTrustedDomainResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.RemoveTrustedDomain(ctx, req)
}
if err := s.checkPermission(ctx, domain.PermissionSystemInstanceWrite, domain.PermissionInstanceWrite); err != nil {
return nil, err
}
details, err := s.command.RemoveTrustedDomain(ctx, req.Msg.GetTrustedDomain())
details, err := s.command.RemoveTrustedDomain(ctx, req.Msg.GetTrustedDomain(), false)
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if details != nil {
deletionDate = timestamppb.New(details.EventDate)
}
return connect.NewResponse(&instance.RemoveTrustedDomainResponse{
DeletionDate: timestamppb.New(details.EventDate),
DeletionDate: deletionDate,
}), nil
}
+15 -1
View File
@@ -6,6 +6,8 @@ import (
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
instancev2 "github.com/zitadel/zitadel/backend/v3/api/instance/v2"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/instance/v2"
)
@@ -13,11 +15,19 @@ import (
func (s *Server) DeleteInstance(ctx context.Context, request *connect.Request[instance.DeleteInstanceRequest]) (*connect.Response[instance.DeleteInstanceResponse], error) {
// Deleting an instance is currently only allowed with system permissions,
// so we directly check for them in the auth interceptor and do not check here again.
obj, err := s.command.RemoveInstance(ctx, request.Msg.GetInstanceId())
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.DeleteInstance(ctx, request)
}
obj, err := s.command.RemoveInstance(ctx, request.Msg.GetInstanceId(), false)
if err != nil {
return nil, err
}
// obj is nil when the instance is not found, so we return an empty message.
if obj == nil {
return &connect.Response[instance.DeleteInstanceResponse]{}, nil
}
return connect.NewResponse(&instance.DeleteInstanceResponse{
DeletionDate: timestamppb.New(obj.EventDate),
}), nil
@@ -25,6 +35,10 @@ func (s *Server) DeleteInstance(ctx context.Context, request *connect.Request[in
}
func (s *Server) UpdateInstance(ctx context.Context, request *connect.Request[instance.UpdateInstanceRequest]) (*connect.Response[instance.UpdateInstanceResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.UpdateInstance(ctx, request)
}
if err := s.checkPermission(ctx, domain.PermissionSystemInstanceWrite, domain.PermissionInstanceWrite); err != nil {
return nil, err
}
@@ -4,16 +4,19 @@ 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"
)
@@ -29,77 +32,95 @@ func TestAddCustomDomain(t *testing.T) {
inst := integration.NewInstance(ctxWithSysAuthZ)
iamOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
t.Cleanup(func() {
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
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())
})
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: inst.ID(),
CustomDomain: integration.DomainName(),
t.Cleanup(func() {
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
relationalInst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: relationalInst.ID()})
})
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",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: inst.ID(),
CustomDomain: integration.DomainName(),
{
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)",
},
inputContext: iamOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when invalid domain should return invalid argument error",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: inst.ID(),
CustomDomain: " ",
{
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",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument (INST-28nlD)",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: inst.ID(),
CustomDomain: " " + integration.DomainName(),
{
testName: "when valid request should return successful response",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: instAndCtx.instance.ID(),
CustomDomain: " " + integration.DomainName(),
},
inputContext: ctxWithSysAuthZ,
},
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)
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Cleanup(func() {
if tc.expectedErrorMsg == "" {
inst.Client.InstanceV2.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{CustomDomain: strings.TrimSpace(tc.inputRequest.CustomDomain)})
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetCreationDate())
}
})
// Test
res, err := inst.Client.InstanceV2.AddCustomDomain(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 == "" {
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetCreationDate())
}
})
}
}
}
@@ -107,84 +128,108 @@ func TestRemoveCustomDomain(t *testing.T) {
t.Parallel()
// Given
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
ctx, cancel := context.WithTimeout(t.Context(), 15*time.Minute)
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
iamOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
instES := integration.NewInstance(ctxWithSysAuthZ)
iamOwnerCtxES := instES.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner)
customDomain := integration.DomainName()
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())
})
_, err := inst.Client.InstanceV2.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), CustomDomain: customDomain})
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() {
inst.Client.InstanceV2.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), CustomDomain: customDomain})
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
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()})
})
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: inst.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: inst.ID(),
CustomDomain: "custom1",
},
inputContext: iamOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when invalid domain should return invalid argument error",
inputRequest: &instance.RemoveCustomDomainRequest{
InstanceId: inst.ID(),
CustomDomain: " ",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument (INST-39nls)",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.RemoveCustomDomainRequest{
InstanceId: inst.ID(),
CustomDomain: " " + customDomain,
},
inputContext: ctxWithSysAuthZ,
},
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 _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Test
res, err := inst.Client.InstanceV2.RemoveCustomDomain(tc.inputContext, tc.inputRequest)
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,
},
}
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message())
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)
if tc.expectedErrorMsg == "" {
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetDeletionDate())
}
})
// 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())
}
})
}
}
}
@@ -196,80 +241,111 @@ func TestAddTrustedDomain(t *testing.T) {
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
// 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() {
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
instES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
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: inst.ID(),
TrustedDomain: "trusted1",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: inst.ID(),
TrustedDomain: "trusted1",
},
inputContext: orgOwnerCtx,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
testName: "when invalid domain should return invalid argument error",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: inst.ID(),
TrustedDomain: " ",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument (COMMA-Stk21)",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: inst.ID(),
TrustedDomain: " " + integration.DomainName(),
},
inputContext: ctxWithSysAuthZ,
},
type instAndCtx struct {
testType string
inst *integration.Instance
orgOwnerCtx context.Context
domain string
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Cleanup(func() {
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 == "" {
inst.Client.InstanceV2.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{TrustedDomain: strings.TrimSpace(tc.inputRequest.TrustedDomain)})
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetCreationDate())
}
})
// Test
res, err := inst.Client.InstanceV2.AddTrustedDomain(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 == "" {
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetCreationDate())
}
})
}
}
}
@@ -281,69 +357,101 @@ func TestRemoveTrustedDomain(t *testing.T) {
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
trustedDomain := integration.DomainName()
// 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)
_, err := inst.Client.InstanceV2.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), TrustedDomain: trustedDomain})
// 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() {
inst.Client.InstanceV2.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), TrustedDomain: trustedDomain})
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
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()})
})
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: inst.ID(),
TrustedDomain: "trusted1",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.RemoveTrustedDomainRequest{
InstanceId: inst.ID(),
TrustedDomain: "trusted1",
},
inputContext: orgOwnerCtx,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.RemoveTrustedDomainRequest{
InstanceId: inst.ID(),
TrustedDomain: " " + trustedDomain,
},
inputContext: ctxWithSysAuthZ,
},
type instAndCtx struct {
testType string
inst *integration.Instance
orgOwnerCtx context.Context
domain string
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Test
res, err := inst.Client.InstanceV2.RemoveTrustedDomain(tc.inputContext, tc.inputRequest)
testedInstanceAndCtxs := []instAndCtx{
{testType: "eventstore", inst: instES, orgOwnerCtx: orgOwnerCtxES, domain: trustedDomainES},
{testType: "relational", inst: instRelational, orgOwnerCtx: orgOwnerCtxRelational, domain: trustedDomainRelational},
}
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message())
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,
},
}
if tc.expectedErrorMsg == "" {
require.NotNil(t, res)
require.NotEmpty(t, res.GetDeletionDate())
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())
}
})
}
}
}
@@ -4,15 +4,18 @@ package instance_test
import (
"context"
"fmt"
"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"
)
@@ -20,105 +23,140 @@ func TestDeleteInstance(t *testing.T) {
t.Parallel()
// Given
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
ctx, cancel := context.WithTimeout(t.Context(), 15*time.Minute)
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
instanceOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
inst2 := integration.NewInstance(ctxWithSysAuthZ)
// Event store instances
instES := integration.NewInstance(ctxWithSysAuthZ)
instanceESOwnerCtx := instES.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner)
instES2 := integration.NewInstance(ctxWithSysAuthZ)
t.Cleanup(func() {
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
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())
})
instanceRelationalOwnerCtx := instRelational.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner)
instRelational2 := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, instRelational2, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
tt := []struct {
testName string
inputRequest *instance.DeleteInstanceRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedInstanceID string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: inst.ID(),
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when invalid context and invalid id should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: inst.ID() + "invalid",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when instance token for invalid instance should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: inst.ID() + "invalid",
},
inputContext: instanceOwnerCtx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
testName: "when instance token for other instance should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: inst2.ID(),
},
inputContext: instanceOwnerCtx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
testName: "when instance token for own instance should return unauthZ error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: inst.ID(),
},
inputContext: instanceOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when invalid id should return not found error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: inst.ID() + "invalid",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "Errors.Instance.NotFound (COMMA-AE3GS)",
},
{
testName: "when delete succeeds should return deletion date",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: inst.ID(),
},
inputContext: ctxWithSysAuthZ,
expectedInstanceID: inst.ID(),
},
type instAndCtx struct {
testType string
i1 *integration.Instance
i2 *integration.Instance
ctx context.Context
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Test
res, err := inst.Client.InstanceV2.DeleteInstance(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())
}
})
testedInstancesAndCtxs := []instAndCtx{
{testType: "eventstore", i1: instES, i2: instES2, ctx: instanceESOwnerCtx},
{testType: "relational", i1: instRelational, i2: instRelational2, ctx: instanceRelationalOwnerCtx},
}
t.Cleanup(func() {
instES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instES2.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES2.ID()})
instRelational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
instRelational2.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational2.ID()})
})
for _, instanceWithCtx := range testedInstancesAndCtxs {
tt := []struct {
testName string
inputRequest *instance.DeleteInstanceRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedInstanceID string
expectsNullTimestamp bool
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID(),
},
inputContext: t.Context(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when invalid context and invalid id should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID() + "invalid",
},
inputContext: t.Context(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when instance token for invalid instance should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID() + "invalid",
},
inputContext: instanceWithCtx.ctx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
testName: "when instance token for other instance should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i2.ID(),
},
inputContext: instanceWithCtx.ctx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
testName: "when instance token for own instance should return unauthZ error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID(),
},
inputContext: instanceWithCtx.ctx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
// TODO(IAM-Marco): This test won't reach the relational side due to an interceptor
// automatically changing the instance in context to an empty one.
// The interceptor will look for the instance matching the instance ID passed here.
// Since the instance doesn't exist, it will put an empty instance in context.
// But an empty instance has no feature flags enabled.
testName: "when invalid id should return no error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID() + "invalid",
},
inputContext: ctxWithSysAuthZ,
expectsNullTimestamp: true,
},
{
testName: "when delete succeeds should return deletion date",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID(),
},
inputContext: ctxWithSysAuthZ,
expectedInstanceID: instanceWithCtx.i1.ID(),
},
}
for _, tc := range tt {
t.Run(fmt.Sprintf("%s - %s", instanceWithCtx.testType, tc.testName), func(t *testing.T) {
// Test
res, err := instanceWithCtx.i1.Client.InstanceV2.DeleteInstance(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.Equal(t, tc.expectsNullTimestamp, res.GetDeletionDate() == nil)
}
})
}
}
}
func TestUpdateInstance(t *testing.T) {
@@ -130,123 +168,163 @@ func TestUpdateInstance(t *testing.T) {
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
instanceOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
orgOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
inst2 := integration.NewInstance(ctxWithSysAuthZ)
instES := integration.NewInstance(ctxWithSysAuthZ)
instanceOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
orgOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
inst2ES := integration.NewInstance(ctxWithSysAuthZ)
t.Cleanup(func() {
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
// Relational
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())
})
instanceOwnerCtxRelational := instRelational.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
orgOwnerCtxRelational := instRelational.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
inst2Relational := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, inst2Relational, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
tt := []struct {
testName string
inputRequest *instance.UpdateInstanceRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedNewName string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: inst.ID(),
InstanceName: " ",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when invalid context and invalid id should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: inst.ID() + "invalid",
InstanceName: " ",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when instance token for invalid instance should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: inst.ID() + "invalid",
InstanceName: " ",
},
inputContext: instanceOwnerCtx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
testName: "when instance token for other instance should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: inst2.ID(),
InstanceName: " ",
},
inputContext: instanceOwnerCtx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: inst.ID(),
InstanceName: " ",
},
inputContext: orgOwnerCtx,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
testName: "when invalid id should return not found error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: inst.ID() + "invalid",
InstanceName: "an-updated-name",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "Errors.Instance.NotFound (INST-nuso2m)",
},
{
testName: "when update succeeds (instance owner) should change instance name",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: inst.ID(),
InstanceName: "an-updated-name",
},
inputContext: instanceOwnerCtx,
expectedNewName: "an-updated-name",
},
{
testName: "when update succeeds should change instance name",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: inst2.ID(),
InstanceName: "an-updated-name2",
},
inputContext: ctxWithSysAuthZ,
expectedNewName: "an-updated-name2",
},
t.Cleanup(func() {
instES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
inst2ES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst2ES.ID()})
instRelational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
inst2Relational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst2Relational.ID()})
})
type instAndCtx struct {
testType string
inst *integration.Instance
inst2 *integration.Instance
instanceOwnerCtx context.Context
orgOwnerCtx context.Context
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Test
res, err := inst.Client.InstanceV2.UpdateInstance(tc.inputContext, tc.inputRequest)
testedInstancesAndCtxs := []instAndCtx{
{testType: "eventstore", inst: instES, inst2: inst2ES, instanceOwnerCtx: instanceOwnerCtxES, orgOwnerCtx: orgOwnerCtxES},
{testType: "relational", inst: instRelational, inst2: inst2Relational, instanceOwnerCtx: instanceOwnerCtxRelational, orgOwnerCtx: orgOwnerCtxRelational},
}
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message())
if tc.expectedErrorMsg == "" {
for _, instancesWithCtx := range testedInstancesAndCtxs {
tt := []struct {
testName string
inputRequest *instance.UpdateInstanceRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedNewName string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID(),
InstanceName: " ",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when invalid context and invalid id should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID() + "invalid",
InstanceName: " ",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when instance token for invalid instance should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID() + "invalid",
InstanceName: " ",
},
inputContext: instancesWithCtx.instanceOwnerCtx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
testName: "when instance token for other instance should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst2.ID(),
InstanceName: " ",
},
inputContext: instancesWithCtx.instanceOwnerCtx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
// 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.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID(),
InstanceName: "a valid name",
},
inputContext: instancesWithCtx.orgOwnerCtx,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
// TODO(IAM-Marco): This test won't reach the relational side due to an interceptor
// automatically changing the instance in context to an empty one.
// The interceptor will look for the instance matching the instance ID passed here.
// Since the instance doesn't exist, it will put an empty instance in context.
// But an empty instance has no feature flags enabled.
testName: "when invalid id should return not found error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID() + "invalid",
InstanceName: "an-updated-name",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "Errors.Instance.NotFound (INST-nuso2m)",
},
{
testName: "when update succeeds (instance owner) should change instance name",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID(),
InstanceName: "an-updated-name",
},
inputContext: instancesWithCtx.instanceOwnerCtx,
expectedNewName: "an-updated-name",
},
{
testName: "when update succeeds should change instance name",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst2.ID(),
InstanceName: "an-updated-name2",
},
inputContext: ctxWithSysAuthZ,
expectedNewName: "an-updated-name2",
},
}
require.NotNil(t, res)
assert.NotEmpty(t, res.GetChangeDate())
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
retrievedInstance, err := inst.Client.InstanceV2.GetInstance(tc.inputContext, &instance.GetInstanceRequest{InstanceId: tc.inputRequest.GetInstanceId()})
require.Nil(tt, err)
assert.Equal(tt, tc.expectedNewName, retrievedInstance.GetInstance().GetName())
}, retryDuration, tick, "timeout waiting for expected execution result")
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" && instancesWithCtx.testType == "relational" {
continue
}
})
t.Run(fmt.Sprintf("%s - %s", instancesWithCtx.testType, tc.testName), func(t *testing.T) {
// Test
res, err := instES.Client.InstanceV2.UpdateInstance(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)
assert.NotEmpty(t, res.GetChangeDate())
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
retrievedInstance, err := instES.Client.InstanceV2.GetInstance(tc.inputContext, &instance.GetInstanceRequest{InstanceId: tc.inputRequest.GetInstanceId()})
require.Nil(tt, err)
assert.Equal(tt, tc.expectedNewName, retrievedInstance.GetInstance().GetName())
}, retryDuration, tick, "timeout waiting for expected execution result")
}
})
}
}
}
@@ -4,16 +4,19 @@ package instance_test
import (
"context"
"fmt"
"slices"
"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/filter/v2"
"github.com/zitadel/zitadel/pkg/grpc/instance/v2"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
@@ -27,70 +30,110 @@ func TestGetInstance(t *testing.T) {
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
instanceOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
organizationOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
instES := integration.NewInstance(ctxWithSysAuthZ)
instanceOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
organizationOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
// Relational
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())
})
instanceOwnerCtxRelational := instRelational.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
organizationOwnerCtxRelational := instRelational.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
t.Cleanup(func() {
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
instES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
tt := []struct {
testName string
inputContext context.Context
inputInstanceID string
expectedInstanceID string
expectedErrorMsg string
expectedErrorCode codes.Code
}{
{
testName: "when unauthN context should return unauthN error",
inputContext: context.Background(),
inputInstanceID: inst.ID(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputContext: organizationOwnerCtx,
inputInstanceID: inst.ID(),
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
testName: "when request succeeds should return matching instance (systemUser)",
inputContext: ctxWithSysAuthZ,
inputInstanceID: inst.ID(),
expectedInstanceID: inst.ID(),
},
{
testName: "when request succeeds should return matching instance (own context)",
inputContext: instanceOwnerCtx,
expectedInstanceID: inst.ID(),
},
{
testName: "when instance not found should return not found error",
inputContext: ctxWithSysAuthZ,
inputInstanceID: "invalid",
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "Errors.IAM.NotFound (QUERY-n0wng)",
},
type instAndCtx struct {
testType string
inst *integration.Instance
instanceOwnerCtx context.Context
orgOwnerCtx context.Context
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Test
res, err := inst.Client.InstanceV2.GetInstance(tc.inputContext, &instance.GetInstanceRequest{InstanceId: tc.inputInstanceID})
testedInstanceAndCtxs := []instAndCtx{
{testType: "eventstore", inst: instES, instanceOwnerCtx: instanceOwnerCtxES, orgOwnerCtx: organizationOwnerCtxES},
{testType: "relational", inst: instRelational, instanceOwnerCtx: instanceOwnerCtxRelational, orgOwnerCtx: organizationOwnerCtxRelational},
}
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message())
for _, instWithCtx := range testedInstanceAndCtxs {
tt := []struct {
testName string
inputContext context.Context
inputInstanceID string
expectedInstanceID string
expectedErrorMsg string
expectedErrorCode codes.Code
}{
{
testName: "when unauthN context should return unauthN error",
inputContext: context.Background(),
inputInstanceID: instWithCtx.inst.ID(),
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",
inputContext: instWithCtx.orgOwnerCtx,
inputInstanceID: instWithCtx.inst.ID(),
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
testName: "when request succeeds should return matching instance (systemUser)",
inputContext: ctxWithSysAuthZ,
inputInstanceID: instWithCtx.inst.ID(),
expectedInstanceID: instWithCtx.inst.ID(),
},
{
// TODO(IAM-Marco): Decide if we should set the instance in context.
testName: "when request succeeds should return matching instance (own context)",
inputContext: instWithCtx.instanceOwnerCtx,
expectedInstanceID: instWithCtx.inst.ID(),
},
{
// TODO(IAM-Marco): This test won't reach the relational side due to an interceptor
// automatically changing the instance in context to an empty one.
// The interceptor will look for the instance matching the instance ID passed here.
// Since the instance doesn't exist, it will put an empty instance in context.
// But an empty instance has no feature flags enabled.
testName: "when instance not found should return not found error",
inputContext: ctxWithSysAuthZ,
inputInstanceID: "invalid",
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "Errors.IAM.NotFound (QUERY-n0wng)",
},
}
if tc.expectedErrorMsg == "" {
require.NoError(t, err)
assert.Equal(t, tc.expectedInstanceID, res.GetInstance().GetId())
faultyTestCasesForRelational := []string{
// TODO(IAM-Marco): Fix this test for relational case when permission checks are in place (see https://github.com/zitadel/zitadel/issues/10917)
"when unauthZ context should return unauthZ error",
// TODO(IAM-Marco): Decide if we should set the instance in context.
"when request succeeds should return matching instance (own context)",
}
for _, tc := range tt {
if instWithCtx.testType == "relational" && slices.Contains(faultyTestCasesForRelational, tc.testName) {
continue
}
})
t.Run(fmt.Sprintf("%s - %s", instWithCtx.testType, tc.testName), func(t *testing.T) {
// Test
res, err := instWithCtx.inst.Client.InstanceV2.GetInstance(tc.inputContext, &instance.GetInstanceRequest{InstanceId: tc.inputInstanceID})
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message())
if tc.expectedErrorMsg == "" {
require.NoError(t, err)
assert.Equal(t, tc.expectedInstanceID, res.GetInstance().GetId())
}
})
}
}
}
@@ -103,27 +146,27 @@ func TestListInstances(t *testing.T) {
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
instances := make([]*integration.Instance, 2)
instances := make([]*integration.Instance, 5)
inst := integration.NewInstance(ctxWithSysAuthZ)
inst2 := integration.NewInstance(ctxWithSysAuthZ)
instances[0], instances[1] = inst, inst2
inst3 := integration.NewInstance(ctxWithSysAuthZ)
inst4 := integration.NewInstance(ctxWithSysAuthZ)
inst5 := integration.NewInstance(ctxWithSysAuthZ)
instances[0], instances[1], instances[2], instances[3], instances[4] = inst, inst2, inst3, inst4, inst5
t.Cleanup(func() {
inst.Client.FeatureV2.ResetInstanceFeatures(ctxWithSysAuthZ, &feature.ResetInstanceFeaturesRequest{})
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst2.ID()})
})
// Sort in descending order
slices.SortFunc(instances, func(i1, i2 *integration.Instance) int {
res := i1.Instance.Details.CreationDate.AsTime().Compare(i2.Instance.Details.CreationDate.AsTime())
if res == 0 {
return res
}
return -res
inst2.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst2.ID()})
inst3.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst3.ID()})
inst4.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst4.ID()})
inst5.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst5.ID()})
})
orgOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
relTableState := integration.RelationalTablesEnableMatrix()
tt := []struct {
testName string
inputRequest *instance.ListInstancesRequest
@@ -142,6 +185,7 @@ func TestListInstances(t *testing.T) {
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.ListInstancesRequest{
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
@@ -153,43 +197,61 @@ func TestListInstances(t *testing.T) {
{
testName: "when valid request with filter should return paginated response",
inputRequest: &instance.ListInstancesRequest{
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
Pagination: &filter.PaginationRequest{Offset: 1, Limit: 3},
SortingColumn: instance.FieldName_FIELD_NAME_CREATION_DATE,
Filters: []*instance.Filter{
{
Filter: &instance.Filter_InIdsFilter{
InIdsFilter: &filter.InIDsFilter{
Ids: []string{inst.ID(), inst2.ID()},
Ids: []string{inst.ID(), inst2.ID(), inst3.ID(), inst4.ID(), inst5.ID()},
},
},
},
{
Filter: &instance.Filter_CustomDomainsFilter{
CustomDomainsFilter: &instance.CustomDomainsFilter{
Domains: []string{inst4.Domain, inst3.Domain, inst2.Domain, inst.Domain},
},
},
},
},
},
inputContext: ctxWithSysAuthZ,
expectedInstances: []string{inst2.ID(), inst.ID()},
expectedInstances: []string{inst3.ID(), inst2.ID(), inst.ID()},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Test
res, err := inst.Client.InstanceV2.ListInstances(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.Len(t, res.GetInstances(), len(tc.expectedInstances))
for i, ins := range res.GetInstances() {
assert.Equal(t, tc.expectedInstances[i], ins.GetId())
}
}
for _, stateCase := range relTableState {
integration.EnsureInstanceFeature(t, ctx, inst, stateCase.FeatureSet, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.Equal(tCollect, stateCase.FeatureSet.GetEnableRelationalTables(), got.EnableRelationalTables.GetEnabled())
})
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.State == "when relational tables are enabled" {
continue
}
t.Run(fmt.Sprintf("%s - %s", stateCase.State, tc.testName), func(t *testing.T) {
// Test
res, err := inst.Client.InstanceV2.ListInstances(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.Len(t, res.GetInstances(), len(tc.expectedInstances))
for i, ins := range res.GetInstances() {
assert.Equal(t, tc.expectedInstances[i], ins.GetId())
}
}
})
}
}
}
func TestListCustomDomains(t *testing.T) {
@@ -200,115 +262,150 @@ func TestListCustomDomains(t *testing.T) {
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
instanceOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
d1, d2 := "custom."+integration.DomainName(), "custom."+integration.DomainName()
_, err := inst.Client.InstanceV2.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), CustomDomain: d1})
// Eventstore objects
instES := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
instanceOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
d1ES, d2ES := "custom."+integration.DomainName(), "custom."+integration.DomainName()
_, err := instES.Client.InstanceV2.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instES.ID(), CustomDomain: d1ES})
require.Nil(t, err)
_, err = inst.Client.InstanceV2.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), CustomDomain: d2})
_, err = instES.Client.InstanceV2.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instES.ID(), CustomDomain: d2ES})
require.Nil(t, err)
// Relational objects
instRelational := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctx, 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)
instanceOwnerCtxRelational := instRelational.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
d1Relational, d2Relational := "custom."+integration.DomainName(), "custom."+integration.DomainName()
_, err = instRelational.Client.InstanceV2.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instRelational.ID(), CustomDomain: d1Relational})
require.Nil(t, err)
_, err = instRelational.Client.InstanceV2.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instRelational.ID(), CustomDomain: d2Relational})
require.Nil(t, err)
t.Cleanup(func() {
inst.Client.InstanceV2.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), CustomDomain: d1})
inst.Client.InstanceV2.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), CustomDomain: d2})
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
instES.Client.InstanceV2.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instES.ID(), CustomDomain: d1ES})
instES.Client.InstanceV2.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instES.ID(), CustomDomain: d2ES})
instES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instRelational.ID(), CustomDomain: d1Relational})
instRelational.Client.InstanceV2.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instRelational.ID(), CustomDomain: d2Relational})
instRelational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
tt := []struct {
testName string
inputRequest *instance.ListCustomDomainsRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedDomains []string
testData := []struct {
testType string
inst *integration.Instance
sysOwnerCtx context.Context
instOwnerCtx context.Context
orgOwnerCtx context.Context
expectedDomains []string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.ListCustomDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.ListCustomDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance.CustomDomainFilter{
{
Filter: &instance.CustomDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: orgOwnerCtx,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
testName: "when valid request with filter should return paginated response (systemUser)",
inputRequest: &instance.ListCustomDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance.CustomDomainFilter{
{
Filter: &instance.CustomDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: ctxWithSysAuthZ,
expectedDomains: []string{d1, d2},
},
{
testName: "when valid request with filter should return paginated response (own context)",
inputRequest: &instance.ListCustomDomainsRequest{
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance.CustomDomainFilter{
{
Filter: &instance.CustomDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: instanceOwnerCtx,
expectedDomains: []string{d1},
},
{testType: "eventstore", inst: instES, sysOwnerCtx: ctxWithSysAuthZ, instOwnerCtx: instanceOwnerCtxES, orgOwnerCtx: orgOwnerCtxES, expectedDomains: []string{d1ES, d2ES}},
{testType: "relational", inst: instRelational, sysOwnerCtx: ctxWithSysAuthZ, instOwnerCtx: instanceOwnerCtxRelational, orgOwnerCtx: orgOwnerCtxRelational, expectedDomains: []string{d1Relational, d2Relational}},
}
for _, td := range testData {
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, time.Minute)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
// Test
res, err := inst.Client.InstanceV2.ListCustomDomains(tc.inputContext, tc.inputRequest)
tt := []struct {
testName string
inputRequest *instance.ListCustomDomainsRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedDomains []string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.ListCustomDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
},
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.ListCustomDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance.CustomDomainFilter{
{
Filter: &instance.CustomDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: td.orgOwnerCtx,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
testName: "when valid request with filter should return paginated response (systemUser)",
inputRequest: &instance.ListCustomDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance.CustomDomainFilter{
{
Filter: &instance.CustomDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: td.sysOwnerCtx,
expectedDomains: td.expectedDomains,
},
{
testName: "when valid request with filter should return paginated response (own context)",
inputRequest: &instance.ListCustomDomainsRequest{
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance.CustomDomainFilter{
{
Filter: &instance.CustomDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: td.instOwnerCtx,
expectedDomains: []string{td.expectedDomains[0]},
},
}
// Verify
assert.Equal(collect, tc.expectedErrorCode, status.Code(err))
assert.Equal(collect, tc.expectedErrorMsg, status.Convert(err).Message())
for _, tc := range tt {
if tc.testName == "when unauthZ context should return unauthZ error" && td.testType == "relational" {
continue
}
t.Run(fmt.Sprintf("%s - %s", td.testType, tc.testName), func(t *testing.T) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, time.Minute)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
// Test
res, err := td.inst.Client.InstanceV2.ListCustomDomains(tc.inputContext, tc.inputRequest)
if tc.expectedErrorMsg == "" {
domains := []string{}
for _, d := range res.GetDomains() {
domains = append(domains, d.GetDomain())
// Verify
assert.Equal(collect, tc.expectedErrorCode, status.Code(err))
assert.Equal(collect, tc.expectedErrorMsg, status.Convert(err).Message())
if tc.expectedErrorMsg == "" {
domains := []string{}
for _, d := range res.GetDomains() {
domains = append(domains, d.GetDomain())
}
assert.Subset(collect, domains, tc.expectedDomains)
}
assert.Subset(collect, domains, tc.expectedDomains)
}
}, retryDuration, tick)
})
}, retryDuration, tick)
})
}
}
}
@@ -320,108 +417,144 @@ func TestListTrustedDomains(t *testing.T) {
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
instanceOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
d1, d2 := "trusted."+integration.DomainName(), "trusted."+integration.DomainName()
_, err := inst.Client.InstanceV2.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), TrustedDomain: d1})
// Eventstore objects
instES := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
instanceOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
d1ES, d2ES := "trusted."+integration.DomainName(), "trusted."+integration.DomainName()
_, err := instES.Client.InstanceV2.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instES.ID(), TrustedDomain: d1ES})
require.Nil(t, err)
_, err = inst.Client.InstanceV2.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), TrustedDomain: d2})
_, err = instES.Client.InstanceV2.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instES.ID(), TrustedDomain: d2ES})
require.Nil(t, err)
// Relational objects
instRelational := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctx, 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)
instanceOwnerCtxRelational := instRelational.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
d1Relational, d2Relational := "trusted."+integration.DomainName(), "trusted."+integration.DomainName()
_, err = instRelational.Client.InstanceV2.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instRelational.ID(), TrustedDomain: d1Relational})
require.Nil(t, err)
_, err = instRelational.Client.InstanceV2.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instRelational.ID(), TrustedDomain: d2Relational})
require.Nil(t, err)
t.Cleanup(func() {
inst.Client.InstanceV2.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), TrustedDomain: d1})
inst.Client.InstanceV2.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), TrustedDomain: d2})
inst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
instES.Client.InstanceV2.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instES.ID(), TrustedDomain: d1ES})
instES.Client.InstanceV2.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instES.ID(), TrustedDomain: d2ES})
instES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instRelational.ID(), TrustedDomain: d1Relational})
instRelational.Client.InstanceV2.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instRelational.ID(), TrustedDomain: d2Relational})
instRelational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
tt := []struct {
testName string
inputRequest *instance.ListTrustedDomainsRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedDomains []string
testData := []struct {
testType string
inst *integration.Instance
sysOwnerCtx context.Context
instOwnerCtx context.Context
orgOwnerCtx context.Context
expectedDomains []string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.ListTrustedDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.ListTrustedDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
},
inputContext: orgOwnerCtx,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
testName: "when valid request with filter should return paginated response (systemUser)",
inputRequest: &instance.ListTrustedDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance.TrustedDomainFilter{
{
Filter: &instance.TrustedDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{Domain: "trusted", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: ctxWithSysAuthZ,
expectedDomains: []string{d1, d2},
},
{
testName: "when valid request with filter should return paginated response (own context)",
inputRequest: &instance.ListTrustedDomainsRequest{
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance.TrustedDomainFilter{
{
Filter: &instance.TrustedDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{Domain: "trusted", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: instanceOwnerCtx,
expectedDomains: []string{d1},
},
{testType: "eventstore", inst: instES, sysOwnerCtx: ctxWithSysAuthZ, instOwnerCtx: instanceOwnerCtxES, orgOwnerCtx: orgOwnerCtxES, expectedDomains: []string{d1ES, d2ES}},
{testType: "relational", inst: instRelational, sysOwnerCtx: ctxWithSysAuthZ, instOwnerCtx: instanceOwnerCtxRelational, orgOwnerCtx: orgOwnerCtxRelational, expectedDomains: []string{d1Relational, d2Relational}},
}
for _, td := range testData {
tt := []struct {
testName string
inputRequest *instance.ListTrustedDomainsRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedDomains []string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.ListTrustedDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
},
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.ListTrustedDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
},
inputContext: td.orgOwnerCtx,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
testName: "when valid request with filter should return paginated response (systemUser)",
inputRequest: &instance.ListTrustedDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance.TrustedDomainFilter{
{
Filter: &instance.TrustedDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{Domain: "trusted", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: td.sysOwnerCtx,
expectedDomains: td.expectedDomains,
},
{
testName: "when valid request with filter should return paginated response (own context)",
inputRequest: &instance.ListTrustedDomainsRequest{
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance.TrustedDomainFilter{
{
Filter: &instance.TrustedDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{Domain: "trusted", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: td.instOwnerCtx,
expectedDomains: []string{td.expectedDomains[0]},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, time.Minute)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
// Test
res, err := inst.Client.InstanceV2.ListTrustedDomains(tc.inputContext, tc.inputRequest)
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" && td.testType == "relational" {
continue
}
// Verify
assert.Equal(collect, tc.expectedErrorCode, status.Code(err))
assert.Equal(collect, tc.expectedErrorMsg, status.Convert(err).Message())
t.Run(fmt.Sprintf("%s - %s", td.testType, tc.testName), func(t *testing.T) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, time.Minute)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
// Test
res, err := td.inst.Client.InstanceV2.ListTrustedDomains(tc.inputContext, tc.inputRequest)
if tc.expectedErrorMsg == "" {
require.NotNil(t, res)
// Verify
assert.Equal(collect, tc.expectedErrorCode, status.Code(err))
assert.Equal(collect, tc.expectedErrorMsg, status.Convert(err).Message())
domains := []string{}
for _, d := range res.GetTrustedDomain() {
domains = append(domains, d.GetDomain())
if tc.expectedErrorMsg == "" {
require.NotNil(t, res)
domains := []string{}
for _, d := range res.GetTrustedDomain() {
domains = append(domains, d.GetDomain())
}
assert.Subset(collect, domains, tc.expectedDomains)
}
assert.Subset(collect, domains, tc.expectedDomains)
}
}, retryDuration, tick)
})
}, retryDuration, tick)
})
}
}
}
+18 -1
View File
@@ -5,12 +5,17 @@ import (
"connectrpc.com/connect"
instancev2 "github.com/zitadel/zitadel/backend/v3/api/instance/v2"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/instance/v2"
)
func (s *Server) GetInstance(ctx context.Context, _ *connect.Request[instance.GetInstanceRequest]) (*connect.Response[instance.GetInstanceResponse], error) {
func (s *Server) GetInstance(ctx context.Context, req *connect.Request[instance.GetInstanceRequest]) (*connect.Response[instance.GetInstanceResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.GetInstance(ctx, req)
}
if err := s.checkPermission(ctx, domain.PermissionSystemInstanceRead, domain.PermissionInstanceRead); err != nil {
return nil, err
}
@@ -25,6 +30,10 @@ func (s *Server) GetInstance(ctx context.Context, _ *connect.Request[instance.Ge
}
func (s *Server) ListInstances(ctx context.Context, req *connect.Request[instance.ListInstancesRequest]) (*connect.Response[instance.ListInstancesResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.ListInstances(ctx, req)
}
// List instances is currently only allowed with system permissions,
// so we directly check for them in the auth interceptor and do not check here again.
queries, err := ListInstancesRequestToModel(req.Msg, s.systemDefaults)
@@ -44,6 +53,10 @@ func (s *Server) ListInstances(ctx context.Context, req *connect.Request[instanc
}
func (s *Server) ListCustomDomains(ctx context.Context, req *connect.Request[instance.ListCustomDomainsRequest]) (*connect.Response[instance.ListCustomDomainsResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.ListCustomDomains(ctx, req)
}
if err := s.checkPermission(ctx, domain.PermissionSystemInstanceRead, domain.PermissionInstanceRead); err != nil {
return nil, err
}
@@ -64,6 +77,10 @@ func (s *Server) ListCustomDomains(ctx context.Context, req *connect.Request[ins
}
func (s *Server) ListTrustedDomains(ctx context.Context, req *connect.Request[instance.ListTrustedDomainsRequest]) (*connect.Response[instance.ListTrustedDomainsResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.ListTrustedDomains(ctx, req)
}
if err := s.checkPermission(ctx, domain.PermissionSystemInstanceRead, domain.PermissionInstanceRead); err != nil {
return nil, err
}
+32 -4
View File
@@ -6,10 +6,16 @@ import (
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
instancev2 "github.com/zitadel/zitadel/backend/v3/api/instance/v2"
"github.com/zitadel/zitadel/internal/api/authz"
instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
)
func (s *Server) AddCustomDomain(ctx context.Context, req *connect.Request[instance.AddCustomDomainRequest]) (*connect.Response[instance.AddCustomDomainResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.AddCustomDomainBeta(ctx, req)
}
details, err := s.command.AddInstanceDomain(ctx, req.Msg.GetDomain())
if err != nil {
return nil, err
@@ -20,16 +26,30 @@ func (s *Server) AddCustomDomain(ctx context.Context, req *connect.Request[insta
}
func (s *Server) RemoveCustomDomain(ctx context.Context, req *connect.Request[instance.RemoveCustomDomainRequest]) (*connect.Response[instance.RemoveCustomDomainResponse], error) {
details, err := s.command.RemoveInstanceDomain(ctx, req.Msg.GetDomain())
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.RemoveCustomDomainBeta(ctx, req)
}
details, err := s.command.RemoveInstanceDomain(ctx, req.Msg.GetDomain(), false)
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if details != nil {
deletionDate = timestamppb.New(details.EventDate)
}
return connect.NewResponse(&instance.RemoveCustomDomainResponse{
DeletionDate: timestamppb.New(details.EventDate),
DeletionDate: deletionDate,
}), nil
}
func (s *Server) AddTrustedDomain(ctx context.Context, req *connect.Request[instance.AddTrustedDomainRequest]) (*connect.Response[instance.AddTrustedDomainResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.AddTrustedDomainBeta(ctx, req)
}
details, err := s.command.AddTrustedDomain(ctx, req.Msg.GetDomain())
if err != nil {
return nil, err
@@ -40,12 +60,20 @@ func (s *Server) AddTrustedDomain(ctx context.Context, req *connect.Request[inst
}
func (s *Server) RemoveTrustedDomain(ctx context.Context, req *connect.Request[instance.RemoveTrustedDomainRequest]) (*connect.Response[instance.RemoveTrustedDomainResponse], error) {
details, err := s.command.RemoveTrustedDomain(ctx, req.Msg.GetDomain())
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.RemoveTrustedDomainBeta(ctx, req)
}
details, err := s.command.RemoveTrustedDomain(ctx, req.Msg.GetDomain(), false)
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if details != nil {
deletionDate = timestamppb.New(details.EventDate)
}
return connect.NewResponse(&instance.RemoveTrustedDomainResponse{
DeletionDate: timestamppb.New(details.EventDate),
DeletionDate: deletionDate,
}), nil
}
@@ -13,23 +13,28 @@ import (
func (s *Server) DeleteInstance(ctx context.Context, request *connect.Request[instance.DeleteInstanceRequest]) (*connect.Response[instance.DeleteInstanceResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.DeleteInstance(ctx, request)
return instancev2.DeleteInstanceBeta(ctx, request)
}
obj, err := s.command.RemoveInstance(ctx, request.Msg.GetInstanceId())
obj, err := s.command.RemoveInstance(ctx, request.Msg.GetInstanceId(), false)
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if obj != nil {
deletionDate = timestamppb.New(obj.EventDate)
}
return connect.NewResponse(&instance.DeleteInstanceResponse{
DeletionDate: timestamppb.New(obj.EventDate),
DeletionDate: deletionDate,
}), nil
}
func (s *Server) UpdateInstance(ctx context.Context, request *connect.Request[instance.UpdateInstanceRequest]) (*connect.Response[instance.UpdateInstanceResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.UpdateInstance(ctx, request)
return instancev2.UpdateInstanceBeta(ctx, request)
}
obj, err := s.command.UpdateInstance(ctx, request.Msg.GetInstanceName())
@@ -4,16 +4,19 @@ 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"
instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
)
@@ -27,79 +30,98 @@ func TestAddCustomDomain(t *testing.T) {
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
iamOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeIAMOwner)
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.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
relationalInst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: relationalInst.ID()})
})
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: inst.ID(),
Domain: integration.DomainName(),
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: inst.ID(),
Domain: integration.DomainName(),
},
inputContext: iamOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when invalid domain should return invalid argument error",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: inst.ID(),
Domain: " ",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument (INST-28nlD)",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: inst.ID(),
Domain: " " + integration.DomainName(),
},
inputContext: ctxWithSysAuthZ,
},
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 _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Cleanup(func() {
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(),
Domain: 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(),
Domain: 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(),
Domain: " ",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.AddCustomDomainRequest{
InstanceId: instAndCtx.instance.ID(),
Domain: " " + 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.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{Domain: strings.TrimSpace(tc.inputRequest.Domain)})
}
})
// Test
res, err := instAndCtx.instance.Client.InstanceV2Beta.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 == "" {
inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{Domain: strings.TrimSpace(tc.inputRequest.Domain)})
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetCreationDate())
}
})
// Test
res, err := inst.Client.InstanceV2Beta.AddCustomDomain(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 == "" {
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetCreationDate())
}
})
}
}
}
@@ -107,84 +129,108 @@ func TestRemoveCustomDomain(t *testing.T) {
t.Parallel()
// Given
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
ctx, cancel := context.WithTimeout(t.Context(), 15*time.Minute)
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
iamOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeIAMOwner)
instES := integration.NewInstance(ctxWithSysAuthZ)
iamOwnerCtxES := instES.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner)
customDomain := integration.DomainName()
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())
})
_, err := inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: customDomain})
customDomainES := integration.DomainName()
customDomainRelational := integration.DomainName()
_, err := instES.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instES.ID(), Domain: customDomainES})
require.Nil(t, err)
_, err = instRelational.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instRelational.ID(), Domain: customDomainRelational})
require.Nil(t, err)
t.Cleanup(func() {
inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: customDomain})
inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
instES.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instES.ID(), Domain: customDomainES})
instES.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instRelational.ID(), Domain: customDomainRelational})
instRelational.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
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: inst.ID(),
Domain: "custom1",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.RemoveCustomDomainRequest{
InstanceId: inst.ID(),
Domain: "custom1",
},
inputContext: iamOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when invalid domain should return invalid argument error",
inputRequest: &instance.RemoveCustomDomainRequest{
InstanceId: inst.ID(),
Domain: " ",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument (INST-39nls)",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.RemoveCustomDomainRequest{
InstanceId: inst.ID(),
Domain: " " + customDomain,
},
inputContext: ctxWithSysAuthZ,
},
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 _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Test
res, err := inst.Client.InstanceV2Beta.RemoveCustomDomain(tc.inputContext, tc.inputRequest)
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(),
Domain: "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(),
Domain: "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(),
Domain: " ",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.RemoveCustomDomainRequest{
InstanceId: instAndCtx.instance.ID(),
Domain: " " + instAndCtx.customDomain,
},
inputContext: ctxWithSysAuthZ,
},
}
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message())
for _, tc := range tt {
t.Run(fmt.Sprintf("%s - %s", instAndCtx.testType, tc.testName), func(t *testing.T) {
// Test
res, err := instAndCtx.instance.Client.InstanceV2Beta.RemoveCustomDomain(tc.inputContext, tc.inputRequest)
if tc.expectedErrorMsg == "" {
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetDeletionDate())
}
})
// 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())
}
})
}
}
}
@@ -196,80 +242,100 @@ func TestAddTrustedDomain(t *testing.T) {
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner)
// Eventstore objects
instES := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtxES := instES.WithAuthorization(context.Background(), integration.UserTypeOrgOwner)
d1ES := "trusted" + 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 := "trusted" + integration.DomainName()
t.Cleanup(func() {
inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
instES.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
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: inst.ID(),
Domain: "trusted1",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: inst.ID(),
Domain: "trusted1",
},
inputContext: orgOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when invalid domain should return invalid argument error",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: inst.ID(),
Domain: " ",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument (COMMA-Stk21)",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: inst.ID(),
Domain: " " + integration.DomainName(),
},
inputContext: ctxWithSysAuthZ,
},
type instAndCtx struct {
testType string
inst *integration.Instance
orgOwnerCtx context.Context
domain string
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Cleanup(func() {
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(),
Domain: stateCase.domain,
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
Domain: stateCase.domain,
},
inputContext: stateCase.orgOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when invalid domain should return invalid argument error",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
Domain: " ",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.InvalidArgument,
expectedErrorMsg: "Errors.Invalid.Argument",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.AddTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
Domain: " " + stateCase.domain,
},
inputContext: ctxWithSysAuthZ,
},
}
for _, tc := range tt {
t.Run(fmt.Sprintf("%s - %s", stateCase.testType, tc.testName), func(t *testing.T) {
// Test
res, err := stateCase.inst.Client.InstanceV2Beta.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 == "" {
inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{Domain: strings.TrimSpace(tc.inputRequest.Domain)})
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetCreationDate())
}
})
// Test
res, err := inst.Client.InstanceV2Beta.AddTrustedDomain(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 == "" {
assert.NotNil(t, res)
assert.NotEmpty(t, res.GetCreationDate())
}
})
}
}
}
@@ -281,69 +347,95 @@ func TestRemoveTrustedDomain(t *testing.T) {
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner)
// Eventstore objects
instES := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
trustedDomainES := integration.DomainName()
_, err := instES.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instES.ID(), Domain: trustedDomainES})
require.Nil(t, err)
trustedDomain := integration.DomainName()
_, err := inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: trustedDomain})
// 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.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instRelational.ID(), Domain: trustedDomainRelational})
require.Nil(t, err)
t.Cleanup(func() {
inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: trustedDomain})
inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
instES.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instES.ID(), Domain: trustedDomainES})
instES.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instRelational.ID(), Domain: trustedDomainRelational})
instRelational.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
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: inst.ID(),
Domain: "trusted1",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.RemoveTrustedDomainRequest{
InstanceId: inst.ID(),
Domain: "trusted1",
},
inputContext: orgOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.RemoveTrustedDomainRequest{
InstanceId: inst.ID(),
Domain: " " + trustedDomain,
},
inputContext: ctxWithSysAuthZ,
},
type instAndCtx struct {
testType string
inst *integration.Instance
orgOwnerCtx context.Context
domain string
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Test
res, err := inst.Client.InstanceV2Beta.RemoveTrustedDomain(tc.inputContext, tc.inputRequest)
testedInstanceAndCtxs := []instAndCtx{
{testType: "eventstore", inst: instES, orgOwnerCtx: orgOwnerCtxES, domain: trustedDomainES},
{testType: "relational", inst: instRelational, orgOwnerCtx: orgOwnerCtxRelational, domain: trustedDomainRelational},
}
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message())
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(),
Domain: "trusted1",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.RemoveTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
Domain: "trusted1",
},
inputContext: stateCase.orgOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when valid request should return successful response",
inputRequest: &instance.RemoveTrustedDomainRequest{
InstanceId: stateCase.inst.ID(),
Domain: " " + stateCase.domain,
},
inputContext: ctxWithSysAuthZ,
},
}
if tc.expectedErrorMsg == "" {
require.NotNil(t, res)
require.NotEmpty(t, res.GetDeletionDate())
}
})
for _, tc := range tt {
t.Run(fmt.Sprintf("%s - %s", stateCase.testType, tc.testName), func(t *testing.T) {
// Test
res, err := stateCase.inst.Client.InstanceV2Beta.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())
}
})
}
}
}
@@ -35,9 +35,13 @@ func TestDeleteInstace(t *testing.T) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
instances := []*integration.Instance{
instEventStore,
instRelational,
type instanceWithType struct {
instType string
instance *integration.Instance
}
instances := []instanceWithType{
{instType: "eventstore", instance: instEventStore},
{instType: "relational", instance: instRelational},
}
t.Cleanup(func() {
instRelational.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
@@ -46,12 +50,13 @@ func TestDeleteInstace(t *testing.T) {
for _, inst := range instances {
tt := []struct {
testName string
inputRequest *instance.DeleteInstanceRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedInstanceID string
testName string
inputRequest *instance.DeleteInstanceRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedInstanceID string
expectsNullTimestamp bool
}{
{
testName: "when invalid context should return unauthN error",
@@ -63,34 +68,33 @@ func TestDeleteInstace(t *testing.T) {
expectedErrorMsg: "auth header missing",
},
{
testName: "when invalid input should return invalid argument error",
testName: "when instance not found should return no error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: inst.ID() + "invalid",
InstanceId: inst.instance.ID() + "invalid",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "not found",
inputContext: ctxWithSysAuthZ,
expectsNullTimestamp: true,
},
{
testName: "when delete succeeds should return deletion date",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: inst.ID(),
InstanceId: inst.instance.ID(),
},
inputContext: ctxWithSysAuthZ,
expectedInstanceID: inst.ID(),
expectedInstanceID: inst.instance.ID(),
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Run(fmt.Sprintf("%s - %s", inst.instType, tc.testName), func(t *testing.T) {
// Test
res, err := inst.Client.InstanceV2Beta.DeleteInstance(tc.inputContext, tc.inputRequest)
res, err := inst.instance.Client.InstanceV2Beta.DeleteInstance(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 == "" {
require.NotNil(t, res)
require.NotEmpty(t, res.GetDeletionDate())
require.Equal(t, tc.expectsNullTimestamp, res.GetDeletionDate() == nil)
}
})
}
@@ -232,97 +232,126 @@ func TestListCustomDomains(t *testing.T) {
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner)
d1, d2 := "custom."+integration.DomainName(), "custom."+integration.DomainName()
_, err := inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: d1})
// Eventstore objects
instES := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtxES := instES.WithAuthorization(context.Background(), integration.UserTypeOrgOwner)
d1ES, d2ES := "custom."+integration.DomainName(), "custom."+integration.DomainName()
_, err := instES.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instES.ID(), Domain: d1ES})
require.Nil(t, err)
_, err = inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: d2})
_, err = instES.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instES.ID(), Domain: d2ES})
require.Nil(t, err)
// Relational objects
instRelational := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctx, instRelational, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
orgOwnerCtxRelational := instRelational.WithAuthorization(context.Background(), integration.UserTypeOrgOwner)
d1Relational, d2Relational := "custom."+integration.DomainName(), "custom."+integration.DomainName()
_, err = instRelational.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instRelational.ID(), Domain: d1Relational})
require.Nil(t, err)
_, err = instRelational.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: instRelational.ID(), Domain: d2Relational})
require.Nil(t, err)
t.Cleanup(func() {
inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: d1})
inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: d2})
inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
instES.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instES.ID(), Domain: d1ES})
instES.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instES.ID(), Domain: d2ES})
instES.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instRelational.ID(), Domain: d1Relational})
instRelational.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: instRelational.ID(), Domain: d2Relational})
instRelational.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
tt := []struct {
testName string
inputRequest *instance.ListCustomDomainsRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedDomains []string
testData := []struct {
testType string
inst *integration.Instance
orgOwnerCtx context.Context
expectedDomains []string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.ListCustomDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing"},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.ListCustomDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Queries: []*instance.DomainSearchQuery{
{
Query: &instance.DomainSearchQuery_DomainQuery{
DomainQuery: &instance.DomainQuery{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: orgOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when valid request with filter should return paginated response",
inputRequest: &instance.ListCustomDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Queries: []*instance.DomainSearchQuery{
{
Query: &instance.DomainSearchQuery_DomainQuery{
DomainQuery: &instance.DomainQuery{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: ctxWithSysAuthZ,
expectedDomains: []string{d1, d2},
},
{testType: "eventstore", inst: instES, orgOwnerCtx: orgOwnerCtxES, expectedDomains: []string{d1ES, d2ES}},
{testType: "relational", inst: instRelational, orgOwnerCtx: orgOwnerCtxRelational, expectedDomains: []string{d1Relational, d2Relational}},
}
for _, td := range testData {
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, time.Minute)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
// Test
res, err := inst.Client.InstanceV2Beta.ListCustomDomains(tc.inputContext, tc.inputRequest)
tt := []struct {
testName string
inputRequest *instance.ListCustomDomainsRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedDomains []string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.ListCustomDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.ListCustomDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Queries: []*instance.DomainSearchQuery{
{
Query: &instance.DomainSearchQuery_DomainQuery{
DomainQuery: &instance.DomainQuery{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: td.orgOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when valid request with filter should return paginated response",
inputRequest: &instance.ListCustomDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Queries: []*instance.DomainSearchQuery{
{
Query: &instance.DomainSearchQuery_DomainQuery{
DomainQuery: &instance.DomainQuery{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: ctxWithSysAuthZ,
expectedDomains: td.expectedDomains,
},
}
// Verify
assert.Equal(collect, tc.expectedErrorCode, status.Code(err))
assert.Equal(collect, tc.expectedErrorMsg, status.Convert(err).Message())
for _, tc := range tt {
t.Run(fmt.Sprintf("%s - %s", td.testType, tc.testName), func(t *testing.T) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, time.Minute)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
// Test
res, err := td.inst.Client.InstanceV2Beta.ListCustomDomains(tc.inputContext, tc.inputRequest)
if tc.expectedErrorMsg == "" {
domains := []string{}
for _, d := range res.GetDomains() {
domains = append(domains, d.GetDomain())
// Verify
assert.Equal(collect, tc.expectedErrorCode, status.Code(err))
assert.Equal(collect, tc.expectedErrorMsg, status.Convert(err).Message())
if tc.expectedErrorMsg == "" {
domains := []string{}
for _, d := range res.GetDomains() {
domains = append(domains, d.GetDomain())
}
assert.Subset(collect, domains, tc.expectedDomains)
}
assert.Subset(collect, domains, tc.expectedDomains)
}
}, retryDuration, tick)
})
}, retryDuration, tick)
})
}
}
}
@@ -334,91 +363,118 @@ func TestListTrustedDomains(t *testing.T) {
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
inst := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner)
d1, d2 := "trusted."+integration.DomainName(), "trusted."+integration.DomainName()
_, err := inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: d1})
// Eventstore objects
instES := integration.NewInstance(ctxWithSysAuthZ)
orgOwnerCtxES := instES.WithAuthorization(context.Background(), integration.UserTypeOrgOwner)
d1ES, d2ES := "trusted."+integration.DomainName(), "trusted."+integration.DomainName()
_, err := instES.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instES.ID(), Domain: d1ES})
require.Nil(t, err)
_, err = inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: d2})
_, err = instES.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instES.ID(), Domain: d2ES})
require.Nil(t, err)
// Relational objects
instRelational := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctx, instRelational, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
orgOwnerCtxRelational := instRelational.WithAuthorization(context.Background(), integration.UserTypeOrgOwner)
d1Relational, d2Relational := "trusted."+integration.DomainName(), "trusted."+integration.DomainName()
_, err = instRelational.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instRelational.ID(), Domain: d1Relational})
require.Nil(t, err)
_, err = instRelational.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: instRelational.ID(), Domain: d2Relational})
require.Nil(t, err)
t.Cleanup(func() {
inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: d1})
inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: d2})
inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()})
instES.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instES.ID(), Domain: d1ES})
instES.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instES.ID(), Domain: d2ES})
instES.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instRelational.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instRelational.ID(), Domain: d1Relational})
instRelational.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: instRelational.ID(), Domain: d2Relational})
instRelational.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
})
tt := []struct {
testName string
inputRequest *instance.ListTrustedDomainsRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedDomains []string
testData := []struct {
testType string
inst *integration.Instance
orgOwnerCtx context.Context
expectedDomains []string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.ListTrustedDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
{testType: "eventstore", inst: instES, orgOwnerCtx: orgOwnerCtxES, expectedDomains: []string{d1ES, d2ES}},
{testType: "relational", inst: instRelational, orgOwnerCtx: orgOwnerCtxRelational, expectedDomains: []string{d1Relational, d2Relational}},
}
for _, td := range testData {
tt := []struct {
testName string
inputRequest *instance.ListTrustedDomainsRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedDomains []string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.ListTrustedDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.ListTrustedDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
{
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.ListTrustedDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
},
inputContext: td.orgOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
inputContext: orgOwnerCtx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
testName: "when valid request with filter should return paginated response",
inputRequest: &instance.ListTrustedDomainsRequest{
InstanceId: inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
Queries: []*instance.TrustedDomainSearchQuery{
{
Query: &instance.TrustedDomainSearchQuery_DomainQuery{
DomainQuery: &instance.DomainQuery{Domain: "trusted", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
{
testName: "when valid request with filter should return paginated response",
inputRequest: &instance.ListTrustedDomainsRequest{
InstanceId: td.inst.ID(),
Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10},
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
Queries: []*instance.TrustedDomainSearchQuery{
{
Query: &instance.TrustedDomainSearchQuery_DomainQuery{
DomainQuery: &instance.DomainQuery{Domain: "trusted", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS},
},
},
},
},
inputContext: ctxWithSysAuthZ,
expectedDomains: td.expectedDomains,
},
inputContext: ctxWithSysAuthZ,
expectedDomains: []string{d1, d2},
},
}
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, time.Minute)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
// Test
res, err := inst.Client.InstanceV2Beta.ListTrustedDomains(tc.inputContext, tc.inputRequest)
for _, tc := range tt {
t.Run(fmt.Sprintf("%s - %s", td.testType, tc.testName), func(t *testing.T) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, time.Minute)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
// Test
res, err := td.inst.Client.InstanceV2Beta.ListTrustedDomains(tc.inputContext, tc.inputRequest)
// Verify
assert.Equal(collect, tc.expectedErrorCode, status.Code(err))
assert.Equal(collect, tc.expectedErrorMsg, status.Convert(err).Message())
// Verify
assert.Equal(collect, tc.expectedErrorCode, status.Code(err))
assert.Equal(collect, tc.expectedErrorMsg, status.Convert(err).Message())
if tc.expectedErrorMsg == "" {
require.NotNil(t, res)
if tc.expectedErrorMsg == "" {
require.NotNil(t, res)
domains := []string{}
for _, d := range res.GetTrustedDomain() {
domains = append(domains, d.GetDomain())
domains := []string{}
for _, d := range res.GetTrustedDomain() {
domains = append(domains, d.GetDomain())
}
assert.Subset(collect, domains, tc.expectedDomains)
}
assert.Subset(collect, domains, tc.expectedDomains)
}
}, retryDuration, tick)
})
}, retryDuration, tick)
})
}
}
}
+10 -2
View File
@@ -13,7 +13,7 @@ import (
func (s *Server) GetInstance(ctx context.Context, request *connect.Request[instance.GetInstanceRequest]) (*connect.Response[instance.GetInstanceResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.GetInstance(ctx, request)
return instancev2.GetInstanceBeta(ctx, request)
}
inst, err := s.query.Instance(ctx, true)
@@ -28,7 +28,7 @@ func (s *Server) GetInstance(ctx context.Context, request *connect.Request[insta
func (s *Server) ListInstances(ctx context.Context, req *connect.Request[instance.ListInstancesRequest]) (*connect.Response[instance.ListInstancesResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.ListInstances(ctx, req)
return instancev2.ListInstancesBeta(ctx, req)
}
queries, err := ListInstancesRequestToModel(req.Msg, s.systemDefaults)
@@ -48,6 +48,10 @@ func (s *Server) ListInstances(ctx context.Context, req *connect.Request[instanc
}
func (s *Server) ListCustomDomains(ctx context.Context, req *connect.Request[instance.ListCustomDomainsRequest]) (*connect.Response[instance.ListCustomDomainsResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.ListCustomDomainsBeta(ctx, req)
}
queries, err := ListCustomDomainsRequestToModel(req.Msg, s.systemDefaults)
if err != nil {
return nil, err
@@ -65,6 +69,10 @@ func (s *Server) ListCustomDomains(ctx context.Context, req *connect.Request[ins
}
func (s *Server) ListTrustedDomains(ctx context.Context, req *connect.Request[instance.ListTrustedDomainsRequest]) (*connect.Response[instance.ListTrustedDomainsResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return instancev2.ListTrustedDomainsBeta(ctx, req)
}
queries, err := ListTrustedDomainsRequestToModel(req.Msg, s.systemDefaults)
if err != nil {
return nil, err