feat: custom domain feature (#1618)

* fix: custom domain

* fix: custom domain

* fix: custom domain

* fix: custom domain feature in proto

* fix: remove custom domains on feature downgrade

* fix test

* fix: custom domain feature in proto

* ensure tests work

Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>
This commit is contained in:
Livio Amstutz
2021-04-19 16:43:36 +02:00
committed by GitHub
parent 8da733315a
commit 6863aeac59
20 changed files with 692 additions and 70 deletions

View File

@@ -23,6 +23,7 @@ type FeaturesWriteModel struct {
LoginPolicyUsernameLogin bool
PasswordComplexityPolicy bool
LabelPolicy bool
CustomDomain bool
}
func (wm *FeaturesWriteModel) Reduce() error {
@@ -66,6 +67,9 @@ func (wm *FeaturesWriteModel) Reduce() error {
if e.LabelPolicy != nil {
wm.LabelPolicy = *e.LabelPolicy
}
if e.CustomDomain != nil {
wm.CustomDomain = *e.CustomDomain
}
case *features.FeaturesRemovedEvent:
wm.State = domain.FeaturesStateRemoved
}

View File

@@ -47,6 +47,7 @@ func (c *Commands) setDefaultFeatures(ctx context.Context, existingFeatures *IAM
features.LoginPolicyUsernameLogin,
features.PasswordComplexityPolicy,
features.LabelPolicy,
features.CustomDomain,
)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged")

View File

@@ -62,7 +62,8 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent(
loginPolicyRegistration,
loginPolicyUsernameLogin,
passwordComplexityPolicy,
labelPolicy bool,
labelPolicy,
customDomain bool,
) (*iam.FeaturesSetEvent, bool) {
changes := make([]features.FeaturesChanges, 0)
@@ -103,6 +104,9 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent(
if wm.LabelPolicy != labelPolicy {
changes = append(changes, features.ChangeLabelPolicy(labelPolicy))
}
if wm.CustomDomain != customDomain {
changes = append(changes, features.ChangeCustomDomain(customDomain))
}
if len(changes) == 0 {
return nil, false

View File

@@ -2,13 +2,14 @@ package command
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/eventstore"
http_utils "github.com/caos/zitadel/internal/api/http"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
)
@@ -206,6 +207,38 @@ func (c *Commands) addOrgDomain(ctx context.Context, orgAgg *eventstore.Aggregat
return events, nil
}
func (c *Commands) removeCustomDomains(ctx context.Context, orgID string) ([]eventstore.EventPusher, error) {
orgDomains := NewOrgDomainsWriteModel(orgID)
err := c.eventstore.FilterToQueryReducer(ctx, orgDomains)
if err != nil {
return nil, err
}
hasDefault := false
defaultDomain := domain.NewIAMDomainName(orgDomains.OrgName, c.iamDomain)
isPrimary := defaultDomain == orgDomains.PrimaryDomain
orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel)
events := make([]eventstore.EventPusher, 0, len(orgDomains.Domains))
for _, orgDomain := range orgDomains.Domains {
if orgDomain.State == domain.OrgDomainStateActive {
if orgDomain.Domain == defaultDomain {
hasDefault = true
continue
}
events = append(events, org.NewDomainRemovedEvent(ctx, orgAgg, orgDomain.Domain, orgDomain.Verified))
}
}
if !hasDefault {
return append([]eventstore.EventPusher{
org.NewDomainAddedEvent(ctx, orgAgg, defaultDomain),
org.NewDomainPrimarySetEvent(ctx, orgAgg, defaultDomain),
}, events...), nil
}
if !isPrimary {
return append([]eventstore.EventPusher{org.NewDomainPrimarySetEvent(ctx, orgAgg, defaultDomain)}, events...), nil
}
return events, nil
}
func (c *Commands) getOrgDomainWriteModel(ctx context.Context, orgID, domain string) (*OrgDomainWriteModel, error) {
domainWriteModel := NewOrgDomainWriteModel(orgID, domain)
err := c.eventstore.FilterToQueryReducer(ctx, domainWriteModel)

View File

@@ -95,3 +95,72 @@ func (wm *OrgDomainWriteModel) Query() *eventstore.SearchQueryBuilder {
org.OrgDomainPrimarySetEventType,
org.OrgDomainRemovedEventType)
}
type OrgDomainsWriteModel struct {
eventstore.WriteModel
Domains []*Domain
PrimaryDomain string
OrgName string
}
type Domain struct {
Domain string
Verified bool
State domain.OrgDomainState
}
func NewOrgDomainsWriteModel(orgID string) *OrgDomainsWriteModel {
return &OrgDomainsWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: orgID,
ResourceOwner: orgID,
},
Domains: make([]*Domain, 0),
}
}
func (wm *OrgDomainsWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *org.OrgAddedEvent:
wm.OrgName = e.Name
case *org.OrgChangedEvent:
wm.OrgName = e.Name
case *org.DomainAddedEvent:
wm.Domains = append(wm.Domains, &Domain{Domain: e.Domain, State: domain.OrgDomainStateActive})
case *org.DomainVerifiedEvent:
for _, d := range wm.Domains {
if d.Domain == e.Domain {
d.Verified = true
continue
}
}
case *org.DomainPrimarySetEvent:
wm.PrimaryDomain = e.Domain
case *org.DomainRemovedEvent:
for _, d := range wm.Domains {
if d.Domain == e.Domain {
d.State = domain.OrgDomainStateRemoved
continue
}
}
}
}
return nil
}
func (wm *OrgDomainsWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, org.AggregateType).
AggregateIDs(wm.AggregateID).
ResourceOwner(wm.ResourceOwner).
EventTypes(
org.OrgAddedEventType,
org.OrgChangedEventType,
org.OrgDomainAddedEventType,
org.OrgDomainVerifiedEventType,
org.OrgDomainVerificationAddedEventType,
org.OrgDomainVerifiedEventType,
org.OrgDomainPrimarySetEventType,
org.OrgDomainRemovedEventType)
}

View File

@@ -113,6 +113,15 @@ func (c *Commands) ensureOrgSettingsToFeatures(ctx context.Context, orgID string
events = append(events, removeLabelPolicyEvent)
}
}
if !features.CustomDomain {
removeCustomDomainsEvents, err := c.removeCustomDomains(ctx, orgID)
if err != nil {
return nil, err
}
if removeCustomDomainsEvents != nil {
events = append(events, removeCustomDomainsEvents...)
}
}
return events, nil
}

View File

@@ -19,6 +19,7 @@ import (
func TestCommandSide_SetOrgFeatures(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
iamDomain string
}
type args struct {
ctx context.Context
@@ -55,6 +56,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
LoginPolicyUsernameLogin: false,
PasswordComplexityPolicy: false,
LabelPolicy: false,
CustomDomain: false,
},
},
res: res{
@@ -87,6 +89,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
LoginPolicyUsernameLogin: false,
PasswordComplexityPolicy: false,
LabelPolicy: false,
CustomDomain: false,
},
},
res: res{
@@ -136,6 +139,36 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainVerifiedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainPrimarySetEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -144,6 +177,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
},
),
),
iamDomain: "iam-domain",
},
args: args{
ctx: context.Background(),
@@ -159,6 +193,439 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
LoginPolicyUsernameLogin: false,
PasswordComplexityPolicy: false,
LabelPolicy: false,
CustomDomain: false,
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "set with default policies, custom domains, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(
context.Background(),
&iam.NewAggregate().Aggregate,
false,
false,
false,
false,
domain.PasswordlessTypeAllowed,
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&iam.NewAggregate().Aggregate,
8,
false,
false,
false,
false,
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewLabelPolicyAddedEvent(
context.Background(),
&iam.NewAggregate().Aggregate,
"primary",
"secondary",
false,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainVerifiedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainPrimarySetEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"test1",
),
),
eventFromEventPusher(
org.NewDomainVerifiedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"test1",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"test2",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewDomainRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, "test1", true),
),
eventFromEventPusher(
org.NewDomainRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, "test2", false),
),
eventFromEventPusher(
newFeaturesSetEvent(context.Background(), "org1", "Test", domain.FeaturesStateActive, time.Hour),
),
},
uniqueConstraintsFromEventConstraint(org.NewRemoveOrgDomainUniqueConstraint("test1")),
),
),
iamDomain: "iam-domain",
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
features: &domain.Features{
TierName: "Test",
State: domain.FeaturesStateActive,
AuditLogRetention: time.Hour,
LoginPolicyFactors: false,
LoginPolicyIDP: false,
LoginPolicyPasswordless: false,
LoginPolicyRegistration: false,
LoginPolicyUsernameLogin: false,
PasswordComplexityPolicy: false,
LabelPolicy: false,
CustomDomain: false,
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "set with default policies, custom domains, default not primary, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(
context.Background(),
&iam.NewAggregate().Aggregate,
false,
false,
false,
false,
domain.PasswordlessTypeAllowed,
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&iam.NewAggregate().Aggregate,
8,
false,
false,
false,
false,
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewLabelPolicyAddedEvent(
context.Background(),
&iam.NewAggregate().Aggregate,
"primary",
"secondary",
false,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainVerifiedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainPrimarySetEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"test1",
),
),
eventFromEventPusher(
org.NewDomainVerifiedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"test1",
),
),
eventFromEventPusher(
org.NewDomainPrimarySetEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"test1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"test2",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewDomainPrimarySetEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, "org1.iam-domain"),
),
eventFromEventPusher(
org.NewDomainRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, "test1", true),
),
eventFromEventPusher(
org.NewDomainRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, "test2", false),
),
eventFromEventPusher(
newFeaturesSetEvent(context.Background(), "org1", "Test", domain.FeaturesStateActive, time.Hour),
),
},
uniqueConstraintsFromEventConstraint(org.NewRemoveOrgDomainUniqueConstraint("test1")),
),
),
iamDomain: "iam-domain",
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
features: &domain.Features{
TierName: "Test",
State: domain.FeaturesStateActive,
AuditLogRetention: time.Hour,
LoginPolicyFactors: false,
LoginPolicyIDP: false,
LoginPolicyPasswordless: false,
LoginPolicyRegistration: false,
LoginPolicyUsernameLogin: false,
PasswordComplexityPolicy: false,
LabelPolicy: false,
CustomDomain: false,
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "set with default policies, custom domains, default not existing, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(
context.Background(),
&iam.NewAggregate().Aggregate,
false,
false,
false,
false,
domain.PasswordlessTypeAllowed,
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&iam.NewAggregate().Aggregate,
8,
false,
false,
false,
false,
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewLabelPolicyAddedEvent(
context.Background(),
&iam.NewAggregate().Aggregate,
"primary",
"secondary",
false,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainVerifiedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainPrimarySetEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"test1",
),
),
eventFromEventPusher(
org.NewDomainVerifiedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"test1",
),
),
eventFromEventPusher(
org.NewDomainPrimarySetEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"test1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"test2",
),
),
eventFromEventPusher(
org.NewDomainRemovedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain", true,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewDomainAddedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, "org1.iam-domain"),
),
eventFromEventPusher(
org.NewDomainPrimarySetEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, "org1.iam-domain"),
),
eventFromEventPusher(
org.NewDomainRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, "test1", true),
),
eventFromEventPusher(
org.NewDomainRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, "test2", false),
),
eventFromEventPusher(
newFeaturesSetEvent(context.Background(), "org1", "Test", domain.FeaturesStateActive, time.Hour),
),
},
uniqueConstraintsFromEventConstraint(org.NewRemoveOrgDomainUniqueConstraint("test1")),
),
),
iamDomain: "iam-domain",
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
features: &domain.Features{
TierName: "Test",
State: domain.FeaturesStateActive,
AuditLogRetention: time.Hour,
LoginPolicyFactors: false,
LoginPolicyIDP: false,
LoginPolicyPasswordless: false,
LoginPolicyRegistration: false,
LoginPolicyUsernameLogin: false,
PasswordComplexityPolicy: false,
LabelPolicy: false,
CustomDomain: false,
},
},
res: res{
@@ -281,6 +748,36 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainVerifiedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainPrimarySetEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -307,6 +804,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
},
),
),
iamDomain: "iam-domain",
},
args: args{
ctx: context.Background(),
@@ -335,6 +833,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
iamDomain: tt.fields.iamDomain,
}
got, err := r.SetOrgFeatures(tt.args.ctx, tt.args.resourceOwner, tt.args.features)
if tt.res.err == nil {
@@ -353,6 +852,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
func TestCommandSide_RemoveOrgFeatures(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
iamDomain string
}
type args struct {
ctx context.Context
@@ -448,6 +948,36 @@ func TestCommandSide_RemoveOrgFeatures(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1",
),
),
eventFromEventPusher(
org.NewDomainAddedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainVerifiedEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
eventFromEventPusher(
org.NewDomainPrimarySetEvent(
context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"org1.iam-domain",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -456,6 +986,7 @@ func TestCommandSide_RemoveOrgFeatures(t *testing.T) {
},
),
),
iamDomain: "iam-domain",
},
args: args{
ctx: context.Background(),
@@ -472,6 +1003,7 @@ func TestCommandSide_RemoveOrgFeatures(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
iamDomain: tt.fields.iamDomain,
}
got, err := r.RemoveOrgFeatures(tt.args.ctx, tt.args.resourceOwner)
if tt.res.err == nil {