diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index eaad75a63b..654a4cde39 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -169,67 +169,6 @@ all fields are updated. If no value is provided the field will be empty afterwar PUT: /idps/{idp_id}/oidc_config - -### GetDefaultFeatures - -> **rpc** GetDefaultFeatures([GetDefaultFeaturesRequest](#getdefaultfeaturesrequest)) -[GetDefaultFeaturesResponse](#getdefaultfeaturesresponse) - - - - - - GET: /features - - -### SetDefaultFeatures - -> **rpc** SetDefaultFeatures([SetDefaultFeaturesRequest](#setdefaultfeaturesrequest)) -[SetDefaultFeaturesResponse](#setdefaultfeaturesresponse) - - - - - - PUT: /features - - -### GetOrgFeatures - -> **rpc** GetOrgFeatures([GetOrgFeaturesRequest](#getorgfeaturesrequest)) -[GetOrgFeaturesResponse](#getorgfeaturesresponse) - - - - - - GET: /orgs/{org_id}/features - - -### SetOrgFeatures - -> **rpc** SetOrgFeatures([SetOrgFeaturesRequest](#setorgfeaturesrequest)) -[SetOrgFeaturesResponse](#setorgfeaturesresponse) - - - - - - PUT: /orgs/{org_id}/features - - -### ResetOrgFeatures - -> **rpc** ResetOrgFeatures([ResetOrgFeaturesRequest](#resetorgfeaturesrequest)) -[ResetOrgFeaturesResponse](#resetorgfeaturesresponse) - - - - - - DELETE: /orgs/{org_id}/features - - ### GetOrgIAMPolicy > **rpc** GetOrgIAMPolicy([GetOrgIAMPolicyRequest](#getorgiampolicyrequest)) @@ -1427,6 +1366,7 @@ if name or domain is already in use, org is not unique | login_policy_passwordless | bool | - | | password_complexity_policy | bool | - | | label_policy | bool | - | +| custom_domain | bool | - | @@ -1457,6 +1397,7 @@ if name or domain is already in use, org is not unique | login_policy_passwordless | bool | - | | password_complexity_policy | bool | - | | label_policy | bool | - | +| custom_domain | bool | - | diff --git a/internal/api/grpc/admin/features.go b/internal/api/grpc/admin/features.go index f8fe138e27..eb0613fec6 100644 --- a/internal/api/grpc/admin/features.go +++ b/internal/api/grpc/admin/features.go @@ -71,6 +71,7 @@ func setDefaultFeaturesRequestToDomain(req *admin_pb.SetDefaultFeaturesRequest) LoginPolicyUsernameLogin: req.LoginPolicyUsernameLogin, PasswordComplexityPolicy: req.PasswordComplexityPolicy, LabelPolicy: req.LabelPolicy, + CustomDomain: req.CustomDomain, } } @@ -88,5 +89,6 @@ func setOrgFeaturesRequestToDomain(req *admin_pb.SetOrgFeaturesRequest) *domain. LoginPolicyUsernameLogin: req.LoginPolicyUsernameLogin, PasswordComplexityPolicy: req.PasswordComplexityPolicy, LabelPolicy: req.LabelPolicy, + CustomDomain: req.CustomDomain, } } diff --git a/internal/api/grpc/features/features.go b/internal/api/grpc/features/features.go index e0fe88c9f3..e4dc2dceba 100644 --- a/internal/api/grpc/features/features.go +++ b/internal/api/grpc/features/features.go @@ -23,6 +23,7 @@ func FeaturesFromModel(features *features_model.FeaturesView) *features_pb.Featu LoginPolicyUsernameLogin: features.LoginPolicyUsernameLogin, PasswordComplexityPolicy: features.PasswordComplexityPolicy, LabelPolicy: features.LabelPolicy, + CustomDomain: features.CustomDomain, } } diff --git a/internal/command/features_model.go b/internal/command/features_model.go index 0027e1baa1..5483b67621 100644 --- a/internal/command/features_model.go +++ b/internal/command/features_model.go @@ -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 } diff --git a/internal/command/iam_features.go b/internal/command/iam_features.go index 6e0758078f..298522dc48 100644 --- a/internal/command/iam_features.go +++ b/internal/command/iam_features.go @@ -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") diff --git a/internal/command/iam_features_model.go b/internal/command/iam_features_model.go index 431848696a..99fecba611 100644 --- a/internal/command/iam_features_model.go +++ b/internal/command/iam_features_model.go @@ -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 diff --git a/internal/command/org_domain.go b/internal/command/org_domain.go index 8627a2908b..3ac15d6568 100644 --- a/internal/command/org_domain.go +++ b/internal/command/org_domain.go @@ -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) diff --git a/internal/command/org_domain_model.go b/internal/command/org_domain_model.go index e0101f7cd2..0667e80f9a 100644 --- a/internal/command/org_domain_model.go +++ b/internal/command/org_domain_model.go @@ -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) +} diff --git a/internal/command/org_features.go b/internal/command/org_features.go index d30682943e..9384334b78 100644 --- a/internal/command/org_features.go +++ b/internal/command/org_features.go @@ -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 } diff --git a/internal/command/org_features_test.go b/internal/command/org_features_test.go index 7536df86ab..eab0cb72a8 100644 --- a/internal/command/org_features_test.go +++ b/internal/command/org_features_test.go @@ -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 { diff --git a/internal/domain/features.go b/internal/domain/features.go index 50ea6a09cc..5357b12172 100644 --- a/internal/domain/features.go +++ b/internal/domain/features.go @@ -15,6 +15,7 @@ const ( FeatureLoginPolicyUsernameLogin = FeatureLoginPolicy + ".username_login" FeaturePasswordComplexityPolicy = "password_complexity_policy" FeatureLabelPolicy = "label_policy" + FeatureCustomDomain = "custom_domain" ) type Features struct { @@ -34,6 +35,7 @@ type Features struct { LoginPolicyUsernameLogin bool PasswordComplexityPolicy bool LabelPolicy bool + CustomDomain bool } type FeaturesState int32 diff --git a/internal/domain/org.go b/internal/domain/org.go index c3eeaef91f..97c52c14eb 100644 --- a/internal/domain/org.go +++ b/internal/domain/org.go @@ -1,8 +1,6 @@ package domain import ( - "strings" - "github.com/caos/zitadel/internal/eventstore/v1/models" ) @@ -29,11 +27,7 @@ func (o *Org) IsValid() bool { } func (o *Org) AddIAMDomain(iamDomain string) { - o.Domains = append(o.Domains, &OrgDomain{Domain: o.nameForDomain(iamDomain), Verified: true, Primary: true}) -} - -func (o *Org) nameForDomain(iamDomain string) string { - return strings.ToLower(strings.ReplaceAll(o.Name, " ", "-") + "." + iamDomain) + o.Domains = append(o.Domains, &OrgDomain{Domain: NewIAMDomainName(o.Name, iamDomain), Verified: true, Primary: true}) } type OrgState int32 diff --git a/internal/domain/org_domain.go b/internal/domain/org_domain.go index 765c5033e6..40da4d6283 100644 --- a/internal/domain/org_domain.go +++ b/internal/domain/org_domain.go @@ -1,6 +1,8 @@ package domain import ( + "strings" + http_util "github.com/caos/zitadel/internal/api/http" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/eventstore/v1/models" @@ -29,6 +31,10 @@ func (domain *OrgDomain) GenerateVerificationCode(codeGenerator crypto.Generator return validationCode, nil } +func NewIAMDomainName(orgName, iamDomain string) string { + return strings.ToLower(strings.ReplaceAll(orgName, " ", "-") + "." + iamDomain) +} + type OrgDomainValidationType int32 const ( diff --git a/internal/features/model/features_view.go b/internal/features/model/features_view.go index c6787ef9a5..704eb8f405 100644 --- a/internal/features/model/features_view.go +++ b/internal/features/model/features_view.go @@ -25,6 +25,7 @@ type FeaturesView struct { LoginPolicyUsernameLogin bool PasswordComplexityPolicy bool LabelPolicy bool + CustomDomain bool } func (f *FeaturesView) FeatureList() []string { @@ -50,6 +51,9 @@ func (f *FeaturesView) FeatureList() []string { if f.LabelPolicy { list = append(list, domain.FeatureLabelPolicy) } + if f.CustomDomain { + list = append(list, domain.FeatureCustomDomain) + } return list } diff --git a/internal/features/repository/view/model/features.go b/internal/features/repository/view/model/features.go index 98968b2079..44e67ec329 100644 --- a/internal/features/repository/view/model/features.go +++ b/internal/features/repository/view/model/features.go @@ -38,6 +38,7 @@ type FeaturesView struct { LoginPolicyUsernameLogin bool `json:"loginPolicyUsernameLogin" gorm:"column:login_policy_username_login"` PasswordComplexityPolicy bool `json:"passwordComplexityPolicy" gorm:"column:password_complexity_policy"` LabelPolicy bool `json:"labelPolicy" gorm:"column:label_policy"` + CustomDomain bool `json:"customDomain" gorm:"column:custom_domain"` } func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView { @@ -59,6 +60,7 @@ func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView { LoginPolicyUsernameLogin: features.LoginPolicyUsernameLogin, PasswordComplexityPolicy: features.PasswordComplexityPolicy, LabelPolicy: features.LabelPolicy, + CustomDomain: features.CustomDomain, } } diff --git a/internal/repository/features/features.go b/internal/repository/features/features.go index 00de8302f4..388a759368 100644 --- a/internal/repository/features/features.go +++ b/internal/repository/features/features.go @@ -31,6 +31,7 @@ type FeaturesSetEvent struct { LoginPolicyUsernameLogin *bool `json:"loginPolicyUsernameLogin,omitempty"` PasswordComplexityPolicy *bool `json:"passwordComplexityPolicy,omitempty"` LabelPolicy *bool `json:"labelPolicy,omitempty"` + CustomDomain *bool `json:"customDomain,omitempty"` } func (e *FeaturesSetEvent) Data() interface{} { @@ -131,6 +132,12 @@ func ChangeLabelPolicy(labelPolicy bool) func(event *FeaturesSetEvent) { } } +func ChangeCustomDomain(customDomain bool) func(event *FeaturesSetEvent) { + return func(e *FeaturesSetEvent) { + e.CustomDomain = &customDomain + } +} + func FeaturesSetEventMapper(event *repository.Event) (eventstore.EventReader, error) { e := &FeaturesSetEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), diff --git a/migrations/cockroach/V1.39__feature_custom_domain.sql b/migrations/cockroach/V1.39__feature_custom_domain.sql new file mode 100644 index 0000000000..0953471160 --- /dev/null +++ b/migrations/cockroach/V1.39__feature_custom_domain.sql @@ -0,0 +1,4 @@ +ALTER TABLE adminapi.features ADD COLUMN custom_domain BOOLEAN; +ALTER TABLE auth.features ADD COLUMN custom_domain BOOLEAN; +ALTER TABLE authz.features ADD COLUMN custom_domain BOOLEAN; +ALTER TABLE management.features ADD COLUMN custom_domain BOOLEAN; \ No newline at end of file diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index df6c211613..fdaad51af1 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -2188,6 +2188,7 @@ message SetDefaultFeaturesRequest { bool login_policy_passwordless = 10; bool password_complexity_policy = 11; bool label_policy = 12; + bool custom_domain = 13; } message SetDefaultFeaturesResponse { @@ -2217,6 +2218,7 @@ message SetOrgFeaturesRequest { bool login_policy_passwordless = 11; bool password_complexity_policy = 12; bool label_policy = 13; + bool custom_domain = 14; } message SetOrgFeaturesResponse { diff --git a/proto/zitadel/features.proto b/proto/zitadel/features.proto index a3c9db086a..fa23d94f17 100644 --- a/proto/zitadel/features.proto +++ b/proto/zitadel/features.proto @@ -20,6 +20,7 @@ message Features { bool login_policy_passwordless = 9; bool password_complexity_policy = 10; bool label_policy = 11; + bool custom_domain = 12; } message FeatureTier { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index bfa50f07ea..88722cc17e 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -597,6 +597,7 @@ service ManagementService { option (zitadel.v1.auth_option) = { permission: "org.write" + feature: "custom_domain" }; } @@ -618,6 +619,7 @@ service ManagementService { option (zitadel.v1.auth_option) = { permission: "org.write" + feature: "custom_domain" }; } @@ -629,6 +631,7 @@ service ManagementService { option (zitadel.v1.auth_option) = { permission: "org.write" + feature: "custom_domain" }; } @@ -639,6 +642,7 @@ service ManagementService { option (zitadel.v1.auth_option) = { permission: "org.write" + feature: "custom_domain" }; }