diff --git a/backend/v3/domain/domain.go b/backend/v3/domain/domain.go index 2632775a55..03e6c8e243 100644 --- a/backend/v3/domain/domain.go +++ b/backend/v3/domain/domain.go @@ -13,6 +13,13 @@ const ( DomainValidationTypeHTTP DomainValidationType = "http" ) +type DomainType string + +const ( + DomainTypeCustom DomainType = "custom" + DomainTypeTrusted DomainType = "trusted" +) + type domainColumns interface { // InstanceIDColumn returns the column for the instance id field. // `qualified` indicates if the column should be qualified with the table name. @@ -20,15 +27,9 @@ type domainColumns interface { // DomainColumn returns the column for the domain field. // `qualified` indicates if the column should be qualified with the table name. DomainColumn(qualified bool) database.Column - // IsVerifiedColumn returns the column for the is verified field. - // `qualified` indicates if the column should be qualified with the table name. - IsVerifiedColumn(qualified bool) database.Column // IsPrimaryColumn returns the column for the is primary field. // `qualified` indicates if the column should be qualified with the table name. IsPrimaryColumn(qualified bool) database.Column - // ValidationTypeColumn returns the column for the verification type field. - // `qualified` indicates if the column should be qualified with the table name. - ValidationTypeColumn(qualified bool) database.Column // CreatedAtColumn returns the column for the created at field. // `qualified` indicates if the column should be qualified with the table name. CreatedAtColumn(qualified bool) database.Column @@ -44,13 +45,9 @@ type domainConditions interface { DomainCondition(op database.TextOperation, domain string) database.Condition // IsPrimaryCondition returns a filter on the is primary field. IsPrimaryCondition(isPrimary bool) database.Condition - // IsVerifiedCondition returns a filter on the is verified field. - IsVerifiedCondition(isVerified bool) database.Condition } type domainChanges interface { - // SetVerified sets the is verified column to true. - SetVerified() database.Change // SetPrimary sets a domain as primary based on the condition. // All other domains will be set to non-primary. // @@ -62,9 +59,6 @@ type domainChanges interface { // - The domain is already primary. // - No domain matches the condition. SetPrimary() database.Change - // SetValidationType sets the verification type column. - // If the domain is already verified, this is a no-op. - SetValidationType(verificationType DomainValidationType) database.Change // SetUpdatedAt sets the updated at column. // This is used for reducing events. SetUpdatedAt(t time.Time) database.Change diff --git a/backend/v3/domain/instance_domain.go b/backend/v3/domain/instance_domain.go index 21d050dfd5..402a261d3c 100644 --- a/backend/v3/domain/instance_domain.go +++ b/backend/v3/domain/instance_domain.go @@ -8,30 +8,28 @@ import ( ) type InstanceDomain struct { - InstanceID string `json:"instanceId,omitempty" db:"instance_id"` - Domain string `json:"domain,omitempty" db:"domain"` - IsVerified bool `json:"isVerified,omitempty" db:"is_verified"` - IsPrimary bool `json:"isPrimary,omitempty" db:"is_primary"` - ValidationType *DomainValidationType `json:"validationType,omitempty" db:"validation_type"` + InstanceID string `json:"instanceId,omitempty" db:"instance_id"` + Domain string `json:"domain,omitempty" db:"domain"` + IsPrimary bool `json:"isPrimary,omitempty" db:"is_primary"` + Type DomainType `json:"type,omitempty" db:"type"` CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"` UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"` } type AddInstanceDomain struct { - InstanceID string `json:"instanceId,omitempty" db:"instance_id"` - Domain string `json:"domain,omitempty" db:"domain"` - IsVerified bool `json:"isVerified,omitempty" db:"is_verified"` - IsPrimary bool `json:"isPrimary,omitempty" db:"is_primary"` - IsGenerated bool `json:"isGenerated,omitempty" db:"is_generated"` - ValidationType *DomainValidationType `json:"validationType,omitempty" db:"validation_type"` + InstanceID string `json:"instanceId,omitempty" db:"instance_id"` + Domain string `json:"domain,omitempty" db:"domain"` + IsPrimary bool `json:"isPrimary,omitempty" db:"is_primary"` + IsGenerated bool `json:"isGenerated,omitempty" db:"is_generated"` + Type DomainType `json:"type,omitempty" db:"type"` // CreatedAt is the time when the domain was added. // It is set by the repository and should not be set by the caller. - CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"` + CreatedAt *time.Time `json:"createdAt,omitzero" db:"created_at"` // UpdatedAt is the time when the domain was last updated. // It is set by the repository and should not be set by the caller. - UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"` + UpdatedAt *time.Time `json:"updatedAt,omitzero" db:"updated_at"` } type instanceDomainColumns interface { @@ -39,14 +37,21 @@ type instanceDomainColumns interface { // IsGeneratedColumn returns the column for the is generated field. // `qualified` indicates if the column should be qualified with the table name. IsGeneratedColumn(qualified bool) database.Column + // TypeColumn returns the column for the type field. + // `qualified` indicates if the column should be qualified with the table name. + TypeColumn(qualified bool) database.Column } type instanceDomainConditions interface { domainConditions + // TypeCondition returns a filter for the type field. + TypeCondition(typ DomainType) database.Condition } type instanceDomainChanges interface { domainChanges + // SetType sets the type column. + SetType(typ DomainType) database.Change } type InstanceDomainRepository interface { diff --git a/backend/v3/domain/organization_domain.go b/backend/v3/domain/organization_domain.go index a4e3213cb0..09147f408c 100644 --- a/backend/v3/domain/organization_domain.go +++ b/backend/v3/domain/organization_domain.go @@ -38,17 +38,31 @@ type AddOrganizationDomain struct { type organizationDomainColumns interface { domainColumns // OrgIDColumn returns the column for the org id field. + // `qualified` indicates if the column should be qualified with the table name. OrgIDColumn(qualified bool) database.Column + // IsVerifiedColumn returns the column for the is verified field. + // `qualified` indicates if the column should be qualified with the table name. + IsVerifiedColumn(qualified bool) database.Column + // ValidationTypeColumn returns the column for the verification type field. + // `qualified` indicates if the column should be qualified with the table name. + ValidationTypeColumn(qualified bool) database.Column } type organizationDomainConditions interface { domainConditions // OrgIDCondition returns a filter on the org id field. OrgIDCondition(orgID string) database.Condition + // IsVerifiedCondition returns a filter on the is verified field. + IsVerifiedCondition(isVerified bool) database.Condition } type organizationDomainChanges interface { domainChanges + // SetVerified sets the is verified column to true. + SetVerified() database.Change + // SetValidationType sets the verification type column. + // If the domain is already verified, this is a no-op. + SetValidationType(verificationType DomainValidationType) database.Change } type OrganizationDomainRepository interface { diff --git a/backend/v3/storage/database/dialect/postgres/migration/003_domains_table/up.sql b/backend/v3/storage/database/dialect/postgres/migration/003_domains_table/up.sql index 8f5025fe6f..cfa651f1f7 100644 --- a/backend/v3/storage/database/dialect/postgres/migration/003_domains_table/up.sql +++ b/backend/v3/storage/database/dialect/postgres/migration/003_domains_table/up.sql @@ -3,20 +3,28 @@ CREATE TYPE zitadel.domain_validation_type AS ENUM ( , 'dns' ); +CREATE TYPE zitadel.domain_type AS ENUM ( + 'custom' + , 'trusted' +); + CREATE TABLE zitadel.instance_domains( instance_id TEXT NOT NULL , domain TEXT NOT NULL CHECK (LENGTH(domain) BETWEEN 1 AND 255) - , is_verified BOOLEAN NOT NULL DEFAULT FALSE - , is_primary BOOLEAN NOT NULL DEFAULT FALSE - , is_generated BOOLEAN NOT NULL DEFAULT FALSE - , validation_type zitadel.domain_validation_type + , is_primary BOOLEAN + , is_generated BOOLEAN + , type zitadel.domain_type NOT NULL - , created_at TIMESTAMP DEFAULT NOW() - , updated_at TIMESTAMP DEFAULT NOW() + , created_at TIMESTAMPTZ DEFAULT NOW() + , updated_at TIMESTAMPTZ DEFAULT NOW() , PRIMARY KEY (domain) , FOREIGN KEY (instance_id) REFERENCES zitadel.instances(id) ON DELETE CASCADE + + , CONSTRAINT primary_cannot_be_trusted CHECK (is_primary IS NULL OR type != 'trusted') + , CONSTRAINT generated_cannot_be_trusted CHECK (is_generated IS NULL OR type != 'trusted') + , CONSTRAINT custom_values_set CHECK (type = 'custom' AND is_primary IS NOT NULL AND is_generated IS NOT NULL) ); CREATE INDEX idx_instance_domain_instance ON zitadel.instance_domains(instance_id); @@ -29,8 +37,8 @@ CREATE TABLE zitadel.org_domains( , is_primary BOOLEAN NOT NULL DEFAULT FALSE , validation_type zitadel.domain_validation_type - , created_at TIMESTAMP DEFAULT NOW() - , updated_at TIMESTAMP DEFAULT NOW() + , created_at TIMESTAMPTZ DEFAULT NOW() + , updated_at TIMESTAMPTZ DEFAULT NOW() , PRIMARY KEY (instance_id, org_id, domain) @@ -91,7 +99,8 @@ BEGIN SET is_primary = FALSE, updated_at = NOW() WHERE instance_id = NEW.instance_id AND domain != NEW.domain - AND is_primary = TRUE; + AND is_primary = TRUE + AND type = 'custom'; RETURN NEW; END; diff --git a/backend/v3/storage/database/events_testing/events_test.go b/backend/v3/storage/database/events_testing/events_test.go index e1bc903bea..f2427ba4b5 100644 --- a/backend/v3/storage/database/events_testing/events_test.go +++ b/backend/v3/storage/database/events_testing/events_test.go @@ -4,6 +4,7 @@ package events_test import ( "context" + "log" "os" "testing" "time" @@ -14,11 +15,12 @@ import ( "github.com/zitadel/zitadel/backend/v3/storage/database" "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres" "github.com/zitadel/zitadel/internal/integration" + v2beta "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" "github.com/zitadel/zitadel/pkg/grpc/system" ) -const ConnString = "host=localhost port=5432 user=zitadel dbname=zitadel sslmode=disable" +const ConnString = "host=localhost port=5432 user=zitadel password=zitadel dbname=zitadel sslmode=disable" var ( dbPool *pgxpool.Pool @@ -35,12 +37,21 @@ func TestMain(m *testing.M) { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) defer cancel() - Instance = integration.NewInstance(ctx) + CTX = integration.WithSystemAuthorization(ctx) + Instance = integration.NewInstance(CTX) - CTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) SystemClient = integration.SystemClient() OrgClient = Instance.Client.OrgV2beta + defer func() { + _, err := Instance.Client.InstanceV2Beta.DeleteInstance(CTX, &v2beta.DeleteInstanceRequest{ + InstanceId: Instance.Instance.Id, + }) + if err != nil { + log.Printf("Failed to delete instance on cleanup: %v\n", err) + } + }() + var err error dbConfig, err := pgxpool.ParseConfig(ConnString) if err != nil { diff --git a/backend/v3/storage/database/events_testing/instance_domain_test.go b/backend/v3/storage/database/events_testing/instance_domain_test.go new file mode 100644 index 0000000000..de968e0707 --- /dev/null +++ b/backend/v3/storage/database/events_testing/instance_domain_test.go @@ -0,0 +1,224 @@ +//go:build integration + +package events_test + +import ( + "log" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + "github.com/zitadel/zitadel/internal/integration" + v2beta "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/system" +) + +func TestServer_TestInstanceDomainReduces(t *testing.T) { + instance := integration.NewInstance(CTX) + + instanceRepo := repository.InstanceRepository(pool) + instanceDomainRepo := instanceRepo.Domains(true) + + t.Cleanup(func() { + _, err := instance.Client.InstanceV2Beta.DeleteInstance(CTX, &v2beta.DeleteInstanceRequest{ + InstanceId: instance.Instance.Id, + }) + if err != nil { + t.Logf("Failed to delete instance on cleanup: %v", err) + } + }) + + // Wait for instance to be created + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + assert.EventuallyWithT(t, func(ttt *assert.CollectT) { + _, err := instanceRepo.Get(CTX, + database.WithCondition(instanceRepo.IDCondition(instance.Instance.Id)), + ) + assert.NoError(ttt, err) + }, retryDuration, tick) + + t.Run("test instance domain add reduces", func(t *testing.T) { + // Add a domain to the instance + domainName := gofakeit.DomainName() + beforeAdd := time.Now() + _, err := instance.Client.InstanceV2Beta.AddCustomDomain(CTX, &v2beta.AddCustomDomainRequest{ + InstanceId: instance.Instance.Id, + Domain: domainName, + }) + require.NoError(t, err) + afterAdd := time.Now() + + t.Cleanup(func() { + _, err := instance.Client.InstanceV2Beta.RemoveCustomDomain(CTX, &v2beta.RemoveCustomDomainRequest{ + InstanceId: instance.Instance.Id, + Domain: domainName, + }) + if err != nil { + t.Logf("Failed to delete instance domain on cleanup: %v", err) + } + }) + + // Test that domain add reduces + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + assert.EventuallyWithT(t, func(ttt *assert.CollectT) { + domain, err := instanceDomainRepo.Get(CTX, + database.WithCondition( + database.And( + instanceDomainRepo.InstanceIDCondition(instance.Instance.Id), + instanceDomainRepo.DomainCondition(database.TextOperationEqual, domainName), + ), + ), + ) + require.NoError(ttt, err) + // event instance.domain.added + assert.Equal(ttt, domainName, domain.Domain) + assert.Equal(ttt, instance.Instance.Id, domain.InstanceID) + assert.False(ttt, domain.IsPrimary) + log.Printf("created at %v\n", domain.CreatedAt) + log.Printf("after %v\n", afterAdd) + log.Printf("before %v\n", beforeAdd) + assert.WithinRange(ttt, domain.CreatedAt, beforeAdd, afterAdd) + assert.WithinRange(ttt, domain.UpdatedAt, beforeAdd, afterAdd) + }, retryDuration, tick) + }) + t.Run("test instance domain set primary reduces", func(t *testing.T) { + // Add a domain to the instance + domainName := gofakeit.DomainName() + _, err := instance.Client.InstanceV2Beta.AddCustomDomain(CTX, &v2beta.AddCustomDomainRequest{ + InstanceId: instance.Instance.Id, + Domain: domainName, + }) + require.NoError(t, err) + + t.Cleanup(func() { + // first we change the primary domain to something else + domain, err := instanceDomainRepo.Get(CTX, + database.WithCondition( + database.And( + instanceDomainRepo.InstanceIDCondition(instance.Instance.Id), + instanceDomainRepo.IsPrimaryCondition(false), + ), + ), + database.WithLimit(1), + ) + require.NoError(t, err) + _, err = SystemClient.SetPrimaryDomain(CTX, &system.SetPrimaryDomainRequest{ + InstanceId: instance.Instance.Id, + Domain: domain.Domain, + }) + require.NoError(t, err) + + _, err = instance.Client.InstanceV2Beta.RemoveCustomDomain(CTX, &v2beta.RemoveCustomDomainRequest{ + InstanceId: instance.Instance.Id, + Domain: domainName, + }) + if err != nil { + t.Logf("Failed to delete instance domain on cleanup: %v", err) + } + }) + + // Wait for domain to be created + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + assert.EventuallyWithT(t, func(ttt *assert.CollectT) { + domain, err := instanceDomainRepo.Get(CTX, + database.WithCondition( + database.And( + instanceDomainRepo.InstanceIDCondition(instance.Instance.Id), + instanceDomainRepo.DomainCondition(database.TextOperationEqual, domainName)), + ), + ) + require.NoError(ttt, err) + require.False(ttt, domain.IsPrimary) + assert.Equal(ttt, domainName, domain.Domain) + }, retryDuration, tick) + + // Set domain as primary + beforeSetPrimary := time.Now() + _, err = SystemClient.SetPrimaryDomain(CTX, &system.SetPrimaryDomainRequest{ + InstanceId: instance.Instance.Id, + Domain: domainName, + }) + require.NoError(t, err) + afterSetPrimary := time.Now() + + // Test that set primary reduces + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + assert.EventuallyWithT(t, func(ttt *assert.CollectT) { + domain, err := instanceDomainRepo.Get(CTX, + database.WithCondition( + database.And( + instanceDomainRepo.InstanceIDCondition(instance.Instance.Id), + instanceDomainRepo.IsPrimaryCondition(true), + ), + ), + ) + require.NoError(ttt, err) + // event instance.domain.primary.set + assert.Equal(ttt, domainName, domain.Domain) + assert.True(ttt, domain.IsPrimary) + assert.WithinRange(ttt, domain.UpdatedAt, beforeSetPrimary, afterSetPrimary) + }, retryDuration, tick) + }) + + t.Run("test instance domain remove reduces", func(t *testing.T) { + // Add a domain to the instance + domainName := gofakeit.DomainName() + _, err := instance.Client.InstanceV2Beta.AddCustomDomain(CTX, &v2beta.AddCustomDomainRequest{ + InstanceId: instance.Instance.Id, + Domain: domainName, + }) + require.NoError(t, err) + + t.Cleanup(func() { + _, err := instance.Client.InstanceV2Beta.RemoveCustomDomain(CTX, &v2beta.RemoveCustomDomainRequest{ + InstanceId: instance.Instance.Id, + Domain: domainName, + }) + if err != nil { + t.Logf("Failed to delete instance domain on cleanup: %v", err) + } + }) + + // Wait for domain to be created and verify it exists + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + assert.EventuallyWithT(t, func(ttt *assert.CollectT) { + _, err := instanceDomainRepo.Get(CTX, + database.WithCondition( + database.And( + instanceDomainRepo.InstanceIDCondition(instance.Instance.Id), + instanceDomainRepo.DomainCondition(database.TextOperationEqual, domainName), + ), + ), + ) + require.NoError(ttt, err) + }, retryDuration, tick) + + // Remove the domain + _, err = SystemClient.RemoveDomain(CTX, &system.RemoveDomainRequest{ + InstanceId: instance.Instance.Id, + Domain: domainName, + }) + require.NoError(t, err) + + // Test that domain remove reduces + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + assert.EventuallyWithT(t, func(ttt *assert.CollectT) { + domain, err := instanceDomainRepo.Get(CTX, + database.WithCondition( + database.And( + instanceDomainRepo.InstanceIDCondition(instance.Instance.Id), + instanceDomainRepo.DomainCondition(database.TextOperationEqual, domainName), + ), + ), + ) + // event instance.domain.removed + assert.Nil(ttt, domain) + require.ErrorIs(ttt, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} diff --git a/backend/v3/storage/database/events_testing/instance_test.go b/backend/v3/storage/database/events_testing/instance_test.go index 934de8eaee..50b919d877 100644 --- a/backend/v3/storage/database/events_testing/instance_test.go +++ b/backend/v3/storage/database/events_testing/instance_test.go @@ -17,6 +17,8 @@ import ( ) func TestServer_TestInstanceReduces(t *testing.T) { + instanceRepo := repository.InstanceRepository(pool) + t.Run("test instance add reduces", func(t *testing.T) { instanceName := gofakeit.Name() beforeCreate := time.Now() @@ -33,8 +35,15 @@ func TestServer_TestInstanceReduces(t *testing.T) { afterCreate := time.Now() require.NoError(t, err) + t.Cleanup(func() { + _, err = SystemClient.RemoveInstance(CTX, &system.RemoveInstanceRequest{ + InstanceId: instance.GetInstanceId(), + }) + if err != nil { + t.Logf("Failed to delete instance on cleanup: %v", err) + } + }) - instanceRepo := repository.InstanceRepository(pool) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) assert.EventuallyWithT(t, func(ttt *assert.CollectT) { instance, err := instanceRepo.Get(CTX, @@ -71,6 +80,14 @@ func TestServer_TestInstanceReduces(t *testing.T) { }, }) require.NoError(t, err) + t.Cleanup(func() { + _, err = SystemClient.RemoveInstance(CTX, &system.RemoveInstanceRequest{ + InstanceId: res.GetInstanceId(), + }) + if err != nil { + t.Logf("Failed to delete instance on cleanup: %v", err) + } + }) instanceName += "new" beforeUpdate := time.Now() @@ -78,10 +95,9 @@ func TestServer_TestInstanceReduces(t *testing.T) { InstanceId: res.InstanceId, InstanceName: instanceName, }) - require.NoError(t, err) afterUpdate := time.Now() + require.NoError(t, err) - instanceRepo := repository.InstanceRepository(pool) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) assert.EventuallyWithT(t, func(ttt *assert.CollectT) { instance, err := instanceRepo.Get(CTX, @@ -108,8 +124,6 @@ func TestServer_TestInstanceReduces(t *testing.T) { }) require.NoError(t, err) - instanceRepo := repository.InstanceRepository(pool) - // check instance exists retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) assert.EventuallyWithT(t, func(ttt *assert.CollectT) { diff --git a/backend/v3/storage/database/events_testing/organization_test.go b/backend/v3/storage/database/events_testing/organization_test.go index d7a6e622b7..7c89cfbcd5 100644 --- a/backend/v3/storage/database/events_testing/organization_test.go +++ b/backend/v3/storage/database/events_testing/organization_test.go @@ -19,25 +19,33 @@ import ( func TestServer_TestOrganizationReduces(t *testing.T) { instanceID := Instance.ID() + orgRepo := repository.OrganizationRepository(pool) t.Run("test org add reduces", func(t *testing.T) { beforeCreate := time.Now() orgName := gofakeit.Name() - _, err := OrgClient.CreateOrganization(CTX, &v2beta_org.CreateOrganizationRequest{ + org, err := OrgClient.CreateOrganization(CTX, &v2beta_org.CreateOrganizationRequest{ Name: orgName, }) require.NoError(t, err) afterCreate := time.Now() - orgRepo := repository.OrganizationRepository(pool) + t.Cleanup(func() { + _, err = OrgClient.DeleteOrganization(CTX, &v2beta_org.DeleteOrganizationRequest{ + Id: org.GetId(), + }) + if err != nil { + t.Logf("Failed to delete organization on cleanup: %v", err) + } + }) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) assert.EventuallyWithT(t, func(tt *assert.CollectT) { organization, err := orgRepo.Get(CTX, database.WithCondition( database.And( - orgRepo.NameCondition(orgName), + orgRepo.IDCondition(org.GetId()), orgRepo.InstanceIDCondition(instanceID), ), ), @@ -63,6 +71,15 @@ func TestServer_TestOrganizationReduces(t *testing.T) { }) require.NoError(t, err) + t.Cleanup(func() { + _, err = OrgClient.DeleteOrganization(CTX, &v2beta_org.DeleteOrganizationRequest{ + Id: organization.Id, + }) + if err != nil { + t.Logf("Failed to delete organization on cleanup: %v", err) + } + }) + // 2. update org name beforeUpdate := time.Now() orgName = orgName + "_new" @@ -73,14 +90,12 @@ func TestServer_TestOrganizationReduces(t *testing.T) { require.NoError(t, err) afterUpdate := time.Now() - orgRepo := repository.OrganizationRepository(pool) - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) assert.EventuallyWithT(t, func(t *assert.CollectT) { organization, err := orgRepo.Get(CTX, database.WithCondition( database.And( - orgRepo.NameCondition(orgName), + orgRepo.IDCondition(organization.Id), orgRepo.InstanceIDCondition(instanceID), ), ), @@ -101,6 +116,15 @@ func TestServer_TestOrganizationReduces(t *testing.T) { Name: orgName, }) require.NoError(t, err) + t.Cleanup(func() { + // Cleanup: delete the organization + _, err = OrgClient.DeleteOrganization(CTX, &v2beta_org.DeleteOrganizationRequest{ + Id: organization.Id, + }) + if err != nil { + t.Logf("Failed to delete organization on cleanup: %v", err) + } + }) // 2. deactivate org name beforeDeactivate := time.Now() @@ -111,14 +135,12 @@ func TestServer_TestOrganizationReduces(t *testing.T) { require.NoError(t, err) afterDeactivate := time.Now() - orgRepo := repository.OrganizationRepository(pool) - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) assert.EventuallyWithT(t, func(t *assert.CollectT) { organization, err := orgRepo.Get(CTX, database.WithCondition( database.And( - orgRepo.NameCondition(orgName), + orgRepo.IDCondition(organization.Id), orgRepo.InstanceIDCondition(instanceID), ), ), @@ -126,7 +148,6 @@ func TestServer_TestOrganizationReduces(t *testing.T) { require.NoError(t, err) // event org.deactivate - assert.Equal(t, orgName, organization.Name) assert.Equal(t, domain.OrgStateInactive, organization.State) assert.WithinRange(t, organization.UpdatedAt, beforeDeactivate, afterDeactivate) }, retryDuration, tick) @@ -140,6 +161,15 @@ func TestServer_TestOrganizationReduces(t *testing.T) { Name: orgName, }) require.NoError(t, err) + t.Cleanup(func() { + // Cleanup: delete the organization + _, err = OrgClient.DeleteOrganization(CTX, &v2beta_org.DeleteOrganizationRequest{ + Id: organization.Id, + }) + if err != nil { + t.Logf("Failed to delete organization on cleanup: %v", err) + } + }) // 2. deactivate org name _, err = OrgClient.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ @@ -154,14 +184,12 @@ func TestServer_TestOrganizationReduces(t *testing.T) { organization, err := orgRepo.Get(CTX, database.WithCondition( database.And( - orgRepo.NameCondition(orgName), + orgRepo.IDCondition(organization.Id), orgRepo.InstanceIDCondition(instanceID), ), ), ) require.NoError(t, err) - - assert.Equal(t, orgName, organization.Name) assert.Equal(t, domain.OrgStateInactive, organization.State) }, retryDuration, tick) @@ -178,7 +206,7 @@ func TestServer_TestOrganizationReduces(t *testing.T) { organization, err := orgRepo.Get(CTX, database.WithCondition( database.And( - orgRepo.NameCondition(orgName), + orgRepo.IDCondition(organization.Id), orgRepo.InstanceIDCondition(instanceID), ), ), @@ -201,24 +229,19 @@ func TestServer_TestOrganizationReduces(t *testing.T) { }) require.NoError(t, err) - // 2. check org retrivable + // 2. check org retrievable orgRepo := repository.OrganizationRepository(pool) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) assert.EventuallyWithT(t, func(t *assert.CollectT) { - organization, err := orgRepo.Get(CTX, + _, err := orgRepo.Get(CTX, database.WithCondition( database.And( - orgRepo.NameCondition(orgName), + orgRepo.IDCondition(organization.Id), orgRepo.InstanceIDCondition(instanceID), ), ), ) require.NoError(t, err) - - if organization == nil { - assert.Fail(t, "this error is here because of a race condition") - } - assert.Equal(t, orgName, organization.Name) }, retryDuration, tick) // 3. delete org @@ -232,7 +255,7 @@ func TestServer_TestOrganizationReduces(t *testing.T) { organization, err := orgRepo.Get(CTX, database.WithCondition( database.And( - orgRepo.NameCondition(orgName), + orgRepo.IDCondition(organization.Id), orgRepo.InstanceIDCondition(instanceID), ), ), diff --git a/backend/v3/storage/database/repository/instance.go b/backend/v3/storage/database/repository/instance.go index 85123280de..ac869e8a66 100644 --- a/backend/v3/storage/database/repository/instance.go +++ b/backend/v3/storage/database/repository/instance.go @@ -30,7 +30,7 @@ func InstanceRepository(client database.QueryExecutor) domain.InstanceRepository const ( queryInstanceStmt = `SELECT instances.id, instances.name, instances.default_org_id, instances.iam_project_id, instances.console_client_id, instances.console_app_id, instances.default_language, instances.created_at, instances.updated_at` + - ` , CASE WHEN count(instance_domains.domain) > 0 THEN jsonb_agg(json_build_object('domain', instance_domains.domain, 'isVerified', instance_domains.is_verified, 'isPrimary', instance_domains.is_primary, 'isGenerated', instance_domains.is_generated, 'validationType', instance_domains.validation_type, 'createdAt', instance_domains.created_at, 'updatedAt', instance_domains.updated_at)) ELSE NULL::JSONB END domains` + + ` , CASE WHEN count(instance_domains.domain) > 0 THEN jsonb_agg(json_build_object('domain', instance_domains.domain, 'isPrimary', instance_domains.is_primary, 'isGenerated', instance_domains.is_generated, 'createdAt', instance_domains.created_at, 'updatedAt', instance_domains.updated_at)) ELSE NULL::JSONB END domains` + ` FROM zitadel.instances` ) diff --git a/backend/v3/storage/database/repository/instance_domain.go b/backend/v3/storage/database/repository/instance_domain.go index 462d4528c5..0a0c970fe7 100644 --- a/backend/v3/storage/database/repository/instance_domain.go +++ b/backend/v3/storage/database/repository/instance_domain.go @@ -19,7 +19,7 @@ type instanceDomain struct { // repository // ------------------------------------------------------------- -const queryInstanceDomainStmt = `SELECT instance_domains.instance_id, instance_domains.domain, instance_domains.is_verified, instance_domains.is_primary, instance_domains.validation_type, instance_domains.created_at, instance_domains.updated_at ` + +const queryInstanceDomainStmt = `SELECT instance_domains.instance_id, instance_domains.domain, instance_domains.is_primary, instance_domains.created_at, instance_domains.updated_at ` + `FROM zitadel.instance_domains` // Get implements [domain.InstanceDomainRepository]. @@ -56,11 +56,11 @@ func (i *instanceDomain) List(ctx context.Context, opts ...database.QueryOption) func (i *instanceDomain) Add(ctx context.Context, domain *domain.AddInstanceDomain) error { var builder database.StatementBuilder - builder.WriteString(`INSERT INTO zitadel.instance_domains (instance_id, domain, is_verified, is_primary, validation_type) VALUES (`) - builder.WriteArgs(domain.InstanceID, domain.Domain, domain.IsVerified, domain.IsPrimary, domain.ValidationType) + builder.WriteString(`INSERT INTO zitadel.instance_domains (instance_id, domain, is_primary, is_generated, type, created_at, updated_at) VALUES (`) + builder.WriteArgs(domain.InstanceID, domain.Domain, domain.IsPrimary, domain.IsGenerated, domain.Type, domain.CreatedAt, domain.UpdatedAt) builder.WriteString(`) RETURNING created_at, updated_at`) - return i.client.QueryRow(ctx, builder.String(), builder.Args()...).Scan(&domain.CreatedAt, &domain.UpdatedAt) + return i.client.QueryRow(ctx, builder.String(), builder.Args()...).Scan(domain.CreatedAt, domain.UpdatedAt) } // Remove implements [domain.InstanceDomainRepository]. @@ -93,26 +93,21 @@ func (i *instanceDomain) Update(ctx context.Context, condition database.Conditio // changes // ------------------------------------------------------------- -// SetValidationType implements [domain.InstanceDomainRepository]. -func (i instanceDomain) SetValidationType(verificationType domain.DomainValidationType) database.Change { - return database.NewChange(i.ValidationTypeColumn(false), verificationType) -} - // SetPrimary implements [domain.InstanceDomainRepository]. func (i instanceDomain) SetPrimary() database.Change { return database.NewChange(i.IsPrimaryColumn(false), true) } -// SetVerified implements [domain.InstanceDomainRepository]. -func (i instanceDomain) SetVerified() database.Change { - return database.NewChange(i.IsVerifiedColumn(false), true) -} - // SetUpdatedAt implements [domain.OrganizationDomainRepository]. func (i instanceDomain) SetUpdatedAt(updatedAt time.Time) database.Change { return database.NewChange(i.UpdatedAtColumn(false), updatedAt) } +// SetType implements [domain.InstanceDomainRepository]. +func (i instanceDomain) SetType(typ domain.DomainType) database.Change { + return database.NewChange(i.TypeColumn(false), typ) +} + // ------------------------------------------------------------- // conditions // ------------------------------------------------------------- @@ -132,9 +127,9 @@ func (i instanceDomain) IsPrimaryCondition(isPrimary bool) database.Condition { return database.NewBooleanCondition(i.IsPrimaryColumn(true), isPrimary) } -// IsVerifiedCondition implements [domain.InstanceDomainRepository]. -func (i instanceDomain) IsVerifiedCondition(isVerified bool) database.Condition { - return database.NewBooleanCondition(i.IsVerifiedColumn(true), isVerified) +// TypeCondition implements [domain.InstanceDomainRepository]. +func (i instanceDomain) TypeCondition(typ domain.DomainType) database.Condition { + return database.NewTextCondition(i.TypeColumn(true), database.TextOperationEqual, typ) } // ------------------------------------------------------------- @@ -174,14 +169,6 @@ func (instanceDomain) IsPrimaryColumn(qualified bool) database.Column { return database.NewColumn("is_primary") } -// IsVerifiedColumn implements [domain.InstanceDomainRepository]. -func (instanceDomain) IsVerifiedColumn(qualified bool) database.Column { - if qualified { - return database.NewColumn("instance_domains.is_verified") - } - return database.NewColumn("is_verified") -} - // UpdatedAtColumn implements [domain.InstanceDomainRepository]. // Subtle: this method shadows the method ([domain.InstanceRepository]).UpdatedAtColumn of instanceDomain.instance. func (instanceDomain) UpdatedAtColumn(qualified bool) database.Column { @@ -191,14 +178,6 @@ func (instanceDomain) UpdatedAtColumn(qualified bool) database.Column { return database.NewColumn("updated_at") } -// ValidationTypeColumn implements [domain.InstanceDomainRepository]. -func (instanceDomain) ValidationTypeColumn(qualified bool) database.Column { - if qualified { - return database.NewColumn("instance_domains.validation_type") - } - return database.NewColumn("validation_type") -} - // IsGeneratedColumn implements [domain.InstanceDomainRepository]. func (instanceDomain) IsGeneratedColumn(qualified bool) database.Column { if qualified { @@ -207,6 +186,14 @@ func (instanceDomain) IsGeneratedColumn(qualified bool) database.Column { return database.NewColumn("is_generated") } +// TypeColumn implements [domain.InstanceDomainRepository]. +func (instanceDomain) TypeColumn(qualified bool) database.Column { + if qualified { + return database.NewColumn("instance_domains.type") + } + return database.NewColumn("type") +} + // ------------------------------------------------------------- // scanners // ------------------------------------------------------------- diff --git a/backend/v3/storage/database/repository/instance_domain_test.go b/backend/v3/storage/database/repository/instance_domain_test.go index 0c0034d24a..168af16b27 100644 --- a/backend/v3/storage/database/repository/instance_domain_test.go +++ b/backend/v3/storage/database/repository/instance_domain_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/brianvoe/gofakeit/v6" - "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -39,41 +38,25 @@ func TestAddInstanceDomain(t *testing.T) { { name: "happy path", instanceDomain: domain.AddInstanceDomain{ - InstanceID: instanceID, - Domain: gofakeit.DomainName(), - IsVerified: false, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), - }, - }, - { - name: "add verified domain", - instanceDomain: domain.AddInstanceDomain{ - InstanceID: instanceID, - Domain: gofakeit.DomainName(), - IsVerified: true, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeHTTP), + InstanceID: instanceID, + Domain: gofakeit.DomainName(), + IsPrimary: false, }, }, { name: "add primary domain", instanceDomain: domain.AddInstanceDomain{ - InstanceID: instanceID, - Domain: gofakeit.DomainName(), - IsVerified: true, - IsPrimary: true, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + InstanceID: instanceID, + Domain: gofakeit.DomainName(), + IsPrimary: true, }, }, { name: "add domain without domain name", instanceDomain: domain.AddInstanceDomain{ - InstanceID: instanceID, - Domain: "", - IsVerified: false, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + InstanceID: instanceID, + Domain: "", + IsPrimary: false, }, err: new(database.CheckError), }, @@ -83,11 +66,9 @@ func TestAddInstanceDomain(t *testing.T) { domainName := gofakeit.DomainName() instanceDomain := &domain.AddInstanceDomain{ - InstanceID: instanceID, - Domain: domainName, - IsVerified: false, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + InstanceID: instanceID, + Domain: domainName, + IsPrimary: false, } err := domainRepo.Add(ctx, instanceDomain) @@ -95,11 +76,9 @@ func TestAddInstanceDomain(t *testing.T) { // return same domain again return &domain.AddInstanceDomain{ - InstanceID: instanceID, - Domain: domainName, - IsVerified: true, - IsPrimary: true, - ValidationType: gu.Ptr(domain.DomainValidationTypeHTTP), + InstanceID: instanceID, + Domain: domainName, + IsPrimary: true, } }, err: new(database.UniqueError), @@ -107,21 +86,17 @@ func TestAddInstanceDomain(t *testing.T) { { name: "add domain with non-existent instance", instanceDomain: domain.AddInstanceDomain{ - InstanceID: "non-existent-instance", - Domain: gofakeit.DomainName(), - IsVerified: false, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + InstanceID: "non-existent-instance", + Domain: gofakeit.DomainName(), + IsPrimary: false, }, err: new(database.ForeignKeyError), }, { name: "add domain without instance id", instanceDomain: domain.AddInstanceDomain{ - Domain: gofakeit.DomainName(), - IsVerified: false, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + Domain: gofakeit.DomainName(), + IsPrimary: false, }, err: new(database.ForeignKeyError), }, @@ -186,18 +161,14 @@ func TestGetInstanceDomain(t *testing.T) { domainName2 := gofakeit.DomainName() domain1 := &domain.AddInstanceDomain{ - InstanceID: instanceID, - Domain: domainName1, - IsVerified: true, - IsPrimary: true, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + InstanceID: instanceID, + Domain: domainName1, + IsPrimary: true, } domain2 := &domain.AddInstanceDomain{ - InstanceID: instanceID, - Domain: domainName2, - IsVerified: false, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeHTTP), + InstanceID: instanceID, + Domain: domainName2, + IsPrimary: false, } err = domainRepo.Add(t.Context(), domain1) @@ -217,11 +188,9 @@ func TestGetInstanceDomain(t *testing.T) { database.WithCondition(domainRepo.IsPrimaryCondition(true)), }, expected: &domain.InstanceDomain{ - InstanceID: instanceID, - Domain: domainName1, - IsVerified: true, - IsPrimary: true, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + InstanceID: instanceID, + Domain: domainName1, + IsPrimary: true, }, }, { @@ -230,24 +199,9 @@ func TestGetInstanceDomain(t *testing.T) { database.WithCondition(domainRepo.DomainCondition(database.TextOperationEqual, domainName2)), }, expected: &domain.InstanceDomain{ - InstanceID: instanceID, - Domain: domainName2, - IsVerified: false, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeHTTP), - }, - }, - { - name: "get verified domain", - opts: []database.QueryOption{ - database.WithCondition(domainRepo.IsVerifiedCondition(true)), - }, - expected: &domain.InstanceDomain{ - InstanceID: instanceID, - Domain: domainName1, - IsVerified: true, - IsPrimary: true, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + InstanceID: instanceID, + Domain: domainName2, + IsPrimary: false, }, }, { @@ -272,9 +226,7 @@ func TestGetInstanceDomain(t *testing.T) { require.NoError(t, err) assert.Equal(t, test.expected.InstanceID, result.InstanceID) assert.Equal(t, test.expected.Domain, result.Domain) - assert.Equal(t, test.expected.IsVerified, result.IsVerified) assert.Equal(t, test.expected.IsPrimary, result.IsPrimary) - assert.Equal(t, test.expected.ValidationType, result.ValidationType) assert.NotEmpty(t, result.CreatedAt) assert.NotEmpty(t, result.UpdatedAt) }) @@ -307,25 +259,19 @@ func TestListInstanceDomains(t *testing.T) { domainRepo := instanceRepo.Domains(false) domains := []domain.AddInstanceDomain{ { - InstanceID: instanceID, - Domain: gofakeit.DomainName(), - IsVerified: true, - IsPrimary: true, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + InstanceID: instanceID, + Domain: gofakeit.DomainName(), + IsPrimary: true, }, { - InstanceID: instanceID, - Domain: gofakeit.DomainName(), - IsVerified: false, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeHTTP), + InstanceID: instanceID, + Domain: gofakeit.DomainName(), + IsPrimary: false, }, { - InstanceID: instanceID, - Domain: gofakeit.DomainName(), - IsVerified: true, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + InstanceID: instanceID, + Domain: gofakeit.DomainName(), + IsPrimary: false, }, } @@ -344,13 +290,6 @@ func TestListInstanceDomains(t *testing.T) { opts: []database.QueryOption{}, expectedCount: 3, }, - { - name: "list verified domains", - opts: []database.QueryOption{ - database.WithCondition(domainRepo.IsVerifiedCondition(true)), - }, - expectedCount: 2, - }, { name: "list primary domains", opts: []database.QueryOption{ @@ -419,11 +358,9 @@ func TestUpdateInstanceDomain(t *testing.T) { domainRepo := instanceRepo.Domains(false) domainName := gofakeit.DomainName() instanceDomain := &domain.AddInstanceDomain{ - InstanceID: instanceID, - Domain: domainName, - IsVerified: false, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + InstanceID: instanceID, + Domain: domainName, + IsPrimary: false, } err = domainRepo.Add(t.Context(), instanceDomain) @@ -436,38 +373,16 @@ func TestUpdateInstanceDomain(t *testing.T) { expected int64 err error }{ - { - name: "set verified", - condition: domainRepo.DomainCondition(database.TextOperationEqual, domainName), - changes: []database.Change{domainRepo.SetVerified()}, - expected: 1, - }, { name: "set primary", condition: domainRepo.DomainCondition(database.TextOperationEqual, domainName), changes: []database.Change{domainRepo.SetPrimary()}, expected: 1, }, - { - name: "set validation type", - condition: domainRepo.DomainCondition(database.TextOperationEqual, domainName), - changes: []database.Change{domainRepo.SetValidationType(domain.DomainValidationTypeHTTP)}, - expected: 1, - }, - { - name: "multiple changes", - condition: domainRepo.DomainCondition(database.TextOperationEqual, domainName), - changes: []database.Change{ - domainRepo.SetVerified(), - domainRepo.SetPrimary(), - domainRepo.SetValidationType(domain.DomainValidationTypeDNS), - }, - expected: 1, - }, { name: "update non-existent domain", condition: domainRepo.DomainCondition(database.TextOperationEqual, "non-existent.com"), - changes: []database.Change{domainRepo.SetVerified()}, + changes: []database.Change{domainRepo.SetPrimary()}, expected: 0, }, { @@ -533,18 +448,14 @@ func TestRemoveInstanceDomain(t *testing.T) { domainName2 := gofakeit.DomainName() domain1 := &domain.AddInstanceDomain{ - InstanceID: instanceID, - Domain: domainName1, - IsVerified: true, - IsPrimary: true, - ValidationType: gu.Ptr(domain.DomainValidationTypeDNS), + InstanceID: instanceID, + Domain: domainName1, + IsPrimary: true, } domain2 := &domain.AddInstanceDomain{ - InstanceID: instanceID, - Domain: domainName2, - IsVerified: false, - IsPrimary: false, - ValidationType: gu.Ptr(domain.DomainValidationTypeHTTP), + InstanceID: instanceID, + Domain: domainName2, + IsPrimary: false, } err = domainRepo.Add(t.Context(), domain1) @@ -628,16 +539,6 @@ func TestInstanceDomainConditions(t *testing.T) { condition: domainRepo.IsPrimaryCondition(false), expected: "instance_domains.is_primary = $1", }, - { - name: "is verified true", - condition: domainRepo.IsVerifiedCondition(true), - expected: "instance_domains.is_verified = $1", - }, - { - name: "is verified false", - condition: domainRepo.IsVerifiedCondition(false), - expected: "instance_domains.is_verified = $1", - }, } for _, test := range tests { @@ -658,26 +559,11 @@ func TestInstanceDomainChanges(t *testing.T) { change database.Change expected string }{ - { - name: "set verified", - change: domainRepo.SetVerified(), - expected: "is_verified = $1", - }, { name: "set primary", change: domainRepo.SetPrimary(), expected: "is_primary = $1", }, - { - name: "set validation type DNS", - change: domainRepo.SetValidationType(domain.DomainValidationTypeDNS), - expected: "validation_type = $1", - }, - { - name: "set validation type HTTP", - change: domainRepo.SetValidationType(domain.DomainValidationTypeHTTP), - expected: "validation_type = $1", - }, } for _, test := range tests { diff --git a/internal/integration/config/postgres.yaml b/internal/integration/config/postgres.yaml index 904f973d56..7fed3cdac6 100644 --- a/internal/integration/config/postgres.yaml +++ b/internal/integration/config/postgres.yaml @@ -9,6 +9,7 @@ Database: SSL: Mode: disable Admin: - Username: zitadel + Username: adlerhurst + Password: password SSL: Mode: disable diff --git a/internal/query/projection/instance_domain_relational.go b/internal/query/projection/instance_domain_relational.go index e52c8e95cc..111996de59 100644 --- a/internal/query/projection/instance_domain_relational.go +++ b/internal/query/projection/instance_domain_relational.go @@ -4,6 +4,8 @@ import ( "context" "database/sql" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/backend/v3/domain" "github.com/zitadel/zitadel/backend/v3/storage/database" v3_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql" @@ -31,7 +33,7 @@ func (p *instanceDomainRelationalProjection) Reducers() []handler.AggregateReduc EventReducers: []handler.EventReducer{ { Event: instance.InstanceDomainAddedEventType, - Reduce: p.reduceDomainAdded, + Reduce: p.reduceCustomDomainAdded, }, { Event: instance.InstanceDomainPrimarySetEventType, @@ -39,14 +41,22 @@ func (p *instanceDomainRelationalProjection) Reducers() []handler.AggregateReduc }, { Event: instance.InstanceDomainRemovedEventType, - Reduce: p.reduceDomainRemoved, + Reduce: p.reduceCustomDomainRemoved, + }, + { + Event: instance.TrustedDomainAddedEventType, + Reduce: p.reduceTrustedDomainAdded, + }, + { + Event: instance.TrustedDomainRemovedEventType, + Reduce: p.reduceTrustedDomainRemoved, }, }, }, } } -func (p *instanceDomainRelationalProjection) reduceDomainAdded(event eventstore.Event) (*handler.Statement, error) { +func (p *instanceDomainRelationalProjection) reduceCustomDomainAdded(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*instance.DomainAddedEvent) if !ok { return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-DU0xF", "reduce.wrong.event.type %s", instance.InstanceDomainAddedEventType) @@ -60,8 +70,9 @@ func (p *instanceDomainRelationalProjection) reduceDomainAdded(event eventstore. InstanceID: e.Aggregate().InstanceID, Domain: e.Domain, IsGenerated: e.Generated, - CreatedAt: e.CreationDate(), - UpdatedAt: e.CreationDate(), + Type: domain.DomainTypeCustom, + CreatedAt: gu.Ptr(e.CreationDate()), + UpdatedAt: gu.Ptr(e.CreationDate()), }) }), nil } @@ -81,6 +92,7 @@ func (p *instanceDomainRelationalProjection) reduceDomainPrimarySet(event events database.And( domainRepo.InstanceIDCondition(e.Aggregate().InstanceID), domainRepo.DomainCondition(database.TextOperationEqual, e.Domain), + domainRepo.TypeCondition(domain.DomainTypeCustom), ), domainRepo.SetPrimary(), domainRepo.SetUpdatedAt(e.CreationDate()), @@ -89,7 +101,7 @@ func (p *instanceDomainRelationalProjection) reduceDomainPrimarySet(event events }), nil } -func (p *instanceDomainRelationalProjection) reduceDomainRemoved(event eventstore.Event) (*handler.Statement, error) { +func (p *instanceDomainRelationalProjection) reduceCustomDomainRemoved(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*instance.DomainRemovedEvent) if !ok { return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Hhcdl", "reduce.wrong.event.type %s", instance.InstanceDomainRemovedEventType) @@ -104,6 +116,49 @@ func (p *instanceDomainRelationalProjection) reduceDomainRemoved(event eventstor database.And( domainRepo.InstanceIDCondition(e.Aggregate().InstanceID), domainRepo.DomainCondition(database.TextOperationEqual, e.Domain), + domainRepo.TypeCondition(domain.DomainTypeCustom), + ), + ) + return err + }), nil +} + +func (p *instanceDomainRelationalProjection) reduceTrustedDomainAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*instance.TrustedDomainAddedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-svHDh", "reduce.wrong.event.type %s", instance.TrustedDomainAddedEventType) + } + return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-gx7tQ", "reduce.wrong.db.pool %T", ex) + } + return repository.InstanceRepository(v3_sql.SQLTx(tx)).Domains(false).Add(ctx, &domain.AddInstanceDomain{ + InstanceID: e.Aggregate().InstanceID, + Domain: e.Domain, + Type: domain.DomainTypeCustom, + CreatedAt: gu.Ptr(e.CreationDate()), + UpdatedAt: gu.Ptr(e.CreationDate()), + }) + }), nil +} + +func (p *instanceDomainRelationalProjection) reduceTrustedDomainRemoved(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*instance.TrustedDomainRemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-4K74E", "reduce.wrong.event.type %s", instance.TrustedDomainRemovedEventType) + } + return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-D68ap", "reduce.wrong.db.pool %T", ex) + } + domainRepo := repository.InstanceRepository(v3_sql.SQLTx(tx)).Domains(false) + _, err := domainRepo.Remove(ctx, + database.And( + domainRepo.InstanceIDCondition(e.Aggregate().InstanceID), + domainRepo.DomainCondition(database.TextOperationEqual, e.Domain), + domainRepo.TypeCondition(domain.DomainTypeTrusted), ), ) return err diff --git a/internal/query/projection/org_domain_relational.go b/internal/query/projection/org_domain_relational.go index 81f3a6a812..a60e869012 100644 --- a/internal/query/projection/org_domain_relational.go +++ b/internal/query/projection/org_domain_relational.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/backend/v3/storage/database" v3_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql" "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + old_domain "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/repository/org" @@ -31,22 +32,30 @@ func (p *orgDomainRelationalProjection) Reducers() []handler.AggregateReducer { EventReducers: []handler.EventReducer{ { Event: org.OrgDomainAddedEventType, - Reduce: p.reduceDomainAdded, + Reduce: p.reduceAdded, }, { Event: org.OrgDomainPrimarySetEventType, - Reduce: p.reduceDomainPrimarySet, + Reduce: p.reducePrimarySet, }, { Event: org.OrgDomainRemovedEventType, - Reduce: p.reduceDomainRemoved, + Reduce: p.reduceRemoved, + }, + { + Event: org.OrgDomainVerificationAddedEventType, + Reduce: p.reduceVerificationAdded, + }, + { + Event: org.OrgDomainVerifiedEventType, + Reduce: p.reduceVerified, }, }, }, } } -func (p *orgDomainRelationalProjection) reduceDomainAdded(event eventstore.Event) (*handler.Statement, error) { +func (p *orgDomainRelationalProjection) reduceAdded(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*org.DomainAddedEvent) if !ok { return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-ZX9Fw", "reduce.wrong.event.type %s", org.OrgDomainAddedEventType) @@ -66,7 +75,7 @@ func (p *orgDomainRelationalProjection) reduceDomainAdded(event eventstore.Event }), nil } -func (p *orgDomainRelationalProjection) reduceDomainPrimarySet(event eventstore.Event) (*handler.Statement, error) { +func (p *orgDomainRelationalProjection) reducePrimarySet(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*org.DomainPrimarySetEvent) if !ok { return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-dmFdb", "reduce.wrong.event.type %s", org.OrgDomainPrimarySetEventType) @@ -90,7 +99,7 @@ func (p *orgDomainRelationalProjection) reduceDomainPrimarySet(event eventstore. }), nil } -func (p *orgDomainRelationalProjection) reduceDomainRemoved(event eventstore.Event) (*handler.Statement, error) { +func (p *orgDomainRelationalProjection) reduceRemoved(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*org.DomainRemovedEvent) if !ok { return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-MzC0n", "reduce.wrong.event.type %s", org.OrgDomainRemovedEventType) @@ -111,3 +120,58 @@ func (p *orgDomainRelationalProjection) reduceDomainRemoved(event eventstore.Eve return err }), nil } + +func (p *orgDomainRelationalProjection) reduceVerificationAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*org.DomainVerificationAddedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-oGzip", "reduce.wrong.event.type %s", org.OrgDomainVerificationAddedEventType) + } + var validationType domain.DomainValidationType + switch e.ValidationType { + case old_domain.OrgDomainValidationTypeDNS: + validationType = domain.DomainValidationTypeDNS + case old_domain.OrgDomainValidationTypeHTTP: + validationType = domain.DomainValidationTypeHTTP + case old_domain.OrgDomainValidationTypeUnspecified: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-FJfKB", "reduce.unsupported.validation.type %v", e.ValidationType) + } + return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-yF03i", "reduce.wrong.db.pool %T", ex) + } + domainRepo := repository.OrganizationRepository(v3_sql.SQLTx(tx)).Domains(false) + _, err := domainRepo.Update(ctx, + database.And( + domainRepo.InstanceIDCondition(e.Aggregate().InstanceID), + domainRepo.OrgIDCondition(e.Aggregate().ResourceOwner), + domainRepo.DomainCondition(database.TextOperationEqual, e.Domain), + ), + domainRepo.SetValidationType(validationType), + ) + return err + }), nil +} + +func (p *orgDomainRelationalProjection) reduceVerified(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*org.DomainVerifiedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-7WrI2", "reduce.wrong.event.type %s", org.OrgDomainVerifiedEventType) + } + return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-0ZGqC", "reduce.wrong.db.pool %T", ex) + } + domainRepo := repository.OrganizationRepository(v3_sql.SQLTx(tx)).Domains(false) + _, err := domainRepo.Update(ctx, + database.And( + domainRepo.InstanceIDCondition(e.Aggregate().InstanceID), + domainRepo.OrgIDCondition(e.Aggregate().ResourceOwner), + domainRepo.DomainCondition(database.TextOperationEqual, e.Domain), + ), + domainRepo.SetVerified(), + ) + return err + }), nil +}