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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 692 additions and 70 deletions

View File

@ -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 | - |

View File

@ -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,
}
}

View File

@ -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,
}
}

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 {

View File

@ -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

View File

@ -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

View File

@ -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 (

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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),

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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"
};
}