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
@@ -0,0 +1,191 @@
package convert
import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/backend/v3/domain"
filter_v2 "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
instance_v2 "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
instance_v2beta "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
)
/*
* Domain Model to GRPC v2Beta
*/
func DomainInstanceDomainListModelToGRPCBetaResponse(dms []*domain.InstanceDomain) []*instance_v2beta.Domain {
toReturn := make([]*instance_v2beta.Domain, len(dms))
for i, domain := range dms {
isGenerated := domain.IsGenerated != nil && *domain.IsGenerated
isPrimary := domain.IsPrimary != nil && *domain.IsPrimary
toReturn[i] = &instance_v2beta.Domain{
InstanceId: domain.InstanceID,
CreationDate: timestamppb.New(domain.CreatedAt),
Domain: domain.Domain,
Primary: isPrimary,
Generated: isGenerated,
}
}
return toReturn
}
func TrustedDomainInstanceDomainListModelToGRPCBetaResponse(dms []*domain.InstanceDomain) []*instance_v2beta.TrustedDomain {
toReturn := make([]*instance_v2beta.TrustedDomain, len(dms))
for i, domain := range dms {
toReturn[i] = &instance_v2beta.TrustedDomain{
InstanceId: domain.InstanceID,
CreationDate: timestamppb.New(domain.CreatedAt),
Domain: domain.Domain,
}
}
return toReturn
}
/*
* Domain Model to GRPC v2
*/
func DomainInstanceDomainListModelToGRPCResponse(dms []*domain.InstanceDomain) []*instance_v2.CustomDomain {
toReturn := make([]*instance_v2.CustomDomain, len(dms))
for i, domain := range dms {
isGenerated := domain.IsGenerated != nil && *domain.IsGenerated
isPrimary := domain.IsPrimary != nil && *domain.IsPrimary
toReturn[i] = &instance_v2.CustomDomain{
InstanceId: domain.InstanceID,
CreationDate: timestamppb.New(domain.CreatedAt),
Domain: domain.Domain,
Primary: isPrimary,
Generated: isGenerated,
}
}
return toReturn
}
func TrustedDomainInstanceDomainListModelToGRPCResponse(dms []*domain.InstanceDomain) []*instance_v2.TrustedDomain {
toReturn := make([]*instance_v2.TrustedDomain, len(dms))
for i, domain := range dms {
toReturn[i] = &instance_v2.TrustedDomain{
InstanceId: domain.InstanceID,
CreationDate: timestamppb.New(domain.CreatedAt),
Domain: domain.Domain,
}
}
return toReturn
}
/*
* GRPC v2Beta Requests to GRPC v2
*/
func ListCustomDomainsBetaRequestToV2Request(in *instance_v2beta.ListCustomDomainsRequest) *instance_v2.ListCustomDomainsRequest {
return &instance_v2.ListCustomDomainsRequest{
InstanceId: in.GetInstanceId(),
Pagination: &filter_v2.PaginationRequest{
Offset: in.GetPagination().GetOffset(),
Limit: in.GetPagination().GetLimit(),
Asc: in.GetPagination().GetAsc(),
},
SortingColumn: listCustomDomainsBetaSortingColToV2Request(in.GetSortingColumn()),
Filters: listCustomDomainsQueriesToV2Request(in.GetQueries()),
}
}
func listCustomDomainsBetaSortingColToV2Request(domainFieldName instance_v2beta.DomainFieldName) instance_v2.DomainFieldName {
switch domainFieldName {
case instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE:
return instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE
case instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN:
return instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN
case instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED:
return instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED
case instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY:
return instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY
case instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED:
fallthrough
default:
return instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED
}
}
func listCustomDomainsQueriesToV2Request(domainSearchQuery []*instance_v2beta.DomainSearchQuery) []*instance_v2.CustomDomainFilter {
toReturn := make([]*instance_v2.CustomDomainFilter, len(domainSearchQuery))
for i, query := range domainSearchQuery {
switch assertedType := query.GetQuery().(type) {
case *instance_v2beta.DomainSearchQuery_DomainQuery:
filter := &instance_v2.CustomDomainFilter_DomainFilter{
DomainFilter: &instance_v2.DomainFilter{
Domain: assertedType.DomainQuery.GetDomain(),
Method: assertedType.DomainQuery.GetMethod(),
},
}
toReturn[i] = &instance_v2.CustomDomainFilter{
Filter: filter,
}
case *instance_v2beta.DomainSearchQuery_GeneratedQuery:
filter := &instance_v2.CustomDomainFilter_GeneratedFilter{
GeneratedFilter: assertedType.GeneratedQuery.GetGenerated(),
}
toReturn[i] = &instance_v2.CustomDomainFilter{
Filter: filter,
}
case *instance_v2beta.DomainSearchQuery_PrimaryQuery:
filter := &instance_v2.CustomDomainFilter_PrimaryFilter{
PrimaryFilter: assertedType.PrimaryQuery.GetPrimary(),
}
toReturn[i] = &instance_v2.CustomDomainFilter{
Filter: filter,
}
}
}
return toReturn
}
func ListTrustedDomainsBetaRequestToV2Request(in *instance_v2beta.ListTrustedDomainsRequest) *instance_v2.ListTrustedDomainsRequest {
return &instance_v2.ListTrustedDomainsRequest{
InstanceId: in.GetInstanceId(),
Pagination: &filter_v2.PaginationRequest{
Offset: in.GetPagination().GetOffset(),
Limit: in.GetPagination().GetLimit(),
Asc: in.GetPagination().GetAsc(),
},
SortingColumn: listTrustedDomainsBetaSortingColToV2Request(in.GetSortingColumn()),
Filters: listTrustedDomainsQueriesToV2Request(in.GetQueries()),
}
}
func listTrustedDomainsBetaSortingColToV2Request(domainFieldName instance_v2beta.TrustedDomainFieldName) instance_v2.TrustedDomainFieldName {
switch domainFieldName {
case instance_v2beta.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE:
return instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE
case instance_v2beta.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN:
return instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN
case instance_v2beta.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED:
fallthrough
default:
return instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED
}
}
func listTrustedDomainsQueriesToV2Request(domainSearchQuery []*instance_v2beta.TrustedDomainSearchQuery) []*instance_v2.TrustedDomainFilter {
toReturn := make([]*instance_v2.TrustedDomainFilter, len(domainSearchQuery))
for i, query := range domainSearchQuery {
if assertedType, ok := query.GetQuery().(*instance_v2beta.TrustedDomainSearchQuery_DomainQuery); ok {
filter := &instance_v2.TrustedDomainFilter_DomainFilter{
DomainFilter: &instance_v2.DomainFilter{
Domain: assertedType.DomainQuery.GetDomain(),
Method: assertedType.DomainQuery.GetMethod(),
},
}
toReturn[i] = &instance_v2.TrustedDomainFilter{
Filter: filter,
}
}
}
return toReturn
}
@@ -0,0 +1,426 @@
package convert
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/backend/v3/domain"
filter_v2 "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
filter_v2beta "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
instance_v2 "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
instance_v2beta "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)
func TestListCustomDomainsBetaRequestToV2Request(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
inputRequest *instance_v2beta.ListCustomDomainsRequest
expectedRequest *instance_v2.ListCustomDomainsRequest
}{
{
testName: "with all fields",
inputRequest: &instance_v2beta.ListCustomDomainsRequest{
InstanceId: "instance1",
Pagination: &filter_v2beta.PaginationRequest{
Offset: 0,
Limit: 10,
Asc: true,
},
SortingColumn: instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN,
Queries: []*instance_v2beta.DomainSearchQuery{
{
Query: &instance_v2beta.DomainSearchQuery_DomainQuery{
DomainQuery: &instance_v2beta.DomainQuery{
Domain: "test.com",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
{
Query: &instance_v2beta.DomainSearchQuery_GeneratedQuery{
GeneratedQuery: &instance_v2beta.DomainGeneratedQuery{
Generated: true,
},
},
},
{
Query: &instance_v2beta.DomainSearchQuery_PrimaryQuery{
PrimaryQuery: &instance_v2beta.DomainPrimaryQuery{
Primary: true,
},
},
},
},
},
expectedRequest: &instance_v2.ListCustomDomainsRequest{
InstanceId: "instance1",
Pagination: &filter_v2.PaginationRequest{
Offset: 0,
Limit: 10,
Asc: true,
},
SortingColumn: instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN,
Filters: []*instance_v2.CustomDomainFilter{
{
Filter: &instance_v2.CustomDomainFilter_DomainFilter{
DomainFilter: &instance_v2.DomainFilter{
Domain: "test.com",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
{
Filter: &instance_v2.CustomDomainFilter_GeneratedFilter{
GeneratedFilter: true,
},
},
{
Filter: &instance_v2.CustomDomainFilter_PrimaryFilter{
PrimaryFilter: true,
},
},
},
},
},
{
testName: "empty request",
inputRequest: &instance_v2beta.ListCustomDomainsRequest{},
expectedRequest: &instance_v2.ListCustomDomainsRequest{
Pagination: &filter_v2.PaginationRequest{},
Filters: []*instance_v2.CustomDomainFilter{},
},
},
{
testName: "with sorting only",
inputRequest: &instance_v2beta.ListCustomDomainsRequest{
SortingColumn: instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
},
expectedRequest: &instance_v2.ListCustomDomainsRequest{
Pagination: &filter_v2.PaginationRequest{},
SortingColumn: instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance_v2.CustomDomainFilter{},
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
got := ListCustomDomainsBetaRequestToV2Request(tc.inputRequest)
assert.Equal(t, tc.expectedRequest, got)
})
}
}
func Test_listCustomDomainsBetaSortingColToV2Request(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
inputFieldName instance_v2beta.DomainFieldName
expecteFieldName instance_v2.DomainFieldName
}{
{
testName: "creation date",
inputFieldName: instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
expecteFieldName: instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
},
{
testName: "domain",
inputFieldName: instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN,
expecteFieldName: instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN,
},
{
testName: "generated",
inputFieldName: instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED,
expecteFieldName: instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED,
},
{
testName: "primary",
inputFieldName: instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY,
expecteFieldName: instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY,
},
{
testName: "unspecified",
inputFieldName: instance_v2beta.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED,
expecteFieldName: instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED,
},
{
testName: "default",
inputFieldName: instance_v2beta.DomainFieldName(99),
expecteFieldName: instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED,
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
got := listCustomDomainsBetaSortingColToV2Request(tc.inputFieldName)
assert.Equal(t, tc.expecteFieldName, got)
})
}
}
func TestListTrustedDomainsBetaRequestToV2Request(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
inputRequest *instance_v2beta.ListTrustedDomainsRequest
expectedRequest *instance_v2.ListTrustedDomainsRequest
}{
{
testName: "with all fields",
inputRequest: &instance_v2beta.ListTrustedDomainsRequest{
InstanceId: "instance1",
Pagination: &filter_v2beta.PaginationRequest{
Offset: 0,
Limit: 10,
Asc: true,
},
SortingColumn: instance_v2beta.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN,
Queries: []*instance_v2beta.TrustedDomainSearchQuery{
{
Query: &instance_v2beta.TrustedDomainSearchQuery_DomainQuery{
DomainQuery: &instance_v2beta.DomainQuery{
Domain: "test.com",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
},
expectedRequest: &instance_v2.ListTrustedDomainsRequest{
InstanceId: "instance1",
Pagination: &filter_v2.PaginationRequest{
Offset: 0,
Limit: 10,
Asc: true,
},
SortingColumn: instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN,
Filters: []*instance_v2.TrustedDomainFilter{
{
Filter: &instance_v2.TrustedDomainFilter_DomainFilter{
DomainFilter: &instance_v2.DomainFilter{
Domain: "test.com",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
},
},
{
testName: "empty request",
inputRequest: &instance_v2beta.ListTrustedDomainsRequest{},
expectedRequest: &instance_v2.ListTrustedDomainsRequest{
Pagination: &filter_v2.PaginationRequest{},
Filters: []*instance_v2.TrustedDomainFilter{},
},
},
{
testName: "with sorting only",
inputRequest: &instance_v2beta.ListTrustedDomainsRequest{
SortingColumn: instance_v2beta.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
},
expectedRequest: &instance_v2.ListTrustedDomainsRequest{
Pagination: &filter_v2.PaginationRequest{},
SortingColumn: instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
Filters: []*instance_v2.TrustedDomainFilter{},
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
got := ListTrustedDomainsBetaRequestToV2Request(tc.inputRequest)
assert.Equal(t, tc.expectedRequest, got)
})
}
}
func Test_listTrustedDomainsBetaSortingColToV2Request(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
inputFieldName instance_v2beta.TrustedDomainFieldName
expecteFieldName instance_v2.TrustedDomainFieldName
}{
{
testName: "creation date",
inputFieldName: instance_v2beta.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
expecteFieldName: instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
},
{
testName: "domain",
inputFieldName: instance_v2beta.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN,
expecteFieldName: instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN,
},
{
testName: "unspecified",
inputFieldName: instance_v2beta.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED,
expecteFieldName: instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED,
},
{
testName: "default",
inputFieldName: instance_v2beta.TrustedDomainFieldName(99),
expecteFieldName: instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED,
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
got := listTrustedDomainsBetaSortingColToV2Request(tc.inputFieldName)
assert.Equal(t, tc.expecteFieldName, got)
})
}
}
func TestTrustedDomainInstanceDomainListModelToGRPCBetaResponse(t *testing.T) {
t.Parallel()
testTime := time.Now()
tt := []struct {
name string
input []*domain.InstanceDomain
expected []*instance_v2beta.TrustedDomain
}{
{
name: "single domain",
input: []*domain.InstanceDomain{
{
InstanceID: "instance1",
CreatedAt: testTime,
Domain: "test.com",
},
},
expected: []*instance_v2beta.TrustedDomain{
{
InstanceId: "instance1",
CreationDate: timestamppb.New(testTime),
Domain: "test.com",
},
},
},
{
name: "multiple domains",
input: []*domain.InstanceDomain{
{
InstanceID: "instance1",
CreatedAt: testTime,
Domain: "test1.com",
},
{
InstanceID: "instance2",
CreatedAt: testTime,
Domain: "test2.com",
},
},
expected: []*instance_v2beta.TrustedDomain{
{
InstanceId: "instance1",
CreationDate: timestamppb.New(testTime),
Domain: "test1.com",
},
{
InstanceId: "instance2",
CreationDate: timestamppb.New(testTime),
Domain: "test2.com",
},
},
},
{
name: "empty list",
input: []*domain.InstanceDomain{},
expected: []*instance_v2beta.TrustedDomain{},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := TrustedDomainInstanceDomainListModelToGRPCBetaResponse(tc.input)
assert.Equal(t, tc.expected, got)
})
}
}
func TestTrustedDomainInstanceDomainListModelToGRPCResponse(t *testing.T) {
t.Parallel()
testTime := time.Now()
tt := []struct {
name string
input []*domain.InstanceDomain
expected []*instance_v2.TrustedDomain
}{
{
name: "single domain",
input: []*domain.InstanceDomain{
{
InstanceID: "instance1",
CreatedAt: testTime,
Domain: "test.com",
},
},
expected: []*instance_v2.TrustedDomain{
{
InstanceId: "instance1",
CreationDate: timestamppb.New(testTime),
Domain: "test.com",
},
},
},
{
name: "multiple domains",
input: []*domain.InstanceDomain{
{
InstanceID: "instance1",
CreatedAt: testTime,
Domain: "test1.com",
},
{
InstanceID: "instance2",
CreatedAt: testTime,
Domain: "test2.com",
},
},
expected: []*instance_v2.TrustedDomain{
{
InstanceId: "instance1",
CreationDate: timestamppb.New(testTime),
Domain: "test1.com",
},
{
InstanceId: "instance2",
CreationDate: timestamppb.New(testTime),
Domain: "test2.com",
},
},
},
{
name: "empty list",
input: []*domain.InstanceDomain{},
expected: []*instance_v2.TrustedDomain{},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := TrustedDomainInstanceDomainListModelToGRPCResponse(tc.input)
assert.Equal(t, tc.expected, got)
})
}
}
+92 -24
View File
@@ -5,15 +5,42 @@ import (
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/cmd/build"
instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
instance_v2 "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
instance_v2beta "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
)
/*
* Domain Model to GRPC v2Beta
*/
func DomainInstanceListModelToGRPCBetaResponse(instances []*domain.Instance) []*instance_v2beta.Instance {
toReturn := make([]*instance_v2beta.Instance, len(instances))
for i, inst := range instances {
toReturn[i] = DomainInstanceModelToGRPCBetaResponse(inst)
}
return toReturn
}
func DomainInstanceModelToGRPCBetaResponse(inst *domain.Instance) *instance_v2beta.Instance {
return &instance_v2beta.Instance{
Id: inst.ID,
ChangeDate: timestamppb.New(inst.UpdatedAt),
CreationDate: timestamppb.New(inst.CreatedAt),
Name: inst.Name,
Version: build.Version(),
Domains: DomainInstanceDomainListModelToGRPCBetaResponse(inst.Domains),
}
}
/*
* Domain Model to GRPC v2
*/
func DomainInstanceListModelToGRPCResponse(instances []*domain.Instance) []*instance.Instance {
toReturn := make([]*instance.Instance, len(instances))
func DomainInstanceListModelToGRPCResponse(instances []*domain.Instance) []*instance_v2.Instance {
toReturn := make([]*instance_v2.Instance, len(instances))
for i, inst := range instances {
toReturn[i] = DomainInstanceModelToGRPCResponse(inst)
@@ -22,31 +49,72 @@ func DomainInstanceListModelToGRPCResponse(instances []*domain.Instance) []*inst
return toReturn
}
func DomainInstanceModelToGRPCResponse(inst *domain.Instance) *instance.Instance {
return &instance.Instance{
Id: inst.ID,
ChangeDate: timestamppb.New(inst.UpdatedAt),
CreationDate: timestamppb.New(inst.CreatedAt),
State: instance.State_STATE_RUNNING, // TODO(IAM-Marco): Not sure what to put here
Name: inst.Name,
Version: build.Version(),
Domains: domainInstanceDomainListModelToGRPCResponse(inst.Domains),
func DomainInstanceModelToGRPCResponse(inst *domain.Instance) *instance_v2.Instance {
return &instance_v2.Instance{
Id: inst.ID,
ChangeDate: timestamppb.New(inst.UpdatedAt),
CreationDate: timestamppb.New(inst.CreatedAt),
State: instance_v2.State_STATE_RUNNING, // TODO(IAM-Marco): Not sure what to put here
Name: inst.Name,
Version: build.Version(),
CustomDomains: DomainInstanceDomainListModelToGRPCResponse(inst.Domains),
}
}
func domainInstanceDomainListModelToGRPCResponse(dms []*domain.InstanceDomain) []*instance.Domain {
toReturn := make([]*instance.Domain, len(dms))
for i, domain := range dms {
isGenerated := domain.IsGenerated != nil && *domain.IsGenerated
isPrimary := domain.IsPrimary != nil && *domain.IsPrimary
toReturn[i] = &instance.Domain{
InstanceId: domain.InstanceID,
CreationDate: timestamppb.New(domain.CreatedAt),
Domain: domain.Domain,
Primary: isPrimary,
Generated: isGenerated,
/*
* GRPC v2Beta Requests to GRPC v2
*/
func ListInstancesBetaRequestToV2Request(in *instance_v2beta.ListInstancesRequest) *instance_v2.ListInstancesRequest {
return &instance_v2.ListInstancesRequest{
Pagination: &filter.PaginationRequest{
Offset: in.GetPagination().GetOffset(),
Limit: in.GetPagination().GetLimit(),
Asc: in.GetPagination().GetAsc(),
},
SortingColumn: listInstancesBetaSortingColToV2Request(in.SortingColumn),
Filters: listInstancesQueriesToV2Request(in.GetQueries()),
}
}
func listInstancesQueriesToV2Request(queries []*instance_v2beta.Query) []*instance_v2.Filter {
toReturn := make([]*instance_v2.Filter, len(queries))
for i, query := range queries {
switch assertedQuery := query.GetQuery().(type) {
case *instance_v2beta.Query_DomainQuery:
filter := &instance_v2.Filter_CustomDomainsFilter{
CustomDomainsFilter: &instance_v2.CustomDomainsFilter{
Domains: assertedQuery.DomainQuery.GetDomains(),
},
}
toReturn[i] = &instance_v2.Filter{Filter: filter}
case *instance_v2beta.Query_IdQuery:
filter := &instance_v2.Filter_InIdsFilter{
InIdsFilter: &filter.InIDsFilter{
Ids: assertedQuery.IdQuery.GetIds(),
},
}
toReturn[i] = &instance_v2.Filter{Filter: filter}
}
}
return toReturn
}
func listInstancesBetaSortingColToV2Request(fieldName *instance_v2beta.FieldName) instance_v2.FieldName {
if fieldName == nil {
return instance_v2.FieldName_FIELD_NAME_UNSPECIFIED
}
switch *fieldName {
case instance_v2beta.FieldName_FIELD_NAME_CREATION_DATE:
return instance_v2.FieldName_FIELD_NAME_CREATION_DATE
case instance_v2beta.FieldName_FIELD_NAME_ID:
return instance_v2.FieldName_FIELD_NAME_ID
case instance_v2beta.FieldName_FIELD_NAME_NAME:
return instance_v2.FieldName_FIELD_NAME_NAME
case instance_v2beta.FieldName_FIELD_NAME_UNSPECIFIED:
fallthrough
default:
return instance_v2.FieldName_FIELD_NAME_UNSPECIFIED
}
}
@@ -9,9 +9,99 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/backend/v3/domain"
instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
filter_v2 "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
filter_v2beta "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
instance_v2 "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
instance_v2beta "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
)
func TestDomainInstanceModelToGRPCBetaResponse(t *testing.T) {
// Given
t.Parallel()
now := time.Now().UTC()
yesterday := now.AddDate(0, 0, -1)
inputInstance := &domain.Instance{
ID: "instance-1",
Name: "Instance One",
CreatedAt: yesterday,
UpdatedAt: now,
Domains: []*domain.InstanceDomain{
{
InstanceID: "instance-1",
Domain: "d1.example.com",
IsPrimary: gu.Ptr(true),
CreatedAt: yesterday,
},
{
InstanceID: "instance-1",
Domain: "d2.example.com",
IsGenerated: gu.Ptr(true),
CreatedAt: yesterday,
},
{
InstanceID: "instance-1",
Domain: "d3.example.com",
IsPrimary: gu.Ptr(true),
IsGenerated: gu.Ptr(false),
CreatedAt: yesterday,
},
{
InstanceID: "instance-1",
Domain: "d4.example.com",
IsPrimary: gu.Ptr(false),
IsGenerated: gu.Ptr(true),
CreatedAt: yesterday,
},
},
}
expectedInstance := &instance_v2beta.Instance{
Id: "instance-1",
ChangeDate: timestamppb.New(now),
CreationDate: timestamppb.New(yesterday),
Name: "Instance One",
Version: "",
Domains: []*instance_v2beta.Domain{
{
InstanceId: "instance-1",
CreationDate: timestamppb.New(yesterday),
Domain: "d1.example.com",
Primary: true,
Generated: false,
},
{
InstanceId: "instance-1",
CreationDate: timestamppb.New(yesterday),
Domain: "d2.example.com",
Primary: false,
Generated: true,
},
{
InstanceId: "instance-1",
CreationDate: timestamppb.New(yesterday),
Domain: "d3.example.com",
Primary: true,
Generated: false,
},
{
InstanceId: "instance-1",
CreationDate: timestamppb.New(yesterday),
Domain: "d4.example.com",
Primary: false,
Generated: true,
},
},
}
// Test
res := DomainInstanceModelToGRPCBetaResponse(inputInstance)
// Verify
assert.Equal(t, expectedInstance, res)
}
func TestDomainInstanceModelToGRPCResponse(t *testing.T) {
// Given
t.Parallel()
@@ -54,14 +144,14 @@ func TestDomainInstanceModelToGRPCResponse(t *testing.T) {
},
}
expectedInstance := &instance.Instance{
expectedInstance := &instance_v2.Instance{
Id: "instance-1",
ChangeDate: timestamppb.New(now),
CreationDate: timestamppb.New(yesterday),
State: instance.State_STATE_RUNNING,
State: instance_v2.State_STATE_RUNNING,
Name: "Instance One",
Version: "",
Domains: []*instance.Domain{
CustomDomains: []*instance_v2.CustomDomain{
{
InstanceId: "instance-1",
CreationDate: timestamppb.New(yesterday),
@@ -100,19 +190,19 @@ func TestDomainInstanceModelToGRPCResponse(t *testing.T) {
assert.Equal(t, expectedInstance, res)
}
func TestDomainInstanceListModelToGRPCResponse(t *testing.T) {
func TestDomainInstanceListModelToGRPCBetaResponse(t *testing.T) {
t.Parallel()
now := time.Now()
tt := []struct {
testName string
inputResult []*domain.Instance
expectedResult []*instance.Instance
expectedResult []*instance_v2beta.Instance
}{
{
testName: "empty result",
inputResult: []*domain.Instance{},
expectedResult: []*instance.Instance{},
expectedResult: []*instance_v2beta.Instance{},
},
{
testName: "single instance without domains",
@@ -125,14 +215,13 @@ func TestDomainInstanceListModelToGRPCResponse(t *testing.T) {
Domains: nil,
},
},
expectedResult: []*instance.Instance{
expectedResult: []*instance_v2beta.Instance{
{
Id: "instance1",
Name: "test-instance",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
State: instance.State_STATE_RUNNING,
Domains: []*instance.Domain{},
Domains: []*instance_v2beta.Domain{},
},
},
},
@@ -162,14 +251,13 @@ func TestDomainInstanceListModelToGRPCResponse(t *testing.T) {
Domains: nil,
},
},
expectedResult: []*instance.Instance{
expectedResult: []*instance_v2beta.Instance{
{
Id: "instance1",
Name: "test-instance-1",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
State: instance.State_STATE_RUNNING,
Domains: []*instance.Domain{
Domains: []*instance_v2beta.Domain{
{
InstanceId: "instance1",
Domain: "domain1.com",
@@ -184,8 +272,110 @@ func TestDomainInstanceListModelToGRPCResponse(t *testing.T) {
Name: "test-instance-2",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
State: instance.State_STATE_RUNNING,
Domains: []*instance.Domain{},
Domains: []*instance_v2beta.Domain{},
},
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Test
result := DomainInstanceListModelToGRPCBetaResponse(tc.inputResult)
// Verify
assert.Equal(t, tc.expectedResult, result)
})
}
}
func TestDomainInstanceListModelToGRPCResponse(t *testing.T) {
t.Parallel()
now := time.Now()
tt := []struct {
testName string
inputResult []*domain.Instance
expectedResult []*instance_v2.Instance
}{
{
testName: "empty result",
inputResult: []*domain.Instance{},
expectedResult: []*instance_v2.Instance{},
},
{
testName: "single instance without domains",
inputResult: []*domain.Instance{
{
ID: "instance1",
Name: "test-instance",
CreatedAt: now,
UpdatedAt: now,
Domains: nil,
},
},
expectedResult: []*instance_v2.Instance{
{
Id: "instance1",
Name: "test-instance",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
State: instance_v2.State_STATE_RUNNING,
CustomDomains: []*instance_v2.CustomDomain{},
},
},
},
{
testName: "multiple instances with domains",
inputResult: []*domain.Instance{
{
ID: "instance1",
Name: "test-instance-1",
CreatedAt: now,
UpdatedAt: now,
Domains: []*domain.InstanceDomain{
{
InstanceID: "instance1",
Domain: "domain1.com",
CreatedAt: now,
IsPrimary: gu.Ptr(true),
IsGenerated: gu.Ptr(false),
},
},
},
{
ID: "instance2",
Name: "test-instance-2",
CreatedAt: now,
UpdatedAt: now,
Domains: nil,
},
},
expectedResult: []*instance_v2.Instance{
{
Id: "instance1",
Name: "test-instance-1",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
State: instance_v2.State_STATE_RUNNING,
CustomDomains: []*instance_v2.CustomDomain{
{
InstanceId: "instance1",
Domain: "domain1.com",
CreationDate: timestamppb.New(now),
Primary: true,
Generated: false,
},
},
},
{
Id: "instance2",
Name: "test-instance-2",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
State: instance_v2.State_STATE_RUNNING,
CustomDomains: []*instance_v2.CustomDomain{},
},
},
},
@@ -202,3 +392,140 @@ func TestDomainInstanceListModelToGRPCResponse(t *testing.T) {
})
}
}
func TestListInstancesBetaRequestToV2Request(t *testing.T) {
t.Parallel()
tt := []struct {
name string
request *instance_v2beta.ListInstancesRequest
want *instance_v2.ListInstancesRequest
}{
{
name: "empty request",
request: &instance_v2beta.ListInstancesRequest{
Pagination: &filter_v2beta.PaginationRequest{},
},
want: &instance_v2.ListInstancesRequest{
Pagination: &filter_v2.PaginationRequest{},
SortingColumn: instance_v2.FieldName_FIELD_NAME_UNSPECIFIED,
Filters: []*instance_v2.Filter{},
},
},
{
name: "request with all fields",
request: &instance_v2beta.ListInstancesRequest{
Pagination: &filter_v2beta.PaginationRequest{
Offset: 10,
Limit: 20,
Asc: true,
},
SortingColumn: gu.Ptr(instance_v2beta.FieldName_FIELD_NAME_NAME),
Queries: []*instance_v2beta.Query{
{
Query: &instance_v2beta.Query_DomainQuery{
DomainQuery: &instance_v2beta.DomainsQuery{
Domains: []string{"domain1.com", "domain2.com"},
},
},
},
{
Query: &instance_v2beta.Query_IdQuery{
IdQuery: &instance_v2beta.IdsQuery{
Ids: []string{"id1", "id2"},
},
},
},
},
},
want: &instance_v2.ListInstancesRequest{
Pagination: &filter_v2.PaginationRequest{
Offset: 10,
Limit: 20,
Asc: true,
},
SortingColumn: instance_v2.FieldName_FIELD_NAME_NAME,
Filters: []*instance_v2.Filter{
{
Filter: &instance_v2.Filter_CustomDomainsFilter{
CustomDomainsFilter: &instance_v2.CustomDomainsFilter{
Domains: []string{"domain1.com", "domain2.com"},
},
},
},
{
Filter: &instance_v2.Filter_InIdsFilter{
InIdsFilter: &filter_v2.InIDsFilter{
Ids: []string{"id1", "id2"},
},
},
},
},
},
},
{
name: "request with different sorting field",
request: &instance_v2beta.ListInstancesRequest{
Pagination: &filter_v2beta.PaginationRequest{},
SortingColumn: gu.Ptr(instance_v2beta.FieldName_FIELD_NAME_CREATION_DATE),
},
want: &instance_v2.ListInstancesRequest{
Pagination: &filter_v2.PaginationRequest{},
SortingColumn: instance_v2.FieldName_FIELD_NAME_CREATION_DATE,
Filters: []*instance_v2.Filter{},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := ListInstancesBetaRequestToV2Request(tc.request)
assert.Equal(t, tc.want, got)
})
}
}
func TestListInstancesBetaSortingColToV2Request(t *testing.T) {
t.Parallel()
tests := []struct {
name string
fieldName *instance_v2beta.FieldName
want instance_v2.FieldName
}{
{
name: "nil field name",
fieldName: nil,
want: instance_v2.FieldName_FIELD_NAME_UNSPECIFIED,
},
{
name: "creation date field",
fieldName: gu.Ptr(instance_v2beta.FieldName_FIELD_NAME_CREATION_DATE),
want: instance_v2.FieldName_FIELD_NAME_CREATION_DATE,
},
{
name: "id field",
fieldName: gu.Ptr(instance_v2beta.FieldName_FIELD_NAME_ID),
want: instance_v2.FieldName_FIELD_NAME_ID,
},
{
name: "name field",
fieldName: gu.Ptr(instance_v2beta.FieldName_FIELD_NAME_NAME),
want: instance_v2.FieldName_FIELD_NAME_NAME,
},
{
name: "unspecified field",
fieldName: gu.Ptr(instance_v2beta.FieldName_FIELD_NAME_UNSPECIFIED),
want: instance_v2.FieldName_FIELD_NAME_UNSPECIFIED,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := listInstancesBetaSortingColToV2Request(tt.fieldName)
assert.Equal(t, tt.want, got)
})
}
}
+287
View File
@@ -0,0 +1,287 @@
package instancev2
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/backend/v3/api/instance/v2/convert"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"github.com/zitadel/zitadel/internal/api/authz"
filter_v2 "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
filter_v2beta "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
instance_v2 "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
instance_v2beta "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
)
// =================
// v2Beta endpoints
// =================
func AddCustomDomainBeta(ctx context.Context, request *connect.Request[instance_v2beta.AddCustomDomainRequest]) (*connect.Response[instance_v2beta.AddCustomDomainResponse], error) {
addCustomDomainCmd := domain.NewAddInstanceDomainCommand(request.Msg.GetInstanceId(), request.Msg.GetDomain(), domain.DomainTypeCustom)
oidcConfigUpdateCmd := domain.NewOIDCConfigurationUpdate(request.Msg.GetDomain(), authz.GetInstance(ctx).ProjectID(), authz.GetInstance(ctx).ConsoleApplicationID())
batchExec := domain.BatchExecutors(
addCustomDomainCmd,
oidcConfigUpdateCmd,
)
err := domain.Invoke(
ctx,
batchExec,
domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()),
// domain.WithOIDCConfigurationRepo(repository.OIDCConfigurationRepository()),
)
if err != nil {
return nil, err
}
return &connect.Response[instance_v2beta.AddCustomDomainResponse]{
Msg: &instance_v2beta.AddCustomDomainResponse{
// TODO(IAM-Marco): Return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10881
CreationDate: timestamppb.Now(),
},
}, nil
}
func RemoveCustomDomainBeta(ctx context.Context, request *connect.Request[instance_v2beta.RemoveCustomDomainRequest]) (*connect.Response[instance_v2beta.RemoveCustomDomainResponse], error) {
removeCustomDomainCmd := domain.NewRemoveInstanceDomainCommand(request.Msg.GetInstanceId(), request.Msg.GetDomain(), domain.DomainTypeCustom)
err := domain.Invoke(ctx, removeCustomDomainCmd, domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()))
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if removeCustomDomainCmd.DeleteTime != nil {
deletionDate = timestamppb.New(*removeCustomDomainCmd.DeleteTime)
}
return &connect.Response[instance_v2beta.RemoveCustomDomainResponse]{
Msg: &instance_v2beta.RemoveCustomDomainResponse{
// TODO(IAM-Marco): Return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10881
DeletionDate: deletionDate,
},
}, nil
}
func ListCustomDomainsBeta(ctx context.Context, request *connect.Request[instance_v2beta.ListCustomDomainsRequest]) (*connect.Response[instance_v2beta.ListCustomDomainsResponse], error) {
listCustomDomainsQuery := domain.NewListInstanceDomainsQuery(convert.ListCustomDomainsBetaRequestToV2Request(request.Msg))
err := domain.Invoke(ctx, listCustomDomainsQuery, domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()))
if err != nil {
return nil, err
}
customDomains := listCustomDomainsQuery.Result()
return &connect.Response[instance_v2beta.ListCustomDomainsResponse]{
Msg: &instance_v2beta.ListCustomDomainsResponse{
Domains: convert.DomainInstanceDomainListModelToGRPCBetaResponse(customDomains),
Pagination: &filter_v2beta.PaginationResponse{
// TODO(IAM-Marco): return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10955
TotalResult: uint64(len(customDomains)),
AppliedLimit: uint64(request.Msg.GetPagination().GetLimit()),
},
},
}, nil
}
func AddTrustedDomainBeta(ctx context.Context, request *connect.Request[instance_v2beta.AddTrustedDomainRequest]) (*connect.Response[instance_v2beta.AddTrustedDomainResponse], error) {
addTrustedDomainCmd := domain.NewAddInstanceDomainCommand(request.Msg.GetInstanceId(), request.Msg.GetDomain(), domain.DomainTypeTrusted)
err := domain.Invoke(
ctx,
addTrustedDomainCmd,
domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()),
)
if err != nil {
return nil, err
}
return &connect.Response[instance_v2beta.AddTrustedDomainResponse]{
Msg: &instance_v2beta.AddTrustedDomainResponse{
// TODO(IAM-Marco): Return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10881
CreationDate: timestamppb.Now(),
},
}, nil
}
func RemoveTrustedDomainBeta(ctx context.Context, request *connect.Request[instance_v2beta.RemoveTrustedDomainRequest]) (*connect.Response[instance_v2beta.RemoveTrustedDomainResponse], error) {
removeTrustedDomainCmd := domain.NewRemoveInstanceDomainCommand(request.Msg.GetInstanceId(), request.Msg.GetDomain(), domain.DomainTypeTrusted)
err := domain.Invoke(ctx, removeTrustedDomainCmd, domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()))
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if removeTrustedDomainCmd.DeleteTime != nil {
deletionDate = timestamppb.New(*removeTrustedDomainCmd.DeleteTime)
}
return &connect.Response[instance_v2beta.RemoveTrustedDomainResponse]{
Msg: &instance_v2beta.RemoveTrustedDomainResponse{
// TODO(IAM-Marco): Return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10881
DeletionDate: deletionDate,
},
}, nil
}
func ListTrustedDomainsBeta(ctx context.Context, request *connect.Request[instance_v2beta.ListTrustedDomainsRequest]) (*connect.Response[instance_v2beta.ListTrustedDomainsResponse], error) {
listTrustedDomainsQuery := domain.NewListInstanceTrustedDomainsQuery(convert.ListTrustedDomainsBetaRequestToV2Request(request.Msg))
err := domain.Invoke(ctx, listTrustedDomainsQuery, domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()))
if err != nil {
return nil, err
}
trustedDomains := listTrustedDomainsQuery.Result()
return &connect.Response[instance_v2beta.ListTrustedDomainsResponse]{
Msg: &instance_v2beta.ListTrustedDomainsResponse{
TrustedDomain: convert.TrustedDomainInstanceDomainListModelToGRPCBetaResponse(trustedDomains),
Pagination: &filter_v2beta.PaginationResponse{
// TODO(IAM-Marco): return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10955
TotalResult: uint64(len(trustedDomains)),
AppliedLimit: uint64(request.Msg.GetPagination().GetLimit()),
},
},
}, nil
}
// =================
// v2 endpoints
// =================
func AddCustomDomain(ctx context.Context, request *connect.Request[instance_v2.AddCustomDomainRequest]) (*connect.Response[instance_v2.AddCustomDomainResponse], error) {
addCustomDomainCmd := domain.NewAddInstanceDomainCommand(request.Msg.GetInstanceId(), request.Msg.GetCustomDomain(), domain.DomainTypeCustom)
oidcConfigUpdateCmd := domain.NewOIDCConfigurationUpdate(request.Msg.GetCustomDomain(), authz.GetInstance(ctx).ProjectID(), authz.GetInstance(ctx).ConsoleApplicationID())
batchExec := domain.BatchExecutors(
addCustomDomainCmd,
oidcConfigUpdateCmd,
)
err := domain.Invoke(
ctx,
batchExec,
domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()),
// domain.WithOIDCConfigurationRepo(repository.OIDCConfigurationRepository()),
)
if err != nil {
return nil, err
}
return &connect.Response[instance_v2.AddCustomDomainResponse]{
Msg: &instance_v2.AddCustomDomainResponse{
// TODO(IAM-Marco): Return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10881
CreationDate: timestamppb.Now(),
},
}, nil
}
func RemoveCustomDomain(ctx context.Context, request *connect.Request[instance_v2.RemoveCustomDomainRequest]) (*connect.Response[instance_v2.RemoveCustomDomainResponse], error) {
removeCustomDomainCmd := domain.NewRemoveInstanceDomainCommand(request.Msg.GetInstanceId(), request.Msg.GetCustomDomain(), domain.DomainTypeCustom)
err := domain.Invoke(ctx, removeCustomDomainCmd, domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()))
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if removeCustomDomainCmd.DeleteTime != nil {
deletionDate = timestamppb.New(*removeCustomDomainCmd.DeleteTime)
}
return &connect.Response[instance_v2.RemoveCustomDomainResponse]{
Msg: &instance_v2.RemoveCustomDomainResponse{
// TODO(IAM-Marco): Return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10881
DeletionDate: deletionDate,
},
}, nil
}
func ListCustomDomains(ctx context.Context, request *connect.Request[instance_v2.ListCustomDomainsRequest]) (*connect.Response[instance_v2.ListCustomDomainsResponse], error) {
listCustomDomainsQuery := domain.NewListInstanceDomainsQuery(request.Msg)
err := domain.Invoke(ctx, listCustomDomainsQuery, domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()))
if err != nil {
return nil, err
}
customDomains := listCustomDomainsQuery.Result()
return &connect.Response[instance_v2.ListCustomDomainsResponse]{
Msg: &instance_v2.ListCustomDomainsResponse{
Domains: convert.DomainInstanceDomainListModelToGRPCResponse(customDomains),
Pagination: &filter_v2.PaginationResponse{
// TODO(IAM-Marco): return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10955
TotalResult: uint64(len(customDomains)),
AppliedLimit: uint64(request.Msg.GetPagination().GetLimit()),
},
},
}, nil
}
func AddTrustedDomain(ctx context.Context, request *connect.Request[instance_v2.AddTrustedDomainRequest]) (*connect.Response[instance_v2.AddTrustedDomainResponse], error) {
addTrustedDomainCmd := domain.NewAddInstanceDomainCommand(request.Msg.GetInstanceId(), request.Msg.GetTrustedDomain(), domain.DomainTypeTrusted)
err := domain.Invoke(
ctx,
addTrustedDomainCmd,
domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()),
)
if err != nil {
return nil, err
}
return &connect.Response[instance_v2.AddTrustedDomainResponse]{
Msg: &instance_v2.AddTrustedDomainResponse{
// TODO(IAM-Marco): Return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10881
CreationDate: timestamppb.Now(),
},
}, nil
}
func RemoveTrustedDomain(ctx context.Context, request *connect.Request[instance_v2.RemoveTrustedDomainRequest]) (*connect.Response[instance_v2.RemoveTrustedDomainResponse], error) {
removeTrustedDomainCmd := domain.NewRemoveInstanceDomainCommand(request.Msg.GetInstanceId(), request.Msg.GetTrustedDomain(), domain.DomainTypeTrusted)
err := domain.Invoke(ctx, removeTrustedDomainCmd, domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()))
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if removeTrustedDomainCmd.DeleteTime != nil {
deletionDate = timestamppb.New(*removeTrustedDomainCmd.DeleteTime)
}
return &connect.Response[instance_v2.RemoveTrustedDomainResponse]{
Msg: &instance_v2.RemoveTrustedDomainResponse{
// TODO(IAM-Marco): Return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10881
DeletionDate: deletionDate,
},
}, nil
}
func ListTrustedDomains(ctx context.Context, request *connect.Request[instance_v2.ListTrustedDomainsRequest]) (*connect.Response[instance_v2.ListTrustedDomainsResponse], error) {
listTrustedDomainsQuery := domain.NewListInstanceTrustedDomainsQuery(request.Msg)
err := domain.Invoke(ctx, listTrustedDomainsQuery, domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()))
if err != nil {
return nil, err
}
trustedDomains := listTrustedDomainsQuery.Result()
return &connect.Response[instance_v2.ListTrustedDomainsResponse]{
Msg: &instance_v2.ListTrustedDomainsResponse{
TrustedDomain: convert.TrustedDomainInstanceDomainListModelToGRPCResponse(trustedDomains),
Pagination: &filter_v2.PaginationResponse{
// TODO(IAM-Marco): return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10955
TotalResult: uint64(len(trustedDomains)),
AppliedLimit: uint64(request.Msg.GetPagination().GetLimit()),
},
},
}, nil
}
+113 -26
View File
@@ -2,61 +2,62 @@ package instancev2
import (
"context"
"errors"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/backend/v3/api/instance/v2/convert"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"github.com/zitadel/zitadel/internal/zerrors"
filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
filter_v2 "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
filter_v2beta "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
instance_v2 "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
instance_v2beta "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
)
func DeleteInstance(ctx context.Context, request *connect.Request[instance.DeleteInstanceRequest]) (*connect.Response[instance.DeleteInstanceResponse], error) {
// =================
// v2Beta endpoints
// =================
func DeleteInstanceBeta(ctx context.Context, request *connect.Request[instance_v2beta.DeleteInstanceRequest]) (*connect.Response[instance_v2beta.DeleteInstanceResponse], error) {
instanceDeleteCmd := domain.NewDeleteInstanceCommand(request.Msg.GetInstanceId())
err := domain.Invoke(ctx, instanceDeleteCmd, domain.WithInstanceRepo(repository.InstanceRepository()))
if err != nil {
if errors.Is(err, &database.NoRowFoundError{}) {
return nil, zerrors.ThrowNotFound(err, "INST-QVrUwc", "instance not found")
}
return nil, err
}
return &connect.Response[instance.DeleteInstanceResponse]{
Msg: &instance.DeleteInstanceResponse{
var deletionDate *timestamppb.Timestamp
if instanceDeleteCmd.DeleteTime != nil {
deletionDate = timestamppb.New(*instanceDeleteCmd.DeleteTime)
}
return &connect.Response[instance_v2beta.DeleteInstanceResponse]{
Msg: &instance_v2beta.DeleteInstanceResponse{
// TODO(IAM-Marco): Change this with the real update date when OrganizationRepo.Update()
// returns the timestamp
DeletionDate: timestamppb.Now(),
DeletionDate: deletionDate,
},
}, nil
}
func GetInstance(ctx context.Context, request *connect.Request[instance.GetInstanceRequest]) (*connect.Response[instance.GetInstanceResponse], error) {
func GetInstanceBeta(ctx context.Context, request *connect.Request[instance_v2beta.GetInstanceRequest]) (*connect.Response[instance_v2beta.GetInstanceResponse], error) {
instanceGetCmd := domain.NewGetInstanceCommand(request.Msg.GetInstanceId())
err := domain.Invoke(ctx, instanceGetCmd, domain.WithInstanceRepo(repository.InstanceRepository()))
if err != nil {
if errors.Is(err, &database.NoRowFoundError{}) {
return nil, zerrors.ThrowNotFound(err, "INST-QVrUwc", "instance not found")
}
return nil, err
}
return &connect.Response[instance.GetInstanceResponse]{
Msg: &instance.GetInstanceResponse{
Instance: convert.DomainInstanceModelToGRPCResponse(instanceGetCmd.Result()),
return &connect.Response[instance_v2beta.GetInstanceResponse]{
Msg: &instance_v2beta.GetInstanceResponse{
Instance: convert.DomainInstanceModelToGRPCBetaResponse(instanceGetCmd.Result()),
},
}, nil
}
func UpdateInstance(ctx context.Context, request *connect.Request[instance.UpdateInstanceRequest]) (*connect.Response[instance.UpdateInstanceResponse], error) {
func UpdateInstanceBeta(ctx context.Context, request *connect.Request[instance_v2beta.UpdateInstanceRequest]) (*connect.Response[instance_v2beta.UpdateInstanceResponse], error) {
instanceUpdateCmd := domain.NewUpdateInstanceCommand(request.Msg.GetInstanceId(), request.Msg.GetInstanceName())
err := domain.Invoke(ctx, instanceUpdateCmd, domain.WithInstanceRepo(repository.InstanceRepository()))
@@ -65,8 +66,8 @@ func UpdateInstance(ctx context.Context, request *connect.Request[instance.Updat
return nil, err
}
return &connect.Response[instance.UpdateInstanceResponse]{
Msg: &instance.UpdateInstanceResponse{
return &connect.Response[instance_v2beta.UpdateInstanceResponse]{
Msg: &instance_v2beta.UpdateInstanceResponse{
// TODO(IAM-Marco): Change this with the real update date when InstanceRepo.Update()
// returns the timestamp
ChangeDate: timestamppb.Now(),
@@ -74,7 +75,93 @@ func UpdateInstance(ctx context.Context, request *connect.Request[instance.Updat
}, nil
}
func ListInstances(ctx context.Context, request *connect.Request[instance.ListInstancesRequest]) (*connect.Response[instance.ListInstancesResponse], error) {
func ListInstancesBeta(ctx context.Context, request *connect.Request[instance_v2beta.ListInstancesRequest]) (*connect.Response[instance_v2beta.ListInstancesResponse], error) {
instancesListCmd := domain.NewListInstancesCommand(convert.ListInstancesBetaRequestToV2Request(request.Msg))
err := domain.Invoke(
ctx,
instancesListCmd,
domain.WithInstanceRepo(repository.InstanceRepository()),
domain.WithInstanceDomainRepo(repository.InstanceDomainRepository()),
)
if err != nil {
return nil, err
}
instances := instancesListCmd.Result()
return &connect.Response[instance_v2beta.ListInstancesResponse]{
Msg: &instance_v2beta.ListInstancesResponse{
Instances: convert.DomainInstanceListModelToGRPCBetaResponse(instances),
Pagination: &filter_v2beta.PaginationResponse{
// TODO(IAM-Marco): return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10955
TotalResult: uint64(len(instances)),
AppliedLimit: uint64(request.Msg.GetPagination().GetLimit()),
},
},
}, nil
}
// =================
// v2 endpoints
// =================
func DeleteInstance(ctx context.Context, request *connect.Request[instance_v2.DeleteInstanceRequest]) (*connect.Response[instance_v2.DeleteInstanceResponse], error) {
instanceDeleteCmd := domain.NewDeleteInstanceCommand(request.Msg.GetInstanceId())
err := domain.Invoke(ctx, instanceDeleteCmd, domain.WithInstanceRepo(repository.InstanceRepository()))
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if instanceDeleteCmd.DeleteTime != nil {
deletionDate = timestamppb.New(*instanceDeleteCmd.DeleteTime)
}
return &connect.Response[instance_v2.DeleteInstanceResponse]{
Msg: &instance_v2.DeleteInstanceResponse{
// TODO(IAM-Marco): Change this with the real update date when OrganizationRepo.Update()
// returns the timestamp
DeletionDate: deletionDate,
},
}, nil
}
func GetInstance(ctx context.Context, request *connect.Request[instance_v2.GetInstanceRequest]) (*connect.Response[instance_v2.GetInstanceResponse], error) {
instanceGetCmd := domain.NewGetInstanceCommand(request.Msg.GetInstanceId())
err := domain.Invoke(ctx, instanceGetCmd, domain.WithInstanceRepo(repository.InstanceRepository()))
if err != nil {
return nil, err
}
return &connect.Response[instance_v2.GetInstanceResponse]{
Msg: &instance_v2.GetInstanceResponse{
Instance: convert.DomainInstanceModelToGRPCResponse(instanceGetCmd.Result()),
},
}, nil
}
func UpdateInstance(ctx context.Context, request *connect.Request[instance_v2.UpdateInstanceRequest]) (*connect.Response[instance_v2.UpdateInstanceResponse], error) {
instanceUpdateCmd := domain.NewUpdateInstanceCommand(request.Msg.GetInstanceId(), request.Msg.GetInstanceName())
err := domain.Invoke(ctx, instanceUpdateCmd, domain.WithInstanceRepo(repository.InstanceRepository()))
if err != nil {
return nil, err
}
return &connect.Response[instance_v2.UpdateInstanceResponse]{
Msg: &instance_v2.UpdateInstanceResponse{
// TODO(IAM-Marco): Change this with the real update date when InstanceRepo.Update()
// returns the timestamp
ChangeDate: timestamppb.Now(),
},
}, nil
}
func ListInstances(ctx context.Context, request *connect.Request[instance_v2.ListInstancesRequest]) (*connect.Response[instance_v2.ListInstancesResponse], error) {
instancesListCmd := domain.NewListInstancesCommand(request.Msg)
err := domain.Invoke(
@@ -88,10 +175,10 @@ func ListInstances(ctx context.Context, request *connect.Request[instance.ListIn
}
instances := instancesListCmd.Result()
return &connect.Response[instance.ListInstancesResponse]{
Msg: &instance.ListInstancesResponse{
return &connect.Response[instance_v2.ListInstancesResponse]{
Msg: &instance_v2.ListInstancesResponse{
Instances: convert.DomainInstanceListModelToGRPCResponse(instances),
Pagination: &filter.PaginationResponse{
Pagination: &filter_v2.PaginationResponse{
// TODO(IAM-Marco): return correct value. Tracked in https://github.com/zitadel/zitadel/issues/10955
TotalResult: uint64(len(instances)),
AppliedLimit: uint64(request.Msg.GetPagination().GetLimit()),
+35 -59
View File
@@ -17,39 +17,11 @@ import (
v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
)
// import (
// "context"
// =================
// v2Beta endpoints
// =================
// "github.com/zitadel/zitadel/backend/v3/domain"
// "github.com/zitadel/zitadel/pkg/grpc/org/v2"
// )
// func CreateOrg(ctx context.Context, req *org.AddOrganizationRequest) (resp *org.AddOrganizationResponse, err error) {
// cmd := domain.NewAddOrgCommand(
// req.GetName(),
// addOrgAdminToCommand(req.GetAdmins()...)...,
// )
// err = domain.Invoke(ctx, cmd)
// if err != nil {
// return nil, err
// }
// return &org.AddOrganizationResponse{
// OrganizationId: cmd.ID,
// }, nil
// }
// func addOrgAdminToCommand(admins ...*org.AddOrganizationRequest_Admin) []*domain.AddMemberCommand {
// cmds := make([]*domain.AddMemberCommand, len(admins))
// for i, admin := range admins {
// cmds[i] = &domain.AddMemberCommand{
// UserID: admin.GetUserId(),
// Roles: admin.GetRoles(),
// }
// }
// return cmds
// }
func UpdateOrganization(ctx context.Context, request *connect.Request[v2beta_org.UpdateOrganizationRequest]) (*connect.Response[v2beta_org.UpdateOrganizationResponse], error) {
func UpdateOrganizationBeta(ctx context.Context, request *connect.Request[v2beta_org.UpdateOrganizationRequest]) (*connect.Response[v2beta_org.UpdateOrganizationResponse], error) {
orgUpdateCmd := domain.NewUpdateOrgCommand(request.Msg.GetId(), request.Msg.GetName())
// TODO(IAM-Marco) Finish implementation in https://github.com/zitadel/zitadel/issues/10447
@@ -73,30 +45,6 @@ func UpdateOrganization(ctx context.Context, request *connect.Request[v2beta_org
}, nil
}
func ListOrganizations(ctx context.Context, request *connect.Request[v2_org.ListOrganizationsRequest]) (*connect.Response[v2_org.ListOrganizationsResponse], error) {
orgListQuery := domain.NewListOrgsQuery(request.Msg)
err := domain.Invoke(ctx, orgListQuery,
domain.WithOrganizationRepo(repository.OrganizationRepository()),
domain.WithOrganizationDomainRepo(repository.OrganizationDomainRepository()),
)
if err != nil {
return nil, err
}
orgs := convert.DomainOrganizationListModelToGRPCResponse(orgListQuery.Result())
return &connect.Response[v2_org.ListOrganizationsResponse]{
Msg: &v2_org.ListOrganizationsResponse{
Result: orgs,
Details: &object.ListDetails{
// TODO(IAM-Marco): Return correct result once permissions are in place
TotalResult: uint64(len(orgs)),
},
SortingColumn: request.Msg.GetSortingColumn(),
},
}, nil
}
// TODO(IAM-Marco): Remove in V5 (see https://github.com/zitadel/zitadel/issues/10877)
func ListOrganizationsBeta(ctx context.Context, request *connect.Request[v2beta_org.ListOrganizationsRequest]) (*connect.Response[v2beta_org.ListOrganizationsResponse], error) {
orgListQuery := domain.NewListOrgsQuery(convert.OrganizationBetaRequestToV2Request(request.Msg))
@@ -121,7 +69,7 @@ func ListOrganizationsBeta(ctx context.Context, request *connect.Request[v2beta_
}, nil
}
func DeleteOrganization(ctx context.Context, request *connect.Request[v2beta_org.DeleteOrganizationRequest]) (*connect.Response[v2beta_org.DeleteOrganizationResponse], error) {
func DeleteOrganizationBeta(ctx context.Context, request *connect.Request[v2beta_org.DeleteOrganizationRequest]) (*connect.Response[v2beta_org.DeleteOrganizationResponse], error) {
orgDeleteCmd := domain.NewDeleteOrgCommand(request.Msg.GetId())
err := domain.Invoke(ctx, orgDeleteCmd,
@@ -145,7 +93,7 @@ func DeleteOrganization(ctx context.Context, request *connect.Request[v2beta_org
}, nil
}
func DeactivateOrganization(ctx context.Context, request *connect.Request[v2beta_org.DeactivateOrganizationRequest]) (*connect.Response[v2beta_org.DeactivateOrganizationResponse], error) {
func DeactivateOrganizationBeta(ctx context.Context, request *connect.Request[v2beta_org.DeactivateOrganizationRequest]) (*connect.Response[v2beta_org.DeactivateOrganizationResponse], error) {
orgDeactivateCmd := domain.NewDeactivateOrgCommand(request.Msg.GetId())
err := domain.Invoke(ctx, orgDeactivateCmd,
@@ -164,7 +112,7 @@ func DeactivateOrganization(ctx context.Context, request *connect.Request[v2beta
}, nil
}
func ActivateOrganization(ctx context.Context, request *connect.Request[v2beta_org.ActivateOrganizationRequest]) (*connect.Response[v2beta_org.ActivateOrganizationResponse], error) {
func ActivateOrganizationBeta(ctx context.Context, request *connect.Request[v2beta_org.ActivateOrganizationRequest]) (*connect.Response[v2beta_org.ActivateOrganizationResponse], error) {
orgActivateCmd := domain.NewActivateOrgCommand(request.Msg.GetId())
err := domain.Invoke(ctx, orgActivateCmd,
@@ -182,3 +130,31 @@ func ActivateOrganization(ctx context.Context, request *connect.Request[v2beta_o
},
}, nil
}
// =================
// v2 endpoints
// =================
func ListOrganizations(ctx context.Context, request *connect.Request[v2_org.ListOrganizationsRequest]) (*connect.Response[v2_org.ListOrganizationsResponse], error) {
orgListQuery := domain.NewListOrgsQuery(request.Msg)
err := domain.Invoke(ctx, orgListQuery,
domain.WithOrganizationRepo(repository.OrganizationRepository()),
domain.WithOrganizationDomainRepo(repository.OrganizationDomainRepository()),
)
if err != nil {
return nil, err
}
orgs := convert.DomainOrganizationListModelToGRPCResponse(orgListQuery.Result())
return &connect.Response[v2_org.ListOrganizationsResponse]{
Msg: &v2_org.ListOrganizationsResponse{
Result: orgs,
Details: &object.ListDetails{
// TODO(IAM-Marco): Return correct result once permissions are in place
TotalResult: uint64(len(orgs)),
},
SortingColumn: request.Msg.GetSortingColumn(),
},
}, nil
}
+18 -5
View File
@@ -2,7 +2,11 @@ package domain
import (
"context"
"errors"
"strings"
"time"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -20,6 +24,8 @@ type DeleteInstanceCommand struct {
// InstanceName is public for testing purposes
// do not use this field
InstanceName string
DeleteTime *time.Time
}
// RequiresTransaction implements [Transactional].
@@ -46,6 +52,9 @@ func (d *DeleteInstanceCommand) Execute(ctx context.Context, opts *InvokeOpts) (
instanceToDelete, err := instanceRepo.Get(ctx, opts.DB(), database.WithCondition(instanceRepo.IDCondition(d.ID)))
if err != nil {
if errors.Is(err, &database.NoRowFoundError{}) {
return nil
}
return err
}
@@ -58,18 +67,22 @@ func (d *DeleteInstanceCommand) Execute(ctx context.Context, opts *InvokeOpts) (
deletedRows, err := instanceRepo.Delete(ctx, opts.DB(), instanceToDelete.ID)
if err != nil {
return err
if errors.Is(err, &database.NoRowFoundError{}) {
return nil
}
return zerrors.ThrowInternal(err, "DOM-caF4Vs", "Errors.Instance.Delete")
}
if deletedRows > 1 {
err = zerrors.ThrowInternalf(nil, "DOM-Od04Jx", "expecting 1 row deleted, got %d", deletedRows)
return err
return zerrors.ThrowInternal(nil, "DOM-Od04Jx", "Errors.Instsance.DeleteMismatch")
}
if deletedRows < 1 {
err = zerrors.ThrowNotFound(nil, "DOM-daglwD", "instance not found")
return nil
}
d.DeleteTime = gu.Ptr(time.Now())
return err
}
@@ -85,7 +98,7 @@ func (d *DeleteInstanceCommand) Validate(ctx context.Context, opts *InvokeOpts)
}
if authZErr := opts.Permissions.CheckInstancePermission(ctx, InstanceWritePermission); authZErr != nil {
return zerrors.ThrowPermissionDenied(authZErr, "DOM-Yz8f1X", "permission denied")
return zerrors.ThrowPermissionDenied(authZErr, "DOM-Yz8f1X", "Errors.PermissionDenied")
}
return nil
+71 -5
View File
@@ -52,7 +52,7 @@ func TestDeleteInstanceCommand_Validate(t *testing.T) {
return permChecker
},
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-Yz8f1X", "permission denied"),
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-Yz8f1X", "Errors.PermissionDenied"),
inputID: "instance-1",
},
{
@@ -116,6 +116,7 @@ func TestDeleteInstanceCommand_Execute(t *testing.T) {
ctx := authz.NewMockContext("inst-1", "org-1", gofakeit.UUID())
deleteErr := errors.New("delete error")
getErr := errors.New("get error")
noRowFoundErr := database.NewNoRowFoundError(nil)
tests := []struct {
testName string
@@ -126,12 +127,37 @@ func TestDeleteInstanceCommand_Execute(t *testing.T) {
expectedError error
expectedInstanceName string
expectedInstanceDomains []string
expectsDeleteTime bool
}{
{
testName: "when retrieving instance fails should return error",
instanceRepo: func(ctrl *gomock.Controller) domain.InstanceRepository {
instanceRepo := domainmock.NewInstanceRepo(ctrl)
instanceRepo.EXPECT().
LoadDomains().
Times(1).
Return(instanceRepo)
instanceRepo.EXPECT().
Get(
gomock.Any(),
gomock.Any(),
dbmock.QueryOptions(database.WithCondition(
instanceRepo.IDCondition("instance-1"),
)),
).
Times(1).
Return(nil, noRowFoundErr)
return instanceRepo
},
inputInstanceID: "instance-1",
},
{
testName: "when retrieving instance fails with no row found should return no error",
instanceRepo: func(ctrl *gomock.Controller) domain.InstanceRepository {
instanceRepo := domainmock.NewInstanceRepo(ctrl)
instanceRepo.EXPECT().
LoadDomains().
Times(1).
@@ -188,7 +214,46 @@ func TestDeleteInstanceCommand_Execute(t *testing.T) {
return instanceRepo
},
inputInstanceID: "instance-1",
expectedError: deleteErr,
expectedError: zerrors.ThrowInternal(deleteErr, "DOM-caF4Vs", "Errors.Instance.Delete"),
expectedInstanceName: "My instance 1",
expectedInstanceDomains: []string{"d1.example.com", "d2.example.com", "d3.example.com"},
},
{
testName: "when delete instance fails with no row found should return no error",
instanceRepo: func(ctrl *gomock.Controller) domain.InstanceRepository {
instanceRepo := domainmock.NewInstanceRepo(ctrl)
instanceRepo.EXPECT().
LoadDomains().
Times(1).
Return(instanceRepo)
instanceRepo.EXPECT().
Get(
gomock.Any(),
gomock.Any(),
dbmock.QueryOptions(database.WithCondition(
instanceRepo.IDCondition("instance-1"),
)),
).
Times(1).
Return(&domain.Instance{
ID: "instance-1",
Name: "My instance 1",
Domains: []*domain.InstanceDomain{
{Domain: "d1.example.com"},
{Domain: "d2.example.com"},
{Domain: "d3.example.com"},
},
}, nil)
instanceRepo.EXPECT().
Delete(gomock.Any(), gomock.Any(), "instance-1").
Times(1).
Return(int64(0), database.NewNoRowFoundError(nil))
return instanceRepo
},
inputInstanceID: "instance-1",
expectedInstanceName: "My instance 1",
expectedInstanceDomains: []string{"d1.example.com", "d2.example.com", "d3.example.com"},
},
@@ -228,12 +293,12 @@ func TestDeleteInstanceCommand_Execute(t *testing.T) {
return instanceRepo
},
inputInstanceID: "instance-1",
expectedError: zerrors.ThrowInternalf(nil, "DOM-Od04Jx", "expecting 1 row deleted, got %d", 2),
expectedError: zerrors.ThrowInternal(nil, "DOM-Od04Jx", "Errors.Instsance.DeleteMismatch"),
expectedInstanceName: "My instance 1",
expectedInstanceDomains: []string{"d1.example.com", "d2.example.com", "d3.example.com"},
},
{
testName: "when no rows deleted should return not found error",
testName: "when no rows deleted should return no error",
instanceRepo: func(ctrl *gomock.Controller) domain.InstanceRepository {
instanceRepo := domainmock.NewInstanceRepo(ctrl)
@@ -268,7 +333,6 @@ func TestDeleteInstanceCommand_Execute(t *testing.T) {
return instanceRepo
},
inputInstanceID: "instance-1",
expectedError: zerrors.ThrowNotFound(nil, "DOM-daglwD", "instance not found"),
expectedInstanceName: "My instance 1",
expectedInstanceDomains: []string{"d1.example.com", "d2.example.com", "d3.example.com"},
},
@@ -310,6 +374,7 @@ func TestDeleteInstanceCommand_Execute(t *testing.T) {
inputInstanceID: "instance-1",
expectedInstanceName: "My instance 1",
expectedInstanceDomains: []string{"d1.example.com", "d2.example.com", "d3.example.com"},
expectsDeleteTime: true,
},
}
@@ -335,6 +400,7 @@ func TestDeleteInstanceCommand_Execute(t *testing.T) {
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedInstanceName, d.InstanceName)
assert.ElementsMatch(t, tc.expectedInstanceDomains, d.InstanceDomains)
assert.Equal(t, tc.expectsDeleteTime, d.DeleteTime != nil && !d.DeleteTime.IsZero())
})
}
}
+2
View File
@@ -51,6 +51,8 @@ type instanceDomainConditions interface {
PrimaryKeyCondition(domain string) database.Condition
// TypeCondition returns a filter for the type field.
TypeCondition(typ DomainType) database.Condition
// IsGeneratedCondition returns a filter for the is_generated field
IsGeneratedCondition(isGenerated bool) database.Condition
}
type instanceDomainChanges interface {
+123
View File
@@ -0,0 +1,123 @@
package domain
import (
"context"
"errors"
"regexp"
"strings"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/zerrors"
)
var allowedDomainCharacters = regexp.MustCompile(`^[a-zA-Z0-9\\.\\-]+$`)
const maxDomainNameLength = 253
type AddInstanceDomainCommand struct {
InstanceID string `json:"instance_id"`
DomainName string `json:"domain_name"`
DType DomainType `json:"domain_type"`
}
// RequiresTransaction implements [Transactional].
func (a *AddInstanceDomainCommand) RequiresTransaction() {}
func NewAddInstanceDomainCommand(instanceID, domainName string, domainType DomainType) *AddInstanceDomainCommand {
return &AddInstanceDomainCommand{
InstanceID: instanceID,
DomainName: domainName,
DType: domainType,
}
}
// Events implements [Commander].
func (a *AddInstanceDomainCommand) Events(ctx context.Context, _ *InvokeOpts) ([]eventstore.Command, error) {
var event eventstore.Command
switch a.DType {
case DomainTypeCustom:
event = instance.NewDomainAddedEvent(ctx, &instance.NewAggregate(a.InstanceID).Aggregate, a.DomainName, false)
case DomainTypeTrusted:
event = instance.NewTrustedDomainAddedEvent(ctx, &instance.NewAggregate(a.InstanceID).Aggregate, a.DomainName)
default:
return nil, nil
}
return []eventstore.Command{
event,
}, nil
}
// Execute implements [Commander].
func (a *AddInstanceDomainCommand) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
instanceDomainRepo := opts.instanceDomainRepo
var isPrimary, isGenerated *bool
if a.DType == DomainTypeCustom {
isPrimary, isGenerated = gu.Ptr(false), gu.Ptr(false)
}
err = instanceDomainRepo.Add(ctx, opts.DB(), &AddInstanceDomain{
InstanceID: a.InstanceID,
Domain: a.DomainName,
IsPrimary: isPrimary,
IsGenerated: isGenerated,
Type: a.DType,
})
if err != nil {
return zerrors.ThrowInternal(err, "DOM-uSCVn3", "Errors.Instance.Domain.Add")
}
return nil
}
// String implements [Commander].
func (a *AddInstanceDomainCommand) String() string {
return "AddInstanceDomainCommand"
}
// Validate implements [Commander].
func (a *AddInstanceDomainCommand) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
if a.DomainName = strings.TrimSpace(a.DomainName); a.DomainName == "" || len(a.DomainName) > maxDomainNameLength {
return zerrors.ThrowInvalidArgument(nil, "DOM-jieuM8", "Errors.Invalid.Argument")
}
if a.InstanceID = strings.TrimSpace(a.InstanceID); a.InstanceID == "" {
return zerrors.ThrowInvalidArgument(nil, "DOM-YaUBp5", "Errors.Invalid.Argument")
}
if !allowedDomainCharacters.MatchString(a.DomainName) {
return zerrors.ThrowInvalidArgument(nil, "DOM-98VcSQ", "Errors.Instance.Domain.InvalidCharacter")
}
// TODO(IAM-Marco): Do we want to restrict to the instance in context?
if a.InstanceID != authz.GetInstance(ctx).InstanceID() {
return zerrors.ThrowInvalidArgument(nil, "DOM-x01cai", "Errors.Invalid.Argument")
}
if authZErr := opts.Permissions.CheckInstancePermission(ctx, DomainWritePermission); authZErr != nil {
return zerrors.ThrowPermissionDenied(authZErr, "DOM-c83vPX", "Errors.PermissionDenied")
}
domainRepo := opts.instanceDomainRepo
_, err = domainRepo.Get(ctx, opts.DB(), database.WithCondition(domainRepo.DomainCondition(database.TextOperationEqual, a.DomainName)))
if err == nil {
return zerrors.ThrowAlreadyExists(nil, "DOM-CvQ8tf", "Errors.Instance.Domain.AlreadyExists")
}
if !errors.Is(err, &database.NoRowFoundError{}) {
return zerrors.ThrowInternal(err, "DOM-LrTy2z", "Errors.Instance.Domain.Get")
}
return nil
}
var (
_ Commander = (*AddInstanceDomainCommand)(nil)
_ Transactional = (*AddInstanceDomainCommand)(nil)
)
@@ -0,0 +1,316 @@
package domain_test
import (
"errors"
"testing"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/backend/v3/domain"
domainmock "github.com/zitadel/zitadel/backend/v3/domain/mock"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
noopdb "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/noop"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestAddInstanceDomainCommand_Validate(t *testing.T) {
t.Parallel()
getErr := errors.New("get error")
permissionErr := errors.New("permission error")
tt := []struct {
testName string
instanceDomainRepo func(ctrl *gomock.Controller) domain.InstanceDomainRepository
permissionChecker func(ctrl *gomock.Controller) domain.PermissionChecker
inputInstanceID string
inputDomainName string
expectedError error
}{
{
testName: "when no name should return invalid argument error",
inputInstanceID: " ",
inputDomainName: " ",
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-jieuM8", "Errors.Invalid.Argument"),
},
{
testName: "when name too long should return invalid argument error",
inputInstanceID: " ",
inputDomainName: "domain.is.to.loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-jieuM8", "Errors.Invalid.Argument"),
},
{
testName: "when no ID should return invalid argument error",
inputInstanceID: " ",
inputDomainName: " domain.name",
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-YaUBp5", "Errors.Invalid.Argument"),
},
{
testName: "when domain name contains invalid characters should return invalid argument error",
inputInstanceID: "instance-1 ",
inputDomainName: "?",
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-98VcSQ", "Errors.Instance.Domain.InvalidCharacter"),
},
{
testName: "when input instance ID doesn't match instance in context should return invalid argument error",
inputInstanceID: " instance-2",
inputDomainName: "valid.domain",
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-x01cai", "Errors.Invalid.Argument"),
},
{
testName: "when user is missing permission should return permission denied",
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
permChecker := domainmock.NewMockPermissionChecker(ctrl)
permChecker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainWritePermission).
Times(1).
Return(permissionErr)
return permChecker
},
inputInstanceID: "instance-1",
inputDomainName: "valid.domain",
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-c83vPX", "Errors.PermissionDenied"),
},
{
testName: "when retrieving instance domain fails with generic error should return error",
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
permChecker := domainmock.NewMockPermissionChecker(ctrl)
permChecker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainWritePermission).
Times(1).
Return(nil)
return permChecker
},
instanceDomainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewInstancesDomainRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
database.WithCondition(
repo.DomainCondition(database.TextOperationEqual, "valid.domain"),
),
)).
Times(1).
Return(nil, getErr)
return repo
},
inputInstanceID: "instance-1",
inputDomainName: "valid.domain",
expectedError: zerrors.ThrowInternal(getErr, "DOM-LrTy2z", "Errors.Instance.Domain.Get"),
},
{
testName: "when retrieving instance domain succeeds should return already exists error",
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
permChecker := domainmock.NewMockPermissionChecker(ctrl)
permChecker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainWritePermission).
Times(1).
Return(nil)
return permChecker
},
instanceDomainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewInstancesDomainRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
database.WithCondition(
repo.DomainCondition(database.TextOperationEqual, "valid.domain"),
),
)).
Times(1).
Return(&domain.InstanceDomain{}, nil)
return repo
},
inputInstanceID: "instance-1",
inputDomainName: "valid.domain",
expectedError: zerrors.ThrowAlreadyExists(nil, "DOM-CvQ8tf", "Errors.Instance.Domain.AlreadyExists"),
},
{
testName: "when retrieving instance domain fails with not found error validation should succeed",
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
permChecker := domainmock.NewMockPermissionChecker(ctrl)
permChecker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainWritePermission).
Times(1).
Return(nil)
return permChecker
},
instanceDomainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewInstancesDomainRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
database.WithCondition(
repo.DomainCondition(database.TextOperationEqual, "valid.domain"),
),
)).
Times(1).
Return(nil, &database.NoRowFoundError{})
return repo
},
inputInstanceID: "instance-1",
inputDomainName: "valid.domain",
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
ctx := authz.NewMockContext("instance-1", "org-1", "")
ctrl := gomock.NewController(t)
cmd := domain.NewAddInstanceDomainCommand(tc.inputInstanceID, tc.inputDomainName, domain.DomainTypeCustom)
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
}
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
if tc.instanceDomainRepo != nil {
domain.WithInstanceDomainRepo(tc.instanceDomainRepo(ctrl))(opts)
}
if tc.permissionChecker != nil {
opts.Permissions = tc.permissionChecker(ctrl)
}
err := cmd.Validate(ctx, opts)
assert.Equal(t, tc.expectedError, err)
})
}
}
func TestAddInstanceDomainCommand_Events(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
domainType domain.DomainType
expectedAggregateID string
expectedEventType eventstore.Command
}{
{
testName: "trusted domain",
domainType: domain.DomainTypeTrusted,
expectedAggregateID: "instance-1",
expectedEventType: &instance.TrustedDomainAddedEvent{},
},
{
testName: "custom domain",
domainType: domain.DomainTypeCustom,
expectedAggregateID: "instance-1",
expectedEventType: &instance.DomainAddedEvent{},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
instanceID := "instance-1"
domainName := "test.domain"
ctx := authz.NewMockContext(instanceID, "org-1", "")
cmd := domain.NewAddInstanceDomainCommand(instanceID, domainName, tc.domainType)
// When
events, err := cmd.Events(ctx, &domain.InvokeOpts{})
// Then
require.NoError(t, err)
require.Len(t, events, 1)
require.NotNil(t, events[0].Aggregate())
assert.Equal(t, instanceID, events[0].Aggregate().ID)
require.IsType(t, tc.expectedEventType, events[0])
switch asserted := events[0].(type) {
case *instance.DomainAddedEvent:
assert.Equal(t, domainName, asserted.Domain)
assert.False(t, asserted.Generated)
case *instance.TrustedDomainAddedEvent:
assert.Equal(t, domainName, asserted.Domain)
}
})
}
}
func TestAddInstanceDomainCommand_Execute(t *testing.T) {
t.Parallel()
addErr := errors.New("add error")
tt := []struct {
testName string
instanceDomainRepo func(ctrl *gomock.Controller) domain.InstanceDomainRepository
inputDomainType domain.DomainType
expectedError error
}{
{
testName: "when adding domain succeeds should return nil",
inputDomainType: domain.DomainTypeCustom,
instanceDomainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewInstancesDomainRepo(ctrl)
repo.EXPECT().
Add(gomock.Any(), gomock.Any(), &domain.AddInstanceDomain{
InstanceID: "instance-1",
Domain: "valid.domain",
IsPrimary: gu.Ptr(false),
IsGenerated: gu.Ptr(false),
Type: domain.DomainTypeCustom,
}).
Times(1).
Return(nil)
return repo
},
},
{
testName: "when adding domain fails should return error",
inputDomainType: domain.DomainTypeTrusted,
instanceDomainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewInstancesDomainRepo(ctrl)
repo.EXPECT().
Add(gomock.Any(), gomock.Any(), &domain.AddInstanceDomain{
InstanceID: "instance-1",
Domain: "valid.domain",
Type: domain.DomainTypeTrusted,
}).
Times(1).
Return(addErr)
return repo
},
expectedError: zerrors.ThrowInternal(addErr, "DOM-uSCVn3", "Errors.Instance.Domain.Add"),
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
ctx := authz.NewMockContext("instance-1", "org-1", "")
ctrl := gomock.NewController(t)
cmd := domain.NewAddInstanceDomainCommand("instance-1", "valid.domain", tc.inputDomainType)
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
}
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
if tc.instanceDomainRepo != nil {
domain.WithInstanceDomainRepo(tc.instanceDomainRepo(ctrl))(opts)
}
// When
err := cmd.Execute(ctx, opts)
// Then
assert.Equal(t, tc.expectedError, err)
})
}
}
+134
View File
@@ -0,0 +1,134 @@
package domain
import (
"context"
"strings"
"github.com/zitadel/zitadel/backend/v3/api/object"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/zerrors"
instance_v2 "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
)
type ListInstanceDomainsQuery struct {
Request *instance_v2.ListCustomDomainsRequest
toReturn []*InstanceDomain
}
var _ Querier[[]*InstanceDomain] = (*ListInstanceDomainsQuery)(nil)
func NewListInstanceDomainsQuery(inputRequest *instance_v2.ListCustomDomainsRequest) *ListInstanceDomainsQuery {
return &ListInstanceDomainsQuery{
Request: inputRequest,
}
}
// Result implements [Querier].
func (l *ListInstanceDomainsQuery) Result() []*InstanceDomain {
return l.toReturn
}
// Execute implements [Querier].
func (l *ListInstanceDomainsQuery) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
domainRepo := opts.instanceDomainRepo
sorting := l.Sorting(domainRepo)
limit, offset := l.Pagination()
conds, err := l.conditions(domainRepo)
if err != nil {
return err
}
instances, err := domainRepo.List(ctx, opts.DB(), conds, sorting, limit, offset)
if err != nil {
return zerrors.ThrowInternal(err, "DOM-ubaPNU", "Errors.Instance.Domain.List")
}
l.toReturn = instances
return nil
}
func (l *ListInstanceDomainsQuery) Pagination() (database.QueryOption, database.QueryOption) {
return database.WithLimit(l.Request.GetPagination().GetLimit()),
database.WithOffset(uint32(l.Request.GetPagination().GetOffset()))
}
func (l *ListInstanceDomainsQuery) conditions(domainRepo InstanceDomainRepository) (database.QueryOption, error) {
conditions := make([]database.Condition, len(l.Request.GetFilters()))
for i, query := range l.Request.GetFilters() {
switch assertedType := query.GetFilter().(type) {
case *instance_v2.CustomDomainFilter_DomainFilter:
method, err := object.TextQueryMethodToTextOperation(assertedType.DomainFilter.GetMethod())
if err != nil {
return nil, err
}
conditions[i] = domainRepo.DomainCondition(
method,
assertedType.DomainFilter.GetDomain(),
)
case *instance_v2.CustomDomainFilter_GeneratedFilter:
conditions[i] = domainRepo.IsGeneratedCondition(assertedType.GeneratedFilter)
case *instance_v2.CustomDomainFilter_PrimaryFilter:
conditions[i] = domainRepo.IsPrimaryCondition(assertedType.PrimaryFilter)
default:
return nil, zerrors.ThrowInvalidArgument(NewUnexpectedQueryTypeError(assertedType), "DOM-CjM93P", "List.Query.Invalid")
}
}
conditions = append(conditions, domainRepo.TypeCondition(DomainTypeCustom))
return database.WithCondition(database.And(conditions...)), nil
}
func (l *ListInstanceDomainsQuery) Sorting(domainRepo InstanceDomainRepository) database.QueryOption {
var sortingCol database.Column
switch l.Request.GetSortingColumn() {
case instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE, instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED:
sortingCol = domainRepo.CreatedAtColumn()
case instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN:
sortingCol = domainRepo.DomainColumn()
case instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED:
sortingCol = domainRepo.IsGeneratedColumn()
case instance_v2.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY:
sortingCol = domainRepo.IsPrimaryColumn()
default:
return func(opts *database.QueryOpts) {}
}
if l.Request.GetPagination().GetAsc() {
return database.WithOrderByAscending(sortingCol)
}
return database.WithOrderByDescending(sortingCol)
}
// String implements [Querier].
func (l *ListInstanceDomainsQuery) String() string {
return "ListInstanceDomainsQuery"
}
// Validate implements [Querier].
func (l *ListInstanceDomainsQuery) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
instanceID := strings.TrimSpace(l.Request.GetInstanceId())
if instanceID == "" || instanceID == authz.GetInstance(ctx).InstanceID() {
return l.checkDomainPerms(ctx, opts)
}
// TODO(IAM-Marco): This is wrong, as it should check the permission for the input instance and not the one in context
if authZErr := opts.Permissions.CheckInstancePermission(ctx, InstanceReadPermission); authZErr != nil {
return zerrors.ThrowPermissionDenied(authZErr, "DOM-yN7oCp", "Errors.PermissionDenied")
}
return l.checkDomainPerms(ctx, opts)
}
func (l *ListInstanceDomainsQuery) checkDomainPerms(ctx context.Context, opts *InvokeOpts) error {
if authZErr := opts.Permissions.CheckInstancePermission(ctx, DomainReadPermission); authZErr != nil {
return zerrors.ThrowPermissionDenied(authZErr, "DOM-RyCEyr", "Errors.PermissionDenied")
}
return nil
}
@@ -0,0 +1,361 @@
package domain_test
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/backend/v3/domain"
domainmock "github.com/zitadel/zitadel/backend/v3/domain/mock"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
noopdb "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/noop"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/zerrors"
filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)
func TestListInstanceDomainsQuery_Validate(t *testing.T) {
t.Parallel()
permissionErr := errors.New("permission error")
tt := []struct {
name string
request *instance.ListCustomDomainsRequest
permissionChecker func(ctrl *gomock.Controller) domain.PermissionChecker
expectedError error
}{
{
name: "when missing domain read permission should return permission denied (empty instance id)",
request: &instance.ListCustomDomainsRequest{InstanceId: " "},
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
checker := domainmock.NewMockPermissionChecker(ctrl)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainReadPermission).
Times(1).
Return(permissionErr)
return checker
},
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-RyCEyr", "Errors.PermissionDenied"),
},
{
name: "when valid permissions should return no error (ctx instance same as input)",
request: &instance.ListCustomDomainsRequest{InstanceId: "instance-1"},
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
checker := domainmock.NewMockPermissionChecker(ctrl)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainReadPermission).
Times(1).
Return(nil)
return checker
},
},
{
name: "when input instance doesn't match context should check instance and return error",
request: &instance.ListCustomDomainsRequest{
InstanceId: "different-instance",
},
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
checker := domainmock.NewMockPermissionChecker(ctrl)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.InstanceReadPermission).
Times(1).
Return(permissionErr)
return checker
},
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-yN7oCp", "Errors.PermissionDenied"),
},
{
name: "when input instance doesn't match context should check instance and succeed then check domain and fail",
request: &instance.ListCustomDomainsRequest{
InstanceId: "different-instance",
},
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
checker := domainmock.NewMockPermissionChecker(ctrl)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.InstanceReadPermission).
Times(1).
Return(nil)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainReadPermission).
Times(1).
Return(permissionErr)
return checker
},
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-RyCEyr", "Errors.PermissionDenied"),
},
{
name: "when input instance doesn't match context should check instance and succeed then check domain and succeed",
request: &instance.ListCustomDomainsRequest{
InstanceId: "different-instance",
},
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
checker := domainmock.NewMockPermissionChecker(ctrl)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.InstanceReadPermission).
Times(1).
Return(nil)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainReadPermission).
Times(1).
Return(nil)
return checker
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
ctx := authz.NewMockContext("instance-1", "org-1", "")
query := domain.NewListInstanceDomainsQuery(tc.request)
err := query.Validate(ctx, &domain.InvokeOpts{
Permissions: tc.permissionChecker(ctrl),
})
if tc.expectedError != nil {
assert.Error(t, err)
assert.Equal(t, tc.expectedError, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestListInstanceDomainsQuery_Execute(t *testing.T) {
t.Parallel()
listErr := errors.New("list mock error")
tt := []struct {
name string
request *instance.ListCustomDomainsRequest
repo func(ctrl *gomock.Controller) domain.InstanceDomainRepository
expectedError error
expectedDomains []*domain.InstanceDomain
}{
{
name: "when parsing conditions fails should return invalid argument error",
request: &instance.ListCustomDomainsRequest{
Pagination: &filter.PaginationRequest{Limit: 2, Offset: 1, Asc: false},
Filters: []*instance.CustomDomainFilter{
{Filter: &instance.CustomDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{
Domain: "test.domain",
Method: 99,
},
}},
},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN,
},
repo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewInstancesDomainRepo(ctrl)
return repo
},
expectedError: zerrors.ThrowInvalidArgument(nil, "OBJ-iBRBVe", "invalid text query method"),
},
{
name: "when listing domains fails should return error",
request: &instance.ListCustomDomainsRequest{
Pagination: &filter.PaginationRequest{Limit: 2, Offset: 1, Asc: false},
Filters: []*instance.CustomDomainFilter{
{Filter: &instance.CustomDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{
Domain: "test.domain",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS,
},
}},
},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN,
},
repo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewInstancesDomainRepo(ctrl)
repo.EXPECT().
List(gomock.Any(), gomock.Any(),
dbmock.QueryOptions(
database.WithCondition(database.And(
repo.DomainCondition(database.TextOperationContains, "test.domain"),
repo.TypeCondition(domain.DomainTypeCustom),
)),
),
dbmock.QueryOptions(database.WithOrderBy(database.OrderDirectionDesc, repo.DomainColumn())),
dbmock.QueryOptions(database.WithLimit(2)),
dbmock.QueryOptions(database.WithOffset(1)),
).
Return(nil, listErr)
return repo
},
expectedError: zerrors.ThrowInternal(listErr, "DOM-ubaPNU", "Errors.Instance.Domain.List"),
},
{
name: "when listing domains succeeds should return domains",
request: &instance.ListCustomDomainsRequest{
Filters: []*instance.CustomDomainFilter{
{Filter: &instance.CustomDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{
Domain: "test.domain",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS,
},
}},
{Filter: &instance.CustomDomainFilter_GeneratedFilter{
GeneratedFilter: true,
}},
{Filter: &instance.CustomDomainFilter_PrimaryFilter{
PrimaryFilter: true,
}},
},
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN,
},
repo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewInstancesDomainRepo(ctrl)
domains := []*domain.InstanceDomain{
{Domain: "test1.domain"},
{Domain: "test2.domain"},
}
repo.EXPECT().
List(gomock.Any(), gomock.Any(),
dbmock.QueryOptions(
database.WithCondition(database.And(
repo.DomainCondition(database.TextOperationContains, "test.domain"),
repo.IsGeneratedCondition(true),
repo.IsPrimaryCondition(true),
repo.TypeCondition(domain.DomainTypeCustom),
)),
),
dbmock.QueryOptions(database.WithOrderBy(database.OrderDirectionDesc, repo.DomainColumn())),
dbmock.QueryOptions(database.WithLimit(0)),
dbmock.QueryOptions(database.WithOffset(0)),
).
Return(domains, nil)
return repo
},
expectedDomains: []*domain.InstanceDomain{
{Domain: "test1.domain"},
{Domain: "test2.domain"},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
}
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
if tc.repo != nil {
instDomain := tc.repo(ctrl)
domain.WithInstanceDomainRepo(instDomain)(opts)
}
query := domain.NewListInstanceDomainsQuery(tc.request)
err := query.Execute(t.Context(), opts)
if tc.expectedError != nil {
assert.Error(t, err)
assert.Equal(t, tc.expectedError, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedDomains, query.Result())
}
})
}
}
func TestListInstanceDomainsQuery_Sorting(t *testing.T) {
t.Parallel()
tt := []struct {
name string
request *instance.ListCustomDomainsRequest
expectedSortingDirection database.OrderDirection
expectedOrderBy database.Columns
}{
{
name: "sort by creation date DESC",
request: &instance.ListCustomDomainsRequest{
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
},
expectedSortingDirection: database.OrderDirectionDesc,
expectedOrderBy: database.Columns{database.NewColumn("instance_domains", "created_at")},
},
{
name: "sort by creation date ASC",
request: &instance.ListCustomDomainsRequest{
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE,
Pagination: &filter.PaginationRequest{Asc: true},
},
expectedSortingDirection: database.OrderDirectionAsc,
expectedOrderBy: database.Columns{database.NewColumn("instance_domains", "created_at")},
},
{
name: "sort by domain DESC",
request: &instance.ListCustomDomainsRequest{
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN,
},
expectedSortingDirection: database.OrderDirectionDesc,
expectedOrderBy: database.Columns{database.NewColumn("instance_domains", "domain")},
},
{
name: "sort by domain ASC",
request: &instance.ListCustomDomainsRequest{
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN,
Pagination: &filter.PaginationRequest{Asc: true},
},
expectedSortingDirection: database.OrderDirectionAsc,
expectedOrderBy: database.Columns{database.NewColumn("instance_domains", "domain")},
},
{
name: "sort by generated DESC",
request: &instance.ListCustomDomainsRequest{
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED,
},
expectedSortingDirection: database.OrderDirectionDesc,
expectedOrderBy: database.Columns{database.NewColumn("instance_domains", "is_generated")},
},
{
name: "sort by primary DESC",
request: &instance.ListCustomDomainsRequest{
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY,
},
expectedSortingDirection: database.OrderDirectionDesc,
expectedOrderBy: database.Columns{database.NewColumn("instance_domains", "is_primary")},
},
{
name: "unspecified field",
request: &instance.ListCustomDomainsRequest{
SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED,
},
expectedSortingDirection: database.OrderDirectionDesc,
expectedOrderBy: database.Columns{database.NewColumn("instance_domains", "created_at")},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Given
mockCtrl := gomock.NewController(t)
mockRepo := domainmock.NewInstancesDomainRepo(mockCtrl)
cmd := domain.NewListInstanceDomainsQuery(tc.request)
opts := &database.QueryOpts{}
// Test
queryOpt := cmd.Sorting(mockRepo)
queryOpt(opts)
// Verify
assert.Equal(t, tc.expectedOrderBy, opts.OrderBy)
assert.Equal(t, tc.expectedSortingDirection, opts.Ordering)
})
}
}
+118
View File
@@ -0,0 +1,118 @@
package domain
import (
"context"
"errors"
"strings"
"time"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/zerrors"
)
type RemoveInstanceDomainCommand struct {
InstanceID string `json:"instance_id"`
DomainName string `json:"domain_name"`
DType DomainType `json:"domain_type"`
DeleteTime *time.Time `json:"delete_date"`
}
// RequiresTransaction implements [Transactional].
func (r *RemoveInstanceDomainCommand) RequiresTransaction() {}
func NewRemoveInstanceDomainCommand(instanceID, domainName string, domainType DomainType) *RemoveInstanceDomainCommand {
return &RemoveInstanceDomainCommand{
InstanceID: instanceID,
DomainName: domainName,
DType: domainType,
}
}
// Events implements [Commander].
func (r *RemoveInstanceDomainCommand) Events(ctx context.Context, _ *InvokeOpts) ([]eventstore.Command, error) {
var toReturn eventstore.Command
switch r.DType {
case DomainTypeCustom:
toReturn = instance.NewDomainRemovedEvent(ctx, &instance.NewAggregate(r.InstanceID).Aggregate, r.DomainName)
case DomainTypeTrusted:
toReturn = instance.NewTrustedDomainRemovedEvent(ctx, &instance.NewAggregate(r.InstanceID).Aggregate, r.DomainName)
default:
return nil, nil
}
return []eventstore.Command{toReturn}, nil
}
// Execute implements [Commander].
func (r *RemoveInstanceDomainCommand) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
instanceRepo := opts.instanceDomainRepo
deletedRows, err := instanceRepo.Remove(ctx, opts.DB(), database.And(instanceRepo.InstanceIDCondition(r.InstanceID), instanceRepo.PrimaryKeyCondition(r.DomainName)))
if err != nil {
return zerrors.ThrowInternal(err, "DOM-KH7AuJ", "Errors.Instance.Domain.Remove")
}
if deletedRows > 1 {
return zerrors.ThrowInternal(nil, "DOM-XSCnJB", "Errors.Instsance.Domain.DeleteMismatch")
}
if deletedRows < 1 {
return nil
}
r.DeleteTime = gu.Ptr(time.Now())
return err
}
// String implements [Commander].
func (r *RemoveInstanceDomainCommand) String() string {
return "RemoveInstanceDomainCommand"
}
// Validate implements [Commander].
func (r *RemoveInstanceDomainCommand) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
if r.DomainName = strings.TrimSpace(r.DomainName); r.DomainName == "" || len(r.DomainName) > 253 {
return zerrors.ThrowInvalidArgument(nil, "DOM-PLpYix", "Errors.Invalid.Argument")
}
if r.InstanceID = strings.TrimSpace(r.InstanceID); r.InstanceID == "" {
return zerrors.ThrowInvalidArgument(nil, "DOM-VSsTTf", "Errors.Invalid.Argument")
}
if !allowedDomainCharacters.MatchString(r.DomainName) {
return zerrors.ThrowInvalidArgument(nil, "DOM-dYD5I7", "Errors.Instance.Domain.InvalidCharacter")
}
if r.InstanceID != authz.GetInstance(ctx).InstanceID() {
return zerrors.ThrowInvalidArgument(nil, "DOM-83FUdY", "Errors.Invalid.Argument")
}
if authZErr := opts.Permissions.CheckInstancePermission(ctx, DomainWritePermission); authZErr != nil {
return zerrors.ThrowPermissionDenied(authZErr, "DOM-eroxID", "Errors.PermissionDenied")
}
domainRepo := opts.instanceDomainRepo
d, err := domainRepo.Get(ctx, opts.DB(), database.WithCondition(domainRepo.DomainCondition(database.TextOperationEqual, r.DomainName)))
if err != nil {
if errors.Is(err, &database.NoRowFoundError{}) {
return nil
}
return zerrors.ThrowInternal(err, "DOM-Zvv1fi", "Errors.Instance.Domain.Get")
}
if d.Type == DomainTypeCustom && d.IsGenerated != nil && *d.IsGenerated {
return zerrors.ThrowPreconditionFailed(nil, "DOM-cSfCVG", "Errors.Instance.Domain.GeneratedNotRemovable")
}
return nil
}
var (
_ Commander = (*RemoveInstanceDomainCommand)(nil)
_ Transactional = (*RemoveInstanceDomainCommand)(nil)
)
@@ -0,0 +1,434 @@
package domain_test
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/backend/v3/domain"
domainmock "github.com/zitadel/zitadel/backend/v3/domain/mock"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
noopdb "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/noop"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestRemoveInstanceDomainCommand_Validate(t *testing.T) {
t.Parallel()
getErr := errors.New("get error")
noRowFoundErr := &database.NoRowFoundError{}
permissionErr := errors.New("permission error")
tt := []struct {
testName string
domainRepo func(ctrl *gomock.Controller) domain.InstanceDomainRepository
permissionChecker func(ctrl *gomock.Controller) domain.PermissionChecker
inputInstanceID string
inputDomainName string
expectedError error
}{
{
testName: "when no instance ID should return invalid argument error",
inputInstanceID: "",
inputDomainName: "test-domain.com",
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-VSsTTf", "Errors.Invalid.Argument"),
},
{
testName: "when no domain name should return invalid argument error",
inputInstanceID: "instance-1",
inputDomainName: "",
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-PLpYix", "Errors.Invalid.Argument"),
},
{
testName: "when name too long should return invalid argument error",
inputInstanceID: " ",
inputDomainName: "domain.is.to.loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-PLpYix", "Errors.Invalid.Argument"),
},
{
testName: "when instance ID does not match context should return invalid argument error",
inputInstanceID: "different-instance",
inputDomainName: "test-domain.com",
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-83FUdY", "Errors.Invalid.Argument"),
},
{
testName: "when user is missing permission should return permission denied",
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
permChecker := domainmock.NewMockPermissionChecker(ctrl)
permChecker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainWritePermission).
Times(1).
Return(permissionErr)
return permChecker
},
inputInstanceID: "instance-1",
inputDomainName: "test-domain.com",
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-eroxID", "Errors.PermissionDenied"),
},
{
testName: "when domain not found should return no error",
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
permChecker := domainmock.NewMockPermissionChecker(ctrl)
permChecker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainWritePermission).
Times(1).
Return(nil)
return permChecker
},
domainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewMockInstanceDomainRepository(ctrl)
domainCond := database.NewTextCondition(
database.NewColumn("instance_domains", "domain"),
database.TextOperationEqual,
"test-domain.com")
repo.EXPECT().
DomainCondition(database.TextOperationEqual, "test-domain.com").
Times(1).
Return(domainCond)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(domainCond))).
Times(1).
Return(nil, noRowFoundErr)
return repo
},
inputInstanceID: "instance-1",
inputDomainName: "test-domain.com",
},
{
testName: "when get domain fails should return error",
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
permChecker := domainmock.NewMockPermissionChecker(ctrl)
permChecker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainWritePermission).
Times(1).
Return(nil)
return permChecker
},
domainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewMockInstanceDomainRepository(ctrl)
domainCond := database.NewTextCondition(
database.NewColumn("instance_domains", "domain"),
database.TextOperationEqual,
"test-domain.com")
repo.EXPECT().
DomainCondition(database.TextOperationEqual, "test-domain.com").
Times(1).
Return(domainCond)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(domainCond))).
Times(1).
Return(nil, getErr)
return repo
},
inputInstanceID: "instance-1",
inputDomainName: "test-domain.com",
expectedError: zerrors.ThrowInternal(getErr, "DOM-Zvv1fi", "Errors.Instance.Domain.Get"),
},
{
testName: "when domain is generated should return precondition failed error",
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
permChecker := domainmock.NewMockPermissionChecker(ctrl)
permChecker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainWritePermission).
Times(1).
Return(nil)
return permChecker
},
domainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewMockInstanceDomainRepository(ctrl)
isGenerated := true
domainCond := database.NewTextCondition(
database.NewColumn("instance_domains", "domain"),
database.TextOperationEqual,
"test-domain.com")
repo.EXPECT().
DomainCondition(database.TextOperationEqual, "test-domain.com").
Times(1).
Return(domainCond)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(domainCond))).
Times(1).
Return(&domain.InstanceDomain{IsGenerated: &isGenerated, Type: domain.DomainTypeCustom}, nil)
return repo
},
inputInstanceID: "instance-1",
inputDomainName: "test-domain.com",
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-cSfCVG", "Errors.Instance.Domain.GeneratedNotRemovable"),
},
{
testName: "when domain exists and is not generated should validate successfully",
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
permChecker := domainmock.NewMockPermissionChecker(ctrl)
permChecker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainWritePermission).
Times(1).
Return(nil)
return permChecker
},
domainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewMockInstanceDomainRepository(ctrl)
isGenerated := false
domainCond := database.NewTextCondition(
database.NewColumn("instance_domains", "domain"),
database.TextOperationEqual,
"test-domain.com")
repo.EXPECT().
DomainCondition(database.TextOperationEqual, "test-domain.com").
Times(1).
Return(domainCond)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(domainCond))).
Times(1).
Return(&domain.InstanceDomain{IsGenerated: &isGenerated, Type: domain.DomainTypeCustom}, nil)
return repo
},
inputInstanceID: "instance-1",
inputDomainName: "test-domain.com",
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
ctx := authz.NewMockContext("instance-1", "", "")
ctrl := gomock.NewController(t)
cmd := domain.NewRemoveInstanceDomainCommand(tc.inputInstanceID, tc.inputDomainName, domain.DomainTypeCustom)
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
}
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
if tc.domainRepo != nil {
domain.WithInstanceDomainRepo(tc.domainRepo(ctrl))(opts)
}
if tc.permissionChecker != nil {
opts.Permissions = tc.permissionChecker(ctrl)
}
err := cmd.Validate(ctx, opts)
assert.Equal(t, tc.expectedError, err)
})
}
}
func TestRemoveInstanceDomainCommand_Execute(t *testing.T) {
t.Parallel()
removeErr := errors.New("remove error")
tt := []struct {
testName string
domainRepo func(ctrl *gomock.Controller) domain.InstanceDomainRepository
inputInstanceID string
inputDomainName string
expectedError error
expectsDeleteTime bool
}{
{
testName: "when domain remove fails should return error",
domainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewMockInstanceDomainRepository(ctrl)
domainCond := database.NewTextCondition(
database.NewColumn("instance_domains", "domain"),
database.TextOperationEqual,
"test-domain.com")
repo.EXPECT().
PrimaryKeyCondition("test-domain.com").
Times(1).
Return(domainCond)
instanceIDCond := database.NewTextCondition(
database.NewColumn("instance_domains", "instance_id"),
database.TextOperationEqual,
"instance-1",
)
repo.EXPECT().
InstanceIDCondition("instance-1").
Times(1).
Return(instanceIDCond)
repo.EXPECT().
Remove(gomock.Any(), gomock.Any(), database.And(instanceIDCond, domainCond)).
Times(1).
Return(int64(0), removeErr)
return repo
},
inputInstanceID: "instance-1",
inputDomainName: "test-domain.com",
expectedError: zerrors.ThrowInternal(removeErr, "DOM-KH7AuJ", "Errors.Instance.Domain.Remove"),
},
{
testName: "when domain remove returns 0 rows removed should return no error",
domainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewMockInstanceDomainRepository(ctrl)
domainCond := database.NewTextCondition(
database.NewColumn("instance_domains", "domain"),
database.TextOperationEqual,
"test-domain.com")
repo.EXPECT().
PrimaryKeyCondition("test-domain.com").
Times(1).
Return(domainCond)
instanceIDCond := database.NewTextCondition(
database.NewColumn("instance_domains", "instance_id"),
database.TextOperationEqual,
"instance-1",
)
repo.EXPECT().
InstanceIDCondition("instance-1").
Times(1).
Return(instanceIDCond)
repo.EXPECT().
Remove(gomock.Any(), gomock.Any(), database.And(instanceIDCond, domainCond)).
Times(1).
Return(int64(0), nil)
return repo
},
inputInstanceID: "instance-1",
inputDomainName: "test-domain.com",
},
{
testName: "when domain remove returns more than 1 row removed should return internal error",
domainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewMockInstanceDomainRepository(ctrl)
domainCond := database.NewTextCondition(
database.NewColumn("instance_domains", "domain"),
database.TextOperationEqual,
"test-domain.com")
repo.EXPECT().
PrimaryKeyCondition("test-domain.com").
Times(1).
Return(domainCond)
instanceIDCond := database.NewTextCondition(
database.NewColumn("instance_domains", "instance_id"),
database.TextOperationEqual,
"instance-1",
)
repo.EXPECT().
InstanceIDCondition("instance-1").
Times(1).
Return(instanceIDCond)
repo.EXPECT().
Remove(gomock.Any(), gomock.Any(), database.And(instanceIDCond, domainCond)).
Times(1).
Return(int64(2), nil)
return repo
},
inputInstanceID: "instance-1",
inputDomainName: "test-domain.com",
expectedError: zerrors.ThrowInternal(nil, "DOM-XSCnJB", "Errors.Instsance.Domain.DeleteMismatch"),
},
{
testName: "when domain remove returns 1 row removed should return no error",
domainRepo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewMockInstanceDomainRepository(ctrl)
domainCond := database.NewTextCondition(
database.NewColumn("instance_domains", "domain"),
database.TextOperationEqual,
"test-domain.com")
repo.EXPECT().
PrimaryKeyCondition("test-domain.com").
Times(1).
Return(domainCond)
instanceIDCond := database.NewTextCondition(
database.NewColumn("instance_domains", "instance_id"),
database.TextOperationEqual,
"instance-1",
)
repo.EXPECT().
InstanceIDCondition("instance-1").
Times(1).
Return(instanceIDCond)
repo.EXPECT().
Remove(gomock.Any(), gomock.Any(), database.And(instanceIDCond, domainCond)).
Times(1).
Return(int64(1), nil)
return repo
},
inputInstanceID: "instance-1",
inputDomainName: "test-domain.com",
expectsDeleteTime: true,
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
ctx := authz.NewMockContext("instance-1", "", "")
ctrl := gomock.NewController(t)
cmd := domain.NewRemoveInstanceDomainCommand(tc.inputInstanceID, tc.inputDomainName, domain.DomainTypeCustom)
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
}
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
if tc.domainRepo != nil {
domain.WithInstanceDomainRepo(tc.domainRepo(ctrl))(opts)
}
err := cmd.Execute(ctx, opts)
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectsDeleteTime, cmd.DeleteTime != nil && !cmd.DeleteTime.IsZero())
})
}
}
func TestRemoveInstanceDomainCommand_Events(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
domainType domain.DomainType
expectedAggregateID string
expectedEventType eventstore.Command
}{
{
testName: "trusted domain",
domainType: domain.DomainTypeTrusted,
expectedAggregateID: "instance-1",
expectedEventType: &instance.TrustedDomainRemovedEvent{},
},
{
testName: "custom domain",
domainType: domain.DomainTypeCustom,
expectedAggregateID: "instance-1",
expectedEventType: &instance.DomainRemovedEvent{},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
instanceID := "instance-1"
domainName := "test.domain"
ctx := authz.NewMockContext(instanceID, "org-1", "")
cmd := domain.NewRemoveInstanceDomainCommand(instanceID, domainName, tc.domainType)
// When
events, err := cmd.Events(ctx, &domain.InvokeOpts{})
// Then
require.NoError(t, err)
require.Len(t, events, 1)
require.NotNil(t, events[0].Aggregate())
assert.Equal(t, instanceID, events[0].Aggregate().ID)
require.IsType(t, tc.expectedEventType, events[0])
switch asserted := events[0].(type) {
case *instance.DomainRemovedEvent:
assert.Equal(t, domainName, asserted.Domain)
case *instance.TrustedDomainRemovedEvent:
assert.Equal(t, domainName, asserted.Domain)
}
})
}
}
+7 -3
View File
@@ -2,6 +2,7 @@ package domain
import (
"context"
"errors"
"strings"
"github.com/zitadel/zitadel/backend/v3/storage/database"
@@ -29,7 +30,10 @@ func (g *GetInstanceQuery) Execute(ctx context.Context, opts *InvokeOpts) (err e
instance, err := instanceRepo.Get(ctx, opts.DB(), database.WithCondition(instanceRepo.IDCondition(g.ID)))
if err != nil {
return err
if errors.Is(err, &database.NoRowFoundError{}) {
return zerrors.ThrowNotFound(err, "DOM-QVrUwc", "Errors.Instance.NotFound")
}
return zerrors.ThrowInternal(err, "DOM-lvsRce", "Errors.Instance.Get")
}
g.returnedInstance = instance
@@ -45,14 +49,14 @@ func (g *GetInstanceQuery) String() string {
func (g *GetInstanceQuery) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
g.ID = strings.TrimSpace(g.ID)
if g.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "DOM-32a0o2", "invalid instance ID")
return zerrors.ThrowInvalidArgument(nil, "DOM-32a0o2", "Errors.Instance.ID")
}
if g.ID != authz.GetInstance(ctx).InstanceID() {
return zerrors.ThrowPermissionDenied(nil, "DOM-n0SvVB", "input instance ID doesn't match context instance")
}
if authZErr := opts.Permissions.CheckInstancePermission(ctx, InstanceReadPermission); authZErr != nil {
return zerrors.ThrowPermissionDenied(authZErr, "DOM-Uq6b00", "permission denied")
return zerrors.ThrowPermissionDenied(authZErr, "DOM-Uq6b00", "Errors.PermissionDenied")
}
return nil
+30 -4
View File
@@ -34,13 +34,13 @@ func TestGetInstanceCommand_Validate(t *testing.T) {
name: "empty instance id, error",
inputInstanceID: "",
ctx: context.Background(),
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-32a0o2", "invalid instance ID"),
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-32a0o2", "Errors.Instance.ID"),
},
{
name: "whitespace instance id, error",
inputInstanceID: " ",
ctx: context.Background(),
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-32a0o2", "invalid instance ID"),
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-32a0o2", "Errors.Instance.ID"),
},
{
name: "instance id mismatch, error",
@@ -62,7 +62,7 @@ func TestGetInstanceCommand_Validate(t *testing.T) {
},
inputInstanceID: "instance1",
ctx: authz.NewMockContext("instance1", "", ""),
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-Uq6b00", "permission denied"),
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-Uq6b00", "Errors.PermissionDenied"),
},
{
name: "valid instance id, success",
@@ -107,6 +107,7 @@ func TestGetInstanceCommand_Execute(t *testing.T) {
ctx := authz.NewMockContext("inst-1", "org-1", gofakeit.UUID())
getErr := errors.New("get error")
notFoundErr := database.NewNoRowFoundError(nil)
tt := []struct {
testName string
@@ -141,7 +142,32 @@ func TestGetInstanceCommand_Execute(t *testing.T) {
return instanceRepo
},
inputInstanceID: "instance-1",
expectedError: getErr,
expectedError: zerrors.ThrowInternal(getErr, "DOM-lvsRce", "Errors.Instance.Get"),
},
{
testName: "when instance is not found should return not found error",
instanceRepo: func(ctrl *gomock.Controller) domain.InstanceRepository {
instanceRepo := domainmock.NewInstanceRepo(ctrl)
instanceRepo.EXPECT().
LoadDomains().
Times(1).
Return(instanceRepo)
instanceRepo.EXPECT().
Get(
gomock.Any(),
gomock.Any(),
dbmock.QueryOptions(database.WithCondition(
instanceRepo.IDCondition("instance-1"),
)),
).
Times(1).
Return(nil, notFoundErr)
return instanceRepo
},
inputInstanceID: "instance-1",
expectedError: zerrors.ThrowNotFound(notFoundErr, "DOM-QVrUwc", "Errors.Instance.NotFound"),
},
{
testName: "when retrieving instance succeeds should set instance on struct and return no error",
+18 -18
View File
@@ -5,11 +5,11 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/zerrors"
instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
instance_v2 "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
)
type ListInstancesQuery struct {
Request *instance.ListInstancesRequest
Request *instance_v2.ListInstancesRequest
toReturn []*Instance
}
@@ -21,7 +21,7 @@ func (l *ListInstancesQuery) Result() []*Instance {
var _ Querier[[]*Instance] = (*ListInstancesQuery)(nil)
func NewListInstancesCommand(inputRequest *instance.ListInstancesRequest) *ListInstancesQuery {
func NewListInstancesCommand(inputRequest *instance_v2.ListInstancesRequest) *ListInstancesQuery {
return &ListInstancesQuery{
Request: inputRequest,
}
@@ -41,7 +41,7 @@ func (l *ListInstancesQuery) Execute(ctx context.Context, opts *InvokeOpts) (err
instances, err := instanceRepo.List(ctx, opts.DB(), conds, sorting, limit, offset)
if err != nil {
return err
return zerrors.ThrowInternal(err, "DOM-AIRPxN", "Errors.Instance.List")
}
l.toReturn = instances
@@ -55,20 +55,20 @@ func (l *ListInstancesQuery) Pagination() (database.QueryOption, database.QueryO
}
func (l *ListInstancesQuery) conditions(instanceRepo InstanceRepository, domainRepo InstanceDomainRepository) (database.QueryOption, error) {
conditions := make([]database.Condition, len(l.Request.GetQueries()))
for i, query := range l.Request.GetQueries() {
switch assertedType := query.GetQuery().(type) {
case *instance.Query_DomainQuery:
domainConditions := make([]database.Condition, len(assertedType.DomainQuery.GetDomains()))
for j, domain := range assertedType.DomainQuery.GetDomains() {
conditions := make([]database.Condition, len(l.Request.GetFilters()))
for i, query := range l.Request.GetFilters() {
switch assertedType := query.GetFilter().(type) {
case *instance_v2.Filter_CustomDomainsFilter:
domainConditions := make([]database.Condition, len(assertedType.CustomDomainsFilter.GetDomains()))
for j, domain := range assertedType.CustomDomainsFilter.GetDomains() {
domainConditions[j] = domainRepo.DomainCondition(database.TextOperationEqual, domain)
}
conditions[i] = instanceRepo.ExistsDomain(
database.Or(domainConditions...),
)
case *instance.Query_IdQuery:
idConditions := make([]database.Condition, len(assertedType.IdQuery.GetIds()))
for j, id := range assertedType.IdQuery.GetIds() {
case *instance_v2.Filter_InIdsFilter:
idConditions := make([]database.Condition, len(assertedType.InIdsFilter.GetIds()))
for j, id := range assertedType.InIdsFilter.GetIds() {
idConditions[j] = instanceRepo.IDCondition(id)
}
conditions[i] = database.Or(idConditions...)
@@ -84,13 +84,13 @@ func (l *ListInstancesQuery) Sorting(instanceRepo InstanceRepository) database.Q
var sortingCol database.Column
switch l.Request.GetSortingColumn() {
case instance.FieldName_FIELD_NAME_CREATION_DATE:
case instance_v2.FieldName_FIELD_NAME_CREATION_DATE:
sortingCol = instanceRepo.CreatedAtColumn()
case instance.FieldName_FIELD_NAME_ID:
case instance_v2.FieldName_FIELD_NAME_ID:
sortingCol = instanceRepo.IDColumn()
case instance.FieldName_FIELD_NAME_NAME:
case instance_v2.FieldName_FIELD_NAME_NAME:
sortingCol = instanceRepo.NameColumn()
case instance.FieldName_FIELD_NAME_UNSPECIFIED:
case instance_v2.FieldName_FIELD_NAME_UNSPECIFIED:
fallthrough
default:
return func(opts *database.QueryOpts) {}
@@ -115,7 +115,7 @@ func (l *ListInstancesQuery) Validate(ctx context.Context, opts *InvokeOpts) (er
// the instance in context. In here, instead we should probably loop through all retrieved
// instances and check their permissions.
if authZErr := opts.Permissions.CheckInstancePermission(ctx, InstanceReadPermission); authZErr != nil {
return zerrors.ThrowPermissionDenied(authZErr, "DOM-cuT6Ws", "permission denied")
return zerrors.ThrowPermissionDenied(authZErr, "DOM-cuT6Ws", "Errors.PermissionDenied")
}
return nil
+17 -17
View File
@@ -15,8 +15,8 @@ import (
noopdb "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/noop"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/zerrors"
filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
)
func TestListInstancesCommand_Sorting(t *testing.T) {
@@ -31,7 +31,7 @@ func TestListInstancesCommand_Sorting(t *testing.T) {
{
name: "sort by creation date DESC",
request: &instance.ListInstancesRequest{
SortingColumn: instance.FieldName_FIELD_NAME_CREATION_DATE.Enum(),
SortingColumn: instance.FieldName_FIELD_NAME_CREATION_DATE,
},
expectedSortingDirection: database.OrderDirectionDesc,
@@ -40,7 +40,7 @@ func TestListInstancesCommand_Sorting(t *testing.T) {
{
name: "sort by creation date ASC",
request: &instance.ListInstancesRequest{
SortingColumn: instance.FieldName_FIELD_NAME_CREATION_DATE.Enum(),
SortingColumn: instance.FieldName_FIELD_NAME_CREATION_DATE,
Pagination: &filter.PaginationRequest{Asc: true},
},
@@ -50,7 +50,7 @@ func TestListInstancesCommand_Sorting(t *testing.T) {
{
name: "sort by id DESC",
request: &instance.ListInstancesRequest{
SortingColumn: instance.FieldName_FIELD_NAME_ID.Enum(),
SortingColumn: instance.FieldName_FIELD_NAME_ID,
},
expectedSortingDirection: database.OrderDirectionDesc,
expectedOrderBy: database.Columns{database.NewColumn("instances", "id")},
@@ -58,7 +58,7 @@ func TestListInstancesCommand_Sorting(t *testing.T) {
{
name: "sort by id ASC",
request: &instance.ListInstancesRequest{
SortingColumn: instance.FieldName_FIELD_NAME_ID.Enum(),
SortingColumn: instance.FieldName_FIELD_NAME_ID,
Pagination: &filter.PaginationRequest{Asc: true},
},
expectedSortingDirection: database.OrderDirectionAsc,
@@ -67,7 +67,7 @@ func TestListInstancesCommand_Sorting(t *testing.T) {
{
name: "unspecified field",
request: &instance.ListInstancesRequest{
SortingColumn: instance.FieldName_FIELD_NAME_UNSPECIFIED.Enum(),
SortingColumn: instance.FieldName_FIELD_NAME_UNSPECIFIED,
},
expectedSortingDirection: database.OrderDirectionAsc,
},
@@ -151,17 +151,17 @@ func TestListInstancesCommand_Execute(t *testing.T) {
},
inputRequest: &instance.ListInstancesRequest{
Pagination: &filter.PaginationRequest{Limit: 2, Offset: 1, Asc: false},
SortingColumn: instance.FieldName_FIELD_NAME_NAME.Enum(),
Queries: []*instance.Query{
{Query: &instance.Query_IdQuery{IdQuery: &instance.IdsQuery{
SortingColumn: instance.FieldName_FIELD_NAME_NAME,
Filters: []*instance.Filter{
{Filter: &instance.Filter_InIdsFilter{InIdsFilter: &filter.InIDsFilter{
Ids: []string{"instance-1", "instance-2"},
}}},
{Query: &instance.Query_DomainQuery{DomainQuery: &instance.DomainsQuery{
{Filter: &instance.Filter_CustomDomainsFilter{CustomDomainsFilter: &instance.CustomDomainsFilter{
Domains: []string{"domain1.example.com", "domain2.example.com"},
}}},
},
},
expectedError: listErr,
expectedError: zerrors.ThrowInternal(listErr, "DOM-AIRPxN", "Errors.Instance.List"),
},
{
testName: "when listing instances succeeds should save into result and return nil",
@@ -216,12 +216,12 @@ func TestListInstancesCommand_Execute(t *testing.T) {
},
inputRequest: &instance.ListInstancesRequest{
Pagination: &filter.PaginationRequest{Limit: 2, Offset: 1, Asc: false},
SortingColumn: instance.FieldName_FIELD_NAME_NAME.Enum(),
Queries: []*instance.Query{
{Query: &instance.Query_IdQuery{IdQuery: &instance.IdsQuery{
SortingColumn: instance.FieldName_FIELD_NAME_NAME,
Filters: []*instance.Filter{
{Filter: &instance.Filter_InIdsFilter{InIdsFilter: &filter.InIDsFilter{
Ids: []string{"instance-1", "instance-2"},
}}},
{Query: &instance.Query_DomainQuery{DomainQuery: &instance.DomainsQuery{
{Filter: &instance.Filter_CustomDomainsFilter{CustomDomainsFilter: &instance.CustomDomainsFilter{
Domains: []string{"domain1.example.com", "domain2.example.com"},
}}},
},
@@ -288,7 +288,7 @@ func TestListInstancesCommand_Validate(t *testing.T) {
return permChecker
},
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-cuT6Ws", "permission denied"),
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-cuT6Ws", "Errors.PermissionDenied"),
},
{
name: "when valid permission should return no error",
@@ -0,0 +1,133 @@
package domain
import (
"context"
"strings"
"github.com/zitadel/zitadel/backend/v3/api/object"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/zerrors"
instance_v2 "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
)
type ListInstanceTrustedDomainsQuery struct {
Request *instance_v2.ListTrustedDomainsRequest
toReturn []*InstanceDomain
}
var _ Querier[[]*InstanceDomain] = (*ListInstanceTrustedDomainsQuery)(nil)
func NewListInstanceTrustedDomainsQuery(inputRequest *instance_v2.ListTrustedDomainsRequest) *ListInstanceTrustedDomainsQuery {
return &ListInstanceTrustedDomainsQuery{
Request: inputRequest,
}
}
// Result implements [Querier].
func (l *ListInstanceTrustedDomainsQuery) Result() []*InstanceDomain {
return l.toReturn
}
// Execute implements [Querier].
func (l *ListInstanceTrustedDomainsQuery) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
domainRepo := opts.instanceDomainRepo
sorting := l.Sorting(domainRepo)
limit, offset := l.Pagination()
conds, err := l.conditions(domainRepo)
if err != nil {
return err
}
instances, err := domainRepo.List(ctx, opts.DB(), conds, sorting, limit, offset)
if err != nil {
return zerrors.ThrowInternal(err, "DOM-8eoWqY", "Errors.Instance.TrustedDomain.List")
}
l.toReturn = instances
return nil
}
func (l *ListInstanceTrustedDomainsQuery) Pagination() (database.QueryOption, database.QueryOption) {
return database.WithLimit(l.Request.GetPagination().GetLimit()),
database.WithOffset(uint32(l.Request.GetPagination().GetOffset()))
}
func (l *ListInstanceTrustedDomainsQuery) conditions(domainRepo InstanceDomainRepository) (database.QueryOption, error) {
conditions := make([]database.Condition, len(l.Request.GetFilters()))
for i, query := range l.Request.GetFilters() {
switch assertedType := query.GetFilter().(type) {
case *instance_v2.TrustedDomainFilter_DomainFilter:
method, err := object.TextQueryMethodToTextOperation(assertedType.DomainFilter.GetMethod())
if err != nil {
return nil, err
}
conditions[i] = domainRepo.DomainCondition(
method,
assertedType.DomainFilter.GetDomain(),
)
default:
return nil, zerrors.ThrowInvalidArgument(NewUnexpectedQueryTypeError(assertedType), "DOM-qMUBMr", "List.Query.Invalid")
}
}
conditions = append(conditions, domainRepo.TypeCondition(DomainTypeTrusted))
return database.WithCondition(database.And(conditions...)), nil
}
func (l *ListInstanceTrustedDomainsQuery) Sorting(domainRepo InstanceDomainRepository) database.QueryOption {
var sortingCol database.Column
switch l.Request.GetSortingColumn() {
case instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE:
sortingCol = domainRepo.CreatedAtColumn()
case instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN:
sortingCol = domainRepo.DomainColumn()
case instance_v2.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED:
fallthrough
default:
return func(opts *database.QueryOpts) {}
}
orderDirection := database.OrderDirectionDesc
if l.Request.GetPagination().GetAsc() {
orderDirection = database.OrderDirectionAsc
}
return database.WithOrderBy(orderDirection, sortingCol)
}
// String implements [Querier].
func (l *ListInstanceTrustedDomainsQuery) String() string {
return "ListInstanceTrustedDomainsQuery"
}
// Validate implements [Querier].
func (l *ListInstanceTrustedDomainsQuery) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
// TODO(IAM-Marco): These validations are copied from custom domains.
// The eventstore side has no validation in place. To decide if we want to keep this or remove it
instanceID := strings.TrimSpace(l.Request.GetInstanceId())
if instanceID == "" || instanceID == authz.GetInstance(ctx).InstanceID() {
return l.checkDomainPerms(ctx, opts)
}
// TODO(IAM-Marco): This is wrong, as it should check the permission for the input instance and not the one in context
if authZErr := opts.Permissions.CheckInstancePermission(ctx, InstanceReadPermission); authZErr != nil {
return zerrors.ThrowPermissionDenied(authZErr, "DOM-nx0QKI", "Errors.PermissionDenied")
}
return l.checkDomainPerms(ctx, opts)
}
func (l *ListInstanceTrustedDomainsQuery) checkDomainPerms(ctx context.Context, opts *InvokeOpts) error {
if authZErr := opts.Permissions.CheckInstancePermission(ctx, DomainReadPermission); authZErr != nil {
return zerrors.ThrowPermissionDenied(authZErr, "DOM-RyCEyr", "Errors.PermissionDenied")
}
return nil
}
@@ -0,0 +1,336 @@
package domain_test
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/backend/v3/domain"
domainmock "github.com/zitadel/zitadel/backend/v3/domain/mock"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
noopdb "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/noop"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/zerrors"
filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)
func TestListInstanceTrustedDomainsQuery_Validate(t *testing.T) {
t.Parallel()
permissionErr := errors.New("permission error")
tt := []struct {
name string
request *instance.ListTrustedDomainsRequest
permissionChecker func(ctrl *gomock.Controller) domain.PermissionChecker
expectedError error
}{
{
name: "when missing domain read permission should return permission denied (empty instance id)",
request: &instance.ListTrustedDomainsRequest{InstanceId: " "},
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
checker := domainmock.NewMockPermissionChecker(ctrl)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainReadPermission).
Times(1).
Return(permissionErr)
return checker
},
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-RyCEyr", "Errors.PermissionDenied"),
},
{
name: "when valid permissions should return no error (ctx instance same as input)",
request: &instance.ListTrustedDomainsRequest{InstanceId: "instance-1"},
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
checker := domainmock.NewMockPermissionChecker(ctrl)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainReadPermission).
Times(1).
Return(nil)
return checker
},
},
{
name: "when input instance doesn't match context should check instance and return error",
request: &instance.ListTrustedDomainsRequest{
InstanceId: "different-instance",
},
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
checker := domainmock.NewMockPermissionChecker(ctrl)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.InstanceReadPermission).
Times(1).
Return(permissionErr)
return checker
},
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-nx0QKI", "Errors.PermissionDenied"),
},
{
name: "when input instance doesn't match context should check instance and succeed then check domain and fail",
request: &instance.ListTrustedDomainsRequest{
InstanceId: "different-instance",
},
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
checker := domainmock.NewMockPermissionChecker(ctrl)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.InstanceReadPermission).
Times(1).
Return(nil)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainReadPermission).
Times(1).
Return(permissionErr)
return checker
},
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-RyCEyr", "Errors.PermissionDenied"),
},
{
name: "when input instance doesn't match context should check instance and succeed then check domain and succeed",
request: &instance.ListTrustedDomainsRequest{
InstanceId: "different-instance",
},
permissionChecker: func(ctrl *gomock.Controller) domain.PermissionChecker {
checker := domainmock.NewMockPermissionChecker(ctrl)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.InstanceReadPermission).
Times(1).
Return(nil)
checker.EXPECT().
CheckInstancePermission(gomock.Any(), domain.DomainReadPermission).
Times(1).
Return(nil)
return checker
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
ctx := authz.NewMockContext("instance-1", "org-1", "")
query := domain.NewListInstanceTrustedDomainsQuery(tc.request)
err := query.Validate(ctx, &domain.InvokeOpts{
Permissions: tc.permissionChecker(ctrl),
})
if tc.expectedError != nil {
assert.Error(t, err)
assert.Equal(t, tc.expectedError, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestListInstanceTrustedDomainsQuery_Execute(t *testing.T) {
t.Parallel()
listErr := errors.New("list mock error")
tt := []struct {
name string
request *instance.ListTrustedDomainsRequest
repo func(ctrl *gomock.Controller) domain.InstanceDomainRepository
expectedError error
expectedDomains []*domain.InstanceDomain
}{
{
name: "when parsing conditions fails should return invalid argument error",
request: &instance.ListTrustedDomainsRequest{
Pagination: &filter.PaginationRequest{Limit: 2, Offset: 1, Asc: false},
Filters: []*instance.TrustedDomainFilter{
{Filter: &instance.TrustedDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{
Domain: "test.domain",
Method: 99,
},
}},
},
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN,
},
repo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewInstancesDomainRepo(ctrl)
return repo
},
expectedError: zerrors.ThrowInvalidArgument(nil, "OBJ-iBRBVe", "invalid text query method"),
},
{
name: "when listing domains fails should return error",
request: &instance.ListTrustedDomainsRequest{
Pagination: &filter.PaginationRequest{Limit: 2, Offset: 1, Asc: false},
Filters: []*instance.TrustedDomainFilter{
{Filter: &instance.TrustedDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{
Domain: "test.domain",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS,
},
}},
},
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN,
},
repo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewInstancesDomainRepo(ctrl)
repo.EXPECT().
List(gomock.Any(), gomock.Any(),
dbmock.QueryOptions(
database.WithCondition(database.And(
repo.DomainCondition(database.TextOperationContains, "test.domain"),
repo.TypeCondition(domain.DomainTypeTrusted),
)),
),
dbmock.QueryOptions(database.WithOrderBy(database.OrderDirectionDesc, repo.DomainColumn())),
dbmock.QueryOptions(database.WithLimit(2)),
dbmock.QueryOptions(database.WithOffset(1)),
).
Return(nil, listErr)
return repo
},
expectedError: zerrors.ThrowInternal(listErr, "DOM-8eoWqY", "Errors.Instance.TrustedDomain.List"),
},
{
name: "when listing domains succeeds should return domains",
request: &instance.ListTrustedDomainsRequest{
Filters: []*instance.TrustedDomainFilter{
{Filter: &instance.TrustedDomainFilter_DomainFilter{
DomainFilter: &instance.DomainFilter{
Domain: "test.domain",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS,
},
}},
},
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN,
},
repo: func(ctrl *gomock.Controller) domain.InstanceDomainRepository {
repo := domainmock.NewInstancesDomainRepo(ctrl)
domains := []*domain.InstanceDomain{
{Domain: "test1.domain"},
{Domain: "test2.domain"},
}
repo.EXPECT().
List(gomock.Any(), gomock.Any(),
dbmock.QueryOptions(
database.WithCondition(database.And(
repo.DomainCondition(database.TextOperationContains, "test.domain"),
repo.TypeCondition(domain.DomainTypeTrusted),
)),
),
dbmock.QueryOptions(database.WithOrderBy(database.OrderDirectionDesc, repo.DomainColumn())),
dbmock.QueryOptions(database.WithLimit(0)),
dbmock.QueryOptions(database.WithOffset(0)),
).
Return(domains, nil)
return repo
},
expectedDomains: []*domain.InstanceDomain{
{Domain: "test1.domain"},
{Domain: "test2.domain"},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
}
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
if tc.repo != nil {
instDomain := tc.repo(ctrl)
domain.WithInstanceDomainRepo(instDomain)(opts)
}
query := domain.NewListInstanceTrustedDomainsQuery(tc.request)
err := query.Execute(t.Context(), opts)
if tc.expectedError != nil {
assert.Error(t, err)
assert.Equal(t, tc.expectedError, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedDomains, query.Result())
}
})
}
}
func TestListInstanceTrustedDomainsQuery_Sorting(t *testing.T) {
t.Parallel()
tt := []struct {
name string
request *instance.ListTrustedDomainsRequest
expectedSortingDirection database.OrderDirection
expectedOrderBy database.Columns
}{
{
name: "sort by creation date DESC",
request: &instance.ListTrustedDomainsRequest{
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
},
expectedSortingDirection: database.OrderDirectionDesc,
expectedOrderBy: database.Columns{database.NewColumn("instance_domains", "created_at")},
},
{
name: "sort by creation date ASC",
request: &instance.ListTrustedDomainsRequest{
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE,
Pagination: &filter.PaginationRequest{Asc: true},
},
expectedSortingDirection: database.OrderDirectionAsc,
expectedOrderBy: database.Columns{database.NewColumn("instance_domains", "created_at")},
},
{
name: "sort by domain DESC",
request: &instance.ListTrustedDomainsRequest{
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN,
},
expectedSortingDirection: database.OrderDirectionDesc,
expectedOrderBy: database.Columns{database.NewColumn("instance_domains", "domain")},
},
{
name: "sort by domain ASC",
request: &instance.ListTrustedDomainsRequest{
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN,
Pagination: &filter.PaginationRequest{Asc: true},
},
expectedSortingDirection: database.OrderDirectionAsc,
expectedOrderBy: database.Columns{database.NewColumn("instance_domains", "domain")},
},
{
name: "unspecified field",
request: &instance.ListTrustedDomainsRequest{
SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED,
},
expectedSortingDirection: database.OrderDirectionAsc,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Given
mockCtrl := gomock.NewController(t)
mockRepo := domainmock.NewInstancesDomainRepo(mockCtrl)
cmd := domain.NewListInstanceTrustedDomainsQuery(tc.request)
opts := &database.QueryOpts{}
// Test
queryOpt := cmd.Sorting(mockRepo)
queryOpt(opts)
// Verify
assert.Equal(t, tc.expectedOrderBy, opts.OrderBy)
assert.Equal(t, tc.expectedSortingDirection, opts.Ordering)
})
}
}
+20 -15
View File
@@ -11,8 +11,9 @@ import (
)
type UpdateInstanceCommand struct {
ID string `json:"id"`
Name string `json:"name"`
ID string `json:"id"`
Name string `json:"name"`
ShouldSkipUpdate bool `json:"should_skip_update"`
}
// RequiresTransaction implements [Transactional].
@@ -20,6 +21,10 @@ func (u *UpdateInstanceCommand) RequiresTransaction() {}
// Events implements Commander.
func (u *UpdateInstanceCommand) Events(ctx context.Context, _ *InvokeOpts) ([]eventstore.Command, error) {
if u.ShouldSkipUpdate {
return nil, nil
}
return []eventstore.Command{
instance.NewInstanceChangedEvent(ctx, &instance.NewAggregate(u.ID).Aggregate, u.Name),
}, nil
@@ -39,6 +44,10 @@ func NewUpdateInstanceCommand(id, name string) *UpdateInstanceCommand {
// Execute implements [Commander]
func (u *UpdateInstanceCommand) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
if u.ShouldSkipUpdate {
return
}
instanceRepo := opts.instanceRepo
updateCount, err := instanceRepo.Update(
@@ -48,16 +57,14 @@ func (u *UpdateInstanceCommand) Execute(ctx context.Context, opts *InvokeOpts) (
database.NewChange(instanceRepo.NameColumn(), u.Name),
)
if err != nil {
return err
return zerrors.ThrowInternal(err, "DOM-PkVMNR", "Errors.Instance.Update")
}
if updateCount == 0 {
err = zerrors.ThrowNotFound(nil, "DOM-ghfov1", "Errors.Instance.NotFound")
return err
return zerrors.ThrowNotFound(nil, "DOM-ghfov1", "Errors.Instance.NotFound")
}
if updateCount > 1 {
err = zerrors.ThrowInternal(NewMultipleObjectsUpdatedError(1, updateCount), "DOM-HlrNmD", "unexpected number of rows updated")
return err
return zerrors.ThrowInternal(NewMultipleObjectsUpdatedError(1, updateCount), "DOM-HlrNmD", "Errors.Instance.UpdateMismatch")
}
return err
@@ -71,26 +78,24 @@ func (u *UpdateInstanceCommand) String() string {
// Validate implements [Commander]
func (u *UpdateInstanceCommand) Validate(ctx context.Context, opts *InvokeOpts) error {
if u.ID = strings.TrimSpace(u.ID); u.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "DOM-wSs6kG", "invalid instance ID")
return zerrors.ThrowInvalidArgument(nil, "DOM-wSs6kG", "Errors.Instance.ID")
}
if u.Name = strings.TrimSpace(u.Name); u.Name == "" {
return zerrors.ThrowInvalidArgument(nil, "DOM-FPJcLC", "invalid instance name")
return zerrors.ThrowInvalidArgument(nil, "DOM-FPJcLC", "Errors.Instance.Name")
}
if authZErr := opts.Permissions.CheckInstancePermission(ctx, InstanceWritePermission); authZErr != nil {
return zerrors.ThrowPermissionDenied(authZErr, "DOM-M5ObLP", "permission denied")
return zerrors.ThrowPermissionDenied(authZErr, "DOM-M5ObLP", "Errors.PermissionDenied")
}
instanceRepo := opts.instanceRepo
instance, err := instanceRepo.Get(ctx, opts.DB(), database.WithCondition(instanceRepo.IDCondition(u.ID)))
if err != nil {
return err
return zerrors.ThrowInternal(err, "DOM-j05Hdo", "Errors.Instance.Get")
}
if instance.Name == u.Name {
err = zerrors.ThrowPreconditionFailed(nil, "DOM-5MrT21", "Errors.Instance.NotChanged")
return err
}
u.ShouldSkipUpdate = instance.Name == u.Name
return nil
}
+65 -28
View File
@@ -14,6 +14,7 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
noopdb "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/noop"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -24,24 +25,25 @@ func TestUpdateInstanceCommand_Validate(t *testing.T) {
permissionErr := errors.New("permission error")
tt := []struct {
testName string
instanceRepo func(ctrl *gomock.Controller) domain.InstanceRepository
permissionChecker func(ctrl *gomock.Controller) domain.PermissionChecker
inputInstanceID string
inputInstanceName string
expectedError error
testName string
instanceRepo func(ctrl *gomock.Controller) domain.InstanceRepository
permissionChecker func(ctrl *gomock.Controller) domain.PermissionChecker
inputInstanceID string
inputInstanceName string
expectedError error
expectedUpdateSkip bool
}{
{
testName: "when no ID should return invalid argument error",
inputInstanceID: "",
inputInstanceName: "test-name",
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-wSs6kG", "invalid instance ID"),
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-wSs6kG", "Errors.Instance.ID"),
},
{
testName: "when no name shuld return invalid argument error",
inputInstanceID: "test-id",
inputInstanceName: "",
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-FPJcLC", "invalid instance name"),
expectedError: zerrors.ThrowInvalidArgument(nil, "DOM-FPJcLC", "Errors.Instance.Name"),
},
{
testName: "when user is missing permission should return permission denied",
@@ -57,7 +59,7 @@ func TestUpdateInstanceCommand_Validate(t *testing.T) {
},
inputInstanceID: "instance-1",
inputInstanceName: "test instance update",
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-M5ObLP", "permission denied"),
expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-M5ObLP", "Errors.PermissionDenied"),
},
{
testName: "when retrieving instance fails should return error",
@@ -85,7 +87,7 @@ func TestUpdateInstanceCommand_Validate(t *testing.T) {
},
inputInstanceID: "instance-1",
inputInstanceName: "test instance update",
expectedError: getErr,
expectedError: zerrors.ThrowInternal(getErr, "DOM-j05Hdo", "Errors.Instance.Get"),
},
{
testName: "when instance name is not changed should return name not changed error",
@@ -112,9 +114,9 @@ func TestUpdateInstanceCommand_Validate(t *testing.T) {
}, nil)
return repo
},
inputInstanceID: "instance-1",
inputInstanceName: "test instance update",
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-5MrT21", "Errors.Instance.NotChanged"),
inputInstanceID: "instance-1",
inputInstanceName: "test instance update",
expectedUpdateSkip: true,
},
{
testName: "when instance name is changed should validate successfully and return no error",
@@ -167,6 +169,7 @@ func TestUpdateInstanceCommand_Validate(t *testing.T) {
err := cmd.Validate(ctx, opts)
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedUpdateSkip, cmd.ShouldSkipUpdate)
})
}
}
@@ -181,11 +184,16 @@ func TestUpdateInstanceCommand_Execute(t *testing.T) {
instanceRepo func(ctrl *gomock.Controller) domain.InstanceRepository
inputID string
inputName string
inputID string
inputName string
shouldSkipUpdate bool
expectedError error
}{
{
testName: "when ShouldSkipUpdate is true should return nil",
shouldSkipUpdate: true,
},
{
testName: "when instance update fails should return error",
instanceRepo: func(ctrl *gomock.Controller) domain.InstanceRepository {
@@ -201,7 +209,7 @@ func TestUpdateInstanceCommand_Execute(t *testing.T) {
},
inputID: "instance-1",
inputName: "test instance update",
expectedError: updateErr,
expectedError: zerrors.ThrowInternal(updateErr, "DOM-PkVMNR", "Errors.Instance.Update"),
},
{
testName: "when instance update returns 0 rows updated should return not found error",
@@ -235,7 +243,7 @@ func TestUpdateInstanceCommand_Execute(t *testing.T) {
},
inputID: "instance-1",
inputName: "test instance update",
expectedError: zerrors.ThrowInternal(domain.NewMultipleObjectsUpdatedError(1, 2), "DOM-HlrNmD", "unexpected number of rows updated"),
expectedError: zerrors.ThrowInternal(domain.NewMultipleObjectsUpdatedError(1, 2), "DOM-HlrNmD", "Errors.Instance.UpdateMismatch"),
},
{
testName: "when instance update returns 1 row updated should return no error and set non-primary verified domain",
@@ -263,6 +271,7 @@ func TestUpdateInstanceCommand_Execute(t *testing.T) {
ctx := authz.NewMockContext("instance-1", "", "")
ctrl := gomock.NewController(t)
cmd := domain.NewUpdateInstanceCommand(tc.inputID, tc.inputName)
cmd.ShouldSkipUpdate = tc.shouldSkipUpdate
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
@@ -283,18 +292,46 @@ func TestUpdateInstanceCommand_Execute(t *testing.T) {
func TestUpdateInstanceCommand_Events(t *testing.T) {
t.Parallel()
// Given
ctx := authz.NewMockContext("instance-1", "", "")
cmd := domain.NewUpdateInstanceCommand("instance-1", "test-name")
tt := []struct {
testName string
inputName string
inputInstanceID string
shouldSkipUpdate bool
expectedEvent eventstore.Command
}{
{
testName: "when ShouldSkipUpdate is true should return no event and no error",
},
{
testName: "when ShouldSkipUpdate is true should return expected event",
inputName: "test-name",
inputInstanceID: "instance-1",
expectedEvent: instance.NewInstanceChangedEvent(
t.Context(),
&instance.NewAggregate("instance-1").Aggregate,
"test-name",
),
},
}
// Test
events, err := cmd.Events(ctx, &domain.InvokeOpts{})
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Given
ctx := authz.NewMockContext(tc.inputInstanceID, "", "")
cmd := domain.NewUpdateInstanceCommand(tc.inputInstanceID, tc.inputName)
// Verify
assert.NoError(t, err)
require.Len(t, events, 1)
// Test
events, err := cmd.Events(ctx, &domain.InvokeOpts{})
event := events[0].(*instance.InstanceChangedEvent)
assert.Equal(t, "instance-1", event.Aggregate().ID)
assert.Equal(t, "test-name", event.Name)
// Verify
assert.NoError(t, err)
if tc.expectedEvent != nil {
require.Len(t, events, 1)
event := events[0].(*instance.InstanceChangedEvent)
assert.Equal(t, tc.inputInstanceID, event.Aggregate().ID)
assert.Equal(t, tc.inputName, event.Name)
}
})
}
}
@@ -353,6 +353,44 @@ func (c *MockInstanceDomainRepositoryIsGeneratedColumnCall) DoAndReturn(f func()
return c
}
// IsGeneratedCondition mocks base method.
func (m *MockInstanceDomainRepository) IsGeneratedCondition(arg0 bool) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsGeneratedCondition", arg0)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// IsGeneratedCondition indicates an expected call of IsGeneratedCondition.
func (mr *MockInstanceDomainRepositoryMockRecorder) IsGeneratedCondition(arg0 any) *MockInstanceDomainRepositoryIsGeneratedConditionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsGeneratedCondition", reflect.TypeOf((*MockInstanceDomainRepository)(nil).IsGeneratedCondition), arg0)
return &MockInstanceDomainRepositoryIsGeneratedConditionCall{Call: call}
}
// MockInstanceDomainRepositoryIsGeneratedConditionCall wrap *gomock.Call
type MockInstanceDomainRepositoryIsGeneratedConditionCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockInstanceDomainRepositoryIsGeneratedConditionCall) Return(arg0 database.Condition) *MockInstanceDomainRepositoryIsGeneratedConditionCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockInstanceDomainRepositoryIsGeneratedConditionCall) Do(f func(bool) database.Condition) *MockInstanceDomainRepositoryIsGeneratedConditionCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockInstanceDomainRepositoryIsGeneratedConditionCall) DoAndReturn(f func(bool) database.Condition) *MockInstanceDomainRepositoryIsGeneratedConditionCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// IsPrimaryColumn mocks base method.
func (m *MockInstanceDomainRepository) IsPrimaryColumn() database.Column {
m.ctrl.T.Helper()
@@ -0,0 +1,66 @@
package domain
import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
)
// TODO(IAM-Marco): Finish implementation
type OIDCConfigurationUpdate struct {
instanceDomainName string
projectID string
consoleAppID string
// oidcConfigChanges []OIDConfigChange
// appID string
}
// RequiresTransaction implements [Transactional].
func (o *OIDCConfigurationUpdate) RequiresTransaction() {}
func NewOIDCConfigurationUpdate(instanceDomainName, projectID, consoleAppID string) *OIDCConfigurationUpdate {
return &OIDCConfigurationUpdate{
instanceDomainName: instanceDomainName,
projectID: projectID,
consoleAppID: consoleAppID,
}
}
// Events implements [Commander].
func (o *OIDCConfigurationUpdate) Events(ctx context.Context, opts *InvokeOpts) ([]eventstore.Command, error) {
// oidcConfigChangedEvent, err := project.NewOIDCConfigChangedEvent(
// ctx,
// &project.NewAggregate(o.appID, o.projectID).Aggregate,
// o.appID,
// changes,
// o.oidcConfigChanges,
// )
// if err != nil {
// return nil, err
// }
// return []eventstore.Command{
// oidcConfigChangedEvent,
// }, nil
return nil, nil
}
// Execute implements [Commander].
func (o *OIDCConfigurationUpdate) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
return nil
}
// String implements [Commander].
func (o *OIDCConfigurationUpdate) String() string {
return "OIDCConfigurationUpdate"
}
// Validate implements [Commander].
func (o *OIDCConfigurationUpdate) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
return nil
}
var (
_ (Commander) = (*OIDCConfigurationUpdate)(nil)
_ (Transactional) = (*OIDCConfigurationUpdate)(nil)
)
+2
View File
@@ -5,6 +5,8 @@ import "context"
const (
InstanceReadPermission = "instance.read"
InstanceWritePermission = "instance.write"
DomainReadPermission = "domain.read"
DomainWritePermission = "domain.write"
OrganizationReadPermission = "organization.read"
OrganizationWritePermission = "organization.write"
)
@@ -163,6 +163,11 @@ func (i instanceDomain) TypeCondition(typ domain.DomainType) database.Condition
return database.NewTextCondition(i.TypeColumn(), database.TextOperationEqual, typ.String())
}
// IsGeneratedCondition implements domain.InstanceDomainRepository.
func (i *instanceDomain) IsGeneratedCondition(isGenerated bool) database.Condition {
return database.NewBooleanCondition(i.IsGeneratedColumn(), isGenerated)
}
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
+1 -1
View File
@@ -70,7 +70,7 @@ func (s *Server) AddInstanceTrustedDomain(ctx context.Context, req *admin_pb.Add
}
func (s *Server) RemoveInstanceTrustedDomain(ctx context.Context, req *admin_pb.RemoveInstanceTrustedDomainRequest) (*admin_pb.RemoveInstanceTrustedDomainResponse, error) {
details, err := s.command.RemoveTrustedDomain(ctx, req.Domain)
details, err := s.command.RemoveTrustedDomain(ctx, req.Domain, true)
if err != nil {
return nil, err
}
+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
+4 -4
View File
@@ -34,7 +34,7 @@ func (s *Server) CreateOrganization(ctx context.Context, request *connect.Reques
func (s *Server) UpdateOrganization(ctx context.Context, request *connect.Request[v2beta_org.UpdateOrganizationRequest]) (*connect.Response[v2beta_org.UpdateOrganizationResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return orgv2.UpdateOrganization(ctx, request)
return orgv2.UpdateOrganizationBeta(ctx, request)
}
org, err := s.command.ChangeOrg(ctx, request.Msg.GetId(), request.Msg.GetName())
@@ -71,7 +71,7 @@ func (s *Server) ListOrganizations(ctx context.Context, request *connect.Request
func (s *Server) DeleteOrganization(ctx context.Context, request *connect.Request[v2beta_org.DeleteOrganizationRequest]) (*connect.Response[v2beta_org.DeleteOrganizationResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return orgv2.DeleteOrganization(ctx, request)
return orgv2.DeleteOrganizationBeta(ctx, request)
}
details, err := s.command.RemoveOrg(ctx, request.Msg.GetId())
@@ -127,7 +127,7 @@ func (s *Server) DeleteOrganizationMetadata(ctx context.Context, request *connec
func (s *Server) DeactivateOrganization(ctx context.Context, request *connect.Request[org.DeactivateOrganizationRequest]) (*connect.Response[org.DeactivateOrganizationResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return orgv2.DeactivateOrganization(ctx, request)
return orgv2.DeactivateOrganizationBeta(ctx, request)
}
objectDetails, err := s.command.DeactivateOrg(ctx, request.Msg.GetId())
@@ -141,7 +141,7 @@ func (s *Server) DeactivateOrganization(ctx context.Context, request *connect.Re
func (s *Server) ActivateOrganization(ctx context.Context, request *connect.Request[org.ActivateOrganizationRequest]) (*connect.Response[org.ActivateOrganizationResponse], error) {
if authz.GetFeatures(ctx).EnableRelationalTables {
return orgv2.ActivateOrganization(ctx, request)
return orgv2.ActivateOrganizationBeta(ctx, request)
}
objectDetails, err := s.command.ReactivateOrg(ctx, request.Msg.GetId())
+2 -2
View File
@@ -83,7 +83,7 @@ func (s *Server) CreateInstance(ctx context.Context, req *system_pb.CreateInstan
}
func (s *Server) RemoveInstance(ctx context.Context, req *system_pb.RemoveInstanceRequest) (*system_pb.RemoveInstanceResponse, error) {
details, err := s.command.RemoveInstance(ctx, req.InstanceId)
details, err := s.command.RemoveInstance(ctx, req.InstanceId, true)
if err != nil {
return nil, err
}
@@ -160,7 +160,7 @@ func (s *Server) AddDomain(ctx context.Context, req *system_pb.AddDomainRequest)
}
func (s *Server) RemoveDomain(ctx context.Context, req *system_pb.RemoveDomainRequest) (*system_pb.RemoveDomainResponse, error) {
details, err := s.command.RemoveInstanceDomain(ctx, req.Domain)
details, err := s.command.RemoveInstanceDomain(ctx, req.Domain, true)
if err != nil {
return nil, err
}
+24 -47
View File
@@ -894,17 +894,6 @@ func (c *Commands) prepareSetDefaultOrg(a *instance.Aggregate, orgID string) pre
}
}
func (c *Commands) setIAMProject(ctx context.Context, iamAgg *eventstore.Aggregate, iamWriteModel *InstanceWriteModel, projectID string) (eventstore.Command, error) {
err := c.eventstore.FilterToQueryReducer(ctx, iamWriteModel)
if err != nil {
return nil, err
}
if iamWriteModel.ProjectID != "" {
return nil, zerrors.ThrowPreconditionFailed(nil, "IAM-EGbw2", "Errors.IAM.IAMProjectAlreadySet")
}
return instance.NewIAMProjectSetEvent(ctx, iamAgg, projectID), nil
}
func (c *Commands) prepareSetDefaultLanguage(a *instance.Aggregate, defaultLanguage language.Tag) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if err := domain.LanguageIsDefined(defaultLanguage); err != nil {
@@ -959,54 +948,42 @@ func getSystemConfigWriteModel(ctx context.Context, filter preparation.FilterToQ
return writeModel, err
}
func (c *Commands) RemoveInstance(ctx context.Context, id string) (*domain.ObjectDetails, error) {
func (c *Commands) RemoveInstance(ctx context.Context, id string, errorIfNotFound bool) (*domain.ObjectDetails, error) {
instID := strings.TrimSpace(id)
if instID == "" || len(instID) > 200 {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-VeS2zI", "Errors.Invalid.Argument")
}
instanceAgg := instance.NewAggregate(instID)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareRemoveInstance(instanceAgg))
writeModel, err := c.getInstanceWriteModelByID(ctx, instanceAgg.ID)
if err != nil {
return nil, err
}
if !writeModel.State.Exists() {
if errorIfNotFound {
return nil, zerrors.ThrowNotFound(err, "COMMA-AE3GS", "Errors.Instance.NotFound")
}
return nil, nil
}
milestoneAggregate := milestone.NewInstanceAggregate(instanceAgg.ID)
cmds := []eventstore.Command{
instance.NewInstanceRemovedEvent(ctx,
&instanceAgg.Aggregate,
writeModel.Name,
writeModel.Domains),
milestone.NewReachedEvent(ctx,
milestoneAggregate,
milestone.InstanceDeleted),
}
if err := c.pushAppendAndReduce(ctx, writeModel, cmds...); err != nil {
return nil, err
}
events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
err = c.caches.milestones.Invalidate(ctx, milestoneIndexInstanceID, id)
logging.OnError(err).Error("milestone invalidate")
return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreatedAt(),
ResourceOwner: events[len(events)-1].Aggregate().InstanceID,
}, nil
}
func (c *Commands) prepareRemoveInstance(a *instance.Aggregate) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel, err := c.getInstanceWriteModelByID(ctx, a.ID)
if err != nil {
return nil, zerrors.ThrowNotFound(err, "COMMA-pax9m3", "Errors.Instance.NotFound")
}
if !writeModel.State.Exists() {
return nil, zerrors.ThrowNotFound(err, "COMMA-AE3GS", "Errors.Instance.NotFound")
}
milestoneAggregate := milestone.NewInstanceAggregate(a.ID)
return []eventstore.Command{
instance.NewInstanceRemovedEvent(ctx,
&a.Aggregate,
writeModel.Name,
writeModel.Domains),
milestone.NewReachedEvent(ctx,
milestoneAggregate,
milestone.InstanceDeleted),
},
nil
}, nil
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) getInstanceWriteModelByID(ctx context.Context, orgID string) (*InstanceWriteModel, error) {
+24 -32
View File
@@ -17,7 +17,7 @@ import (
)
var (
allowDomainRunes = regexp.MustCompile("^[a-zA-Z0-9\\.\\-]+$")
allowDomainRunes = regexp.MustCompile(`^[a-zA-Z0-9\.\-]+$`)
)
func (c *Commands) AddInstanceDomain(ctx context.Context, instanceDomain string) (*domain.ObjectDetails, error) {
@@ -56,22 +56,35 @@ func (c *Commands) SetPrimaryInstanceDomain(ctx context.Context, instanceDomain
}, nil
}
func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain string) (*domain.ObjectDetails, error) {
func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain string, errorIfNotFound bool) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
validation := removeInstanceDomain(instanceAgg, instanceDomain)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation)
if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "INST-39nls", "Errors.Invalid.Argument")
}
//nolint: staticcheck
writeModel, err := getInstanceDomainWriteModel(ctx, c.eventstore.Filter, instanceDomain)
if err != nil {
return nil, err
}
events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
if writeModel.State != domain.InstanceDomainStateActive {
if errorIfNotFound {
return nil, zerrors.ThrowNotFound(nil, "INSTANCE-8ls9f", "Errors.Instance.Domain.NotFound")
}
return nil, nil
}
if writeModel.Generated {
return nil, zerrors.ThrowPreconditionFailed(nil, "INSTANCE-9hn3n", "Errors.Instance.Domain.GeneratedNotRemovable")
}
cmds := []eventstore.Command{instance.NewDomainRemovedEvent(ctx, &instanceAgg.Aggregate, instanceDomain)}
if err := c.pushAppendAndReduce(ctx, writeModel, cmds...); err != nil {
return nil, err
}
return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreatedAt(),
ResourceOwner: events[len(events)-1].Aggregate().InstanceID,
}, nil
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) addGeneratedInstanceDomain(ctx context.Context, a *instance.Aggregate, instanceName string) ([]preparation.Validation, error) {
@@ -182,27 +195,6 @@ func setPrimaryInstanceDomain(a *instance.Aggregate, instanceDomain string) prep
}
}
func removeInstanceDomain(a *instance.Aggregate, instanceDomain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "INST-39nls", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
domainWriteModel, err := getInstanceDomainWriteModel(ctx, filter, instanceDomain)
if err != nil {
return nil, err
}
if domainWriteModel.State != domain.InstanceDomainStateActive {
return nil, zerrors.ThrowNotFound(nil, "INSTANCE-8ls9f", "Errors.Instance.Domain.NotFound")
}
if domainWriteModel.Generated {
return nil, zerrors.ThrowPreconditionFailed(nil, "INSTANCE-9hn3n", "Errors.Instance.Domain.GeneratedNotRemovable")
}
return []eventstore.Command{instance.NewDomainRemovedEvent(ctx, &a.Aggregate, instanceDomain)}, nil
}, nil
}
}
func getInstanceDomainWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, domain string) (*InstanceDomainWriteModel, error) {
domainWriteModel := NewInstanceDomainWriteModel(ctx, domain)
events, err := filter(ctx, domainWriteModel.Query())
+23 -6
View File
@@ -313,8 +313,9 @@ func TestCommandSide_RemoveInstanceDomain(t *testing.T) {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
domain string
ctx context.Context
domain string
errorIfNotFound bool
}
type res struct {
want *domain.ObjectDetails
@@ -342,7 +343,7 @@ func TestCommandSide_RemoveInstanceDomain(t *testing.T) {
},
},
{
name: "domain not exists, precondition error",
name: "domain not exists and errorIfNotFound set to TRUE should return not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
@@ -350,13 +351,29 @@ func TestCommandSide_RemoveInstanceDomain(t *testing.T) {
),
},
args: args{
ctx: context.Background(),
domain: "domain.ch",
ctx: context.Background(),
domain: "domain.ch",
errorIfNotFound: true,
},
res: res{
err: zerrors.IsNotFound,
},
},
{
name: "domain not exists and errorIfNotFound set to FALSE should return no error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
domain: "domain.ch",
errorIfNotFound: false,
},
res: res{},
},
{
name: "remove domain, ok",
fields: fields{
@@ -420,7 +437,7 @@ func TestCommandSide_RemoveInstanceDomain(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.RemoveInstanceDomain(tt.args.ctx, tt.args.domain)
got, err := r.RemoveInstanceDomain(tt.args.ctx, tt.args.domain, tt.args.errorIfNotFound)
if tt.res.err == nil {
assert.NoError(t, err)
}
+53 -7
View File
@@ -1566,8 +1566,9 @@ func TestCommandSide_RemoveInstance(t *testing.T) {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
instanceID string
ctx context.Context
instanceID string
errorIfNotFound bool
}
type res struct {
want *domain.ObjectDetails
@@ -1613,13 +1614,28 @@ func TestCommandSide_RemoveInstance(t *testing.T) {
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
errorIfNotFound: true,
},
res: res{
err: zerrors.IsNotFound,
},
},
{
name: "instance not existing, no error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
errorIfNotFound: false,
},
res: res{},
},
{
name: "instance removed, not found error",
fields: fields{
@@ -1643,13 +1659,43 @@ func TestCommandSide_RemoveInstance(t *testing.T) {
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
errorIfNotFound: true,
},
res: res{
err: zerrors.IsNotFound,
},
},
{
name: "instance removed, no error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewInstanceAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
eventFromEventPusher(
instance.NewInstanceRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
nil,
),
),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
errorIfNotFound: false,
},
res: res{},
},
{
name: "instance remove, ok",
fields: fields{
@@ -1714,7 +1760,7 @@ func TestCommandSide_RemoveInstance(t *testing.T) {
milestones: noop.NewCache[milestoneIndex, string, *MilestonesReached](),
},
}
got, err := r.RemoveInstance(tt.args.ctx, tt.args.instanceID)
got, err := r.RemoveInstance(tt.args.ctx, tt.args.instanceID, tt.args.errorIfNotFound)
if tt.res.err == nil {
assert.NoError(t, err)
}
+6 -2
View File
@@ -33,7 +33,7 @@ func (c *Commands) AddTrustedDomain(ctx context.Context, trustedDomain string) (
return writeModelToObjectDetails(&model.WriteModel), nil
}
func (c *Commands) RemoveTrustedDomain(ctx context.Context, trustedDomain string) (*domain.ObjectDetails, error) {
func (c *Commands) RemoveTrustedDomain(ctx context.Context, trustedDomain string, errorIfNotFound bool) (*domain.ObjectDetails, error) {
trustedDomain = strings.TrimSpace(trustedDomain)
if trustedDomain == "" || len(trustedDomain) > 253 {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-ajAzwu", "Errors.Invalid.Argument")
@@ -48,8 +48,12 @@ func (c *Commands) RemoveTrustedDomain(ctx context.Context, trustedDomain string
return nil, err
}
if !slices.Contains(model.Domains, trustedDomain) {
return nil, zerrors.ThrowNotFound(nil, "COMMA-de3z9", "Errors.Instance.Domain.NotFound")
if errorIfNotFound {
return nil, zerrors.ThrowNotFound(nil, "COMMA-de3z9", "Errors.Instance.Domain.NotFound")
}
return nil, nil
}
err = c.pushAppendAndReduce(ctx, model, instance.NewTrustedDomainRemovedEvent(ctx, InstanceAggregateFromWriteModel(&model.WriteModel), trustedDomain))
if err != nil {
return nil, err
@@ -129,8 +129,9 @@ func TestCommands_RemoveTrustedDomain(t *testing.T) {
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
trustedDomain string
ctx context.Context
trustedDomain string
errorIfNotFound bool
}
type want struct {
details *domain.ObjectDetails
@@ -182,20 +183,35 @@ func TestCommands_RemoveTrustedDomain(t *testing.T) {
},
},
{
name: "domain does not exists, error",
name: "domain does not exist and flag to error set should return error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
trustedDomain: "domain.com",
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
trustedDomain: "domain.com",
errorIfNotFound: true,
},
want: want{
err: zerrors.ThrowNotFound(nil, "COMMA-de3z9", "Errors.Instance.Domain.NotFound"),
},
},
{
name: "domain does not exist and flag to error not set should return no error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
trustedDomain: "domain.com",
errorIfNotFound: false,
},
want: want{},
},
{
name: "domain remove ok",
fields: fields{
@@ -228,7 +244,7 @@ func TestCommands_RemoveTrustedDomain(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
}
got, err := c.RemoveTrustedDomain(tt.args.ctx, tt.args.trustedDomain)
got, err := c.RemoveTrustedDomain(tt.args.ctx, tt.args.trustedDomain, tt.args.errorIfNotFound)
assert.ErrorIs(t, err, tt.want.err)
assertObjectDetails(t, tt.want.details, got)
})