diff --git a/internal/command/command.go b/internal/command/command.go index 8d77be765e..d8e6cfb7cf 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -82,6 +82,8 @@ type Commands struct { ActionFunctionExisting func(function string) bool EventExisting func(event string) bool EventGroupExisting func(group string) bool + + GenerateDomain func(instanceName, domain string) (string, error) } func StartCommands( @@ -168,6 +170,7 @@ func StartCommands( Issuer: defaults.Multifactors.OTP.Issuer, }, }, + GenerateDomain: domain.NewGeneratedInstanceDomain, } if defaultSecretGenerators != nil && defaultSecretGenerators.ClientSecret != nil { diff --git a/internal/command/instance.go b/internal/command/instance.go index 852e92ee6f..b5fb700459 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -142,6 +142,8 @@ type SecretGenerators struct { } type ZitadelConfig struct { + instanceID string + orgID string projectID string mgmtAppID string adminAppID string @@ -152,6 +154,16 @@ type ZitadelConfig struct { } func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) { + s.zitadel.instanceID, err = idGenerator.Next() + if err != nil { + return err + } + + s.zitadel.orgID, err = idGenerator.Next() + if err != nil { + return err + } + s.zitadel.projectID, err = idGenerator.Next() if err != nil { return err @@ -185,36 +197,88 @@ func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) { } func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (string, string, *MachineKey, *domain.ObjectDetails, error) { - instanceID, err := c.idGenerator.Next() + if err := setup.generateIDs(c.idGenerator); err != nil { + return "", "", nil, nil, err + } + ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain) + + validations, pat, machineKey, err := setUpInstance(ctx, c, setup) if err != nil { return "", "", nil, nil, err } - ctx = authz.SetCtxData(authz.WithRequestedDomain(authz.WithInstanceID(ctx, instanceID), c.externalDomain), authz.CtxData{OrgID: instanceID, ResourceOwner: instanceID}) - - orgID, err := c.idGenerator.Next() + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) if err != nil { return "", "", nil, nil, err } - userID, err := c.idGenerator.Next() + events, err := c.eventstore.Push(ctx, cmds...) if err != nil { return "", "", nil, nil, err } - if err = setup.generateIDs(c.idGenerator); err != nil { - return "", "", nil, nil, err + var token string + if pat != nil { + token = pat.Token } - ctx = authz.WithConsole(ctx, setup.zitadel.projectID, setup.zitadel.consoleAppID) - instanceAgg := instance.NewAggregate(instanceID) - orgAgg := org.NewAggregate(orgID) - userAgg := user.NewAggregate(userID, orgID) - projectAgg := project.NewAggregate(setup.zitadel.projectID, orgID) - limitsAgg := limits.NewAggregate(setup.zitadel.limitsID, instanceID) - restrictionsAgg := restrictions.NewAggregate(setup.zitadel.restrictionsID, instanceID, instanceID) + return setup.zitadel.instanceID, token, machineKey, &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreatedAt(), + ResourceOwner: setup.zitadel.orgID, + }, nil +} - validations := []preparation.Validation{ +func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string) context.Context { + return authz.WithConsole( + authz.SetCtxData( + authz.WithRequestedDomain( + authz.WithInstanceID( + ctx, + instanceID), + externalDomain, + ), + authz.CtxData{ResourceOwner: instanceID}, + ), + projectID, + consoleAppID, + ) +} + +func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (validations []preparation.Validation, pat *PersonalAccessToken, machineKey *MachineKey, err error) { + instanceAgg := instance.NewAggregate(setup.zitadel.instanceID) + + validations = setupInstanceElements(instanceAgg, setup) + + // default organization on setup'd instance + pat, machineKey, err = setupDefaultOrg(ctx, c, &validations, instanceAgg, setup.Org.Name, setup.Org.Machine, setup.Org.Human, setup.zitadel) + if err != nil { + return nil, nil, nil, err + } + + // domains + if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil { + return nil, nil, nil, err + } + setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain) + + // optional setting if set + setupMessageTexts(&validations, setup.MessageTexts, instanceAgg) + if err := setupQuotas(c, &validations, setup.Quotas, setup.zitadel.instanceID); err != nil { + return nil, nil, nil, err + } + setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg) + setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg) + setupFeatures(&validations, setup.Features, setup.zitadel.instanceID) + setupLimits(c, &validations, limits.NewAggregate(setup.zitadel.limitsID, setup.zitadel.instanceID), setup.Limits) + setupRestrictions(c, &validations, restrictions.NewAggregate(setup.zitadel.restrictionsID, setup.zitadel.instanceID, setup.zitadel.instanceID), setup.Restrictions) + + return validations, pat, machineKey, nil +} + +func setupInstanceElements(instanceAgg *instance.Aggregate, setup *InstanceSetup) []preparation.Validation { + return []preparation.Validation{ prepareAddInstance(instanceAgg, setup.InstanceName, setup.DefaultLanguage), prepareAddSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeAppSecret, setup.SecretGenerators.ClientSecret), prepareAddSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeInitCode, setup.SecretGenerators.InitializeUserCode), @@ -292,54 +356,8 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str setup.LabelPolicy.DisableWatermark, setup.LabelPolicy.ThemeMode, ), - prepareActivateDefaultLabelPolicy(instanceAgg), - prepareAddDefaultEmailTemplate(instanceAgg, setup.EmailTemplate), } - if err := setupQuotas(c, &validations, setup.Quotas, instanceID); err != nil { - return "", "", nil, nil, err - } - setupMessageTexts(&validations, setup.MessageTexts, instanceAgg) - validations = append(validations, - AddOrgCommand(ctx, orgAgg, setup.Org.Name), - c.prepareSetDefaultOrg(instanceAgg, orgAgg.ID), - ) - pat, machineKey, err := setupAdmin(c, &validations, setup.Org.Machine, setup.Org.Human, orgID, userID, userAgg) - if err != nil { - return "", "", nil, nil, err - } - setupMinimalInterfaces(c, &validations, instanceAgg, projectAgg, orgAgg, userID, setup.zitadel) - if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil { - return "", "", nil, nil, err - } - setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain) - setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg) - setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg) - setupFeatures(&validations, setup.Features, instanceID) - setupLimits(c, &validations, limitsAgg, setup.Limits) - setupRestrictions(c, &validations, restrictionsAgg, setup.Restrictions) - - //nolint:staticcheck - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) - if err != nil { - return "", "", nil, nil, err - } - - events, err := c.eventstore.Push(ctx, cmds...) - if err != nil { - return "", "", nil, nil, err - } - - var token string - if pat != nil { - token = pat.Token - } - - return instanceID, token, machineKey, &domain.ObjectDetails{ - Sequence: events[len(events)-1].Sequence(), - EventDate: events[len(events)-1].CreatedAt(), - ResourceOwner: orgID, - }, nil } func setupLimits(commands *Commands, validations *[]preparation.Validation, limitsAgg *limits.Aggregate, setLimits *SetLimits) { @@ -369,7 +387,9 @@ func setupQuotas(commands *Commands, validations *[]preparation.Validation, setQ } func setupFeatures(validations *[]preparation.Validation, features *InstanceFeatures, instanceID string) { - *validations = append(*validations, prepareSetFeatures(instanceID, features)) + if features != nil { + *validations = append(*validations, prepareSetFeatures(instanceID, features)) + } } func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation, oidcSettings *OIDCSettings, instanceAgg *instance.Aggregate) { @@ -425,7 +445,9 @@ func setupGeneratedDomain(ctx context.Context, commands *Commands, validations * return nil } -func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, projectAgg *project.Aggregate, orgAgg *org.Aggregate, userID string, ids ZitadelConfig) { +func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, projectOwner string, ids ZitadelConfig) { + projectAgg := project.NewAggregate(ids.projectID, orgAgg.ID) + cnsl := &addOIDCApp{ AddApp: AddApp{ Aggregate: *projectAgg, @@ -446,10 +468,9 @@ func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Valid IDTokenUserinfoAssertion: false, ClockSkew: 0, } + *validations = append(*validations, - commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner), - commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner), - AddProjectCommand(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified), + AddProjectCommand(projectAgg, zitadelProjectName, projectOwner, false, false, false, domain.PrivateLabelingSettingUnspecified), SetIAMProject(instanceAgg, projectAgg.ID), commands.AddAPIAppCommand( @@ -490,37 +511,103 @@ func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Valid ) } -func setupAdmin(commands *Commands, validations *[]preparation.Validation, machine *AddMachine, human *AddHuman, orgID, userID string, userAgg *user.Aggregate) (pat *PersonalAccessToken, machineKey *MachineKey, err error) { - // only a human or a machine user should be created as owner +func setupDefaultOrg(ctx context.Context, + commands *Commands, + validations *[]preparation.Validation, + instanceAgg *instance.Aggregate, + name string, + machine *AddMachine, + human *AddHuman, + ids ZitadelConfig, +) (pat *PersonalAccessToken, machineKey *MachineKey, err error) { + orgAgg := org.NewAggregate(ids.orgID) + + *validations = append( + *validations, + AddOrgCommand(ctx, orgAgg, name), + commands.prepareSetDefaultOrg(instanceAgg, ids.orgID), + ) + + projectOwner, pat, machineKey, err := setupAdmins(commands, validations, instanceAgg, orgAgg, machine, human) + if err != nil { + return nil, nil, err + } + setupMinimalInterfaces(commands, validations, instanceAgg, orgAgg, projectOwner, ids) + return pat, machineKey, nil +} + +func setupAdmins(commands *Commands, + validations *[]preparation.Validation, + instanceAgg *instance.Aggregate, + orgAgg *org.Aggregate, + machine *AddMachine, + human *AddHuman, +) (owner string, pat *PersonalAccessToken, machineKey *MachineKey, err error) { + if human == nil && machine == nil { + return "", nil, nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-z1yi2q2ot7", "Error.Instance.NoAdmin") + } + if machine != nil && machine.Machine != nil && !machine.Machine.IsZero() { - *validations = append(*validations, - AddMachineCommand(userAgg, machine.Machine), - ) - if machine.Pat != nil { - pat = NewPersonalAccessToken(orgID, userID, machine.Pat.ExpirationDate, machine.Pat.Scopes, domain.UserTypeMachine) - pat.TokenID, err = commands.idGenerator.Next() - if err != nil { - return nil, nil, err - } - *validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.keyAlgorithm)) + machineUserID, err := commands.idGenerator.Next() + if err != nil { + return "", nil, nil, err } - if machine.MachineKey != nil { - machineKey = NewMachineKey(orgID, userID, machine.MachineKey.ExpirationDate, machine.MachineKey.Type) - machineKey.KeyID, err = commands.idGenerator.Next() - if err != nil { - return nil, nil, err - } - *validations = append(*validations, prepareAddUserMachineKey(machineKey, commands.machineKeySize)) + owner = machineUserID + + pat, machineKey, err = setupMachineAdmin(commands, validations, machine, orgAgg.ID, machineUserID) + if err != nil { + return "", nil, nil, err } - } else if human != nil { - human.ID = userID + + setupAdminMembers(commands, validations, instanceAgg, orgAgg, machineUserID) + } + if human != nil { + humanUserID, err := commands.idGenerator.Next() + if err != nil { + return "", nil, nil, err + } + owner = humanUserID + human.ID = humanUserID + *validations = append(*validations, - commands.AddHumanCommand(human, orgID, commands.userPasswordHasher, commands.userEncryption, true), + commands.AddHumanCommand(human, orgAgg.ID, commands.userPasswordHasher, commands.userEncryption, true), ) + + setupAdminMembers(commands, validations, instanceAgg, orgAgg, humanUserID) + } + return owner, pat, machineKey, nil +} + +func setupMachineAdmin(commands *Commands, validations *[]preparation.Validation, machine *AddMachine, orgID, userID string) (pat *PersonalAccessToken, machineKey *MachineKey, err error) { + *validations = append(*validations, + AddMachineCommand(user.NewAggregate(userID, orgID), machine.Machine), + ) + if machine.Pat != nil { + pat = NewPersonalAccessToken(orgID, userID, machine.Pat.ExpirationDate, machine.Pat.Scopes, domain.UserTypeMachine) + pat.TokenID, err = commands.idGenerator.Next() + if err != nil { + return nil, nil, err + } + *validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.keyAlgorithm)) + } + if machine.MachineKey != nil { + machineKey = NewMachineKey(orgID, userID, machine.MachineKey.ExpirationDate, machine.MachineKey.Type) + machineKey.KeyID, err = commands.idGenerator.Next() + if err != nil { + return nil, nil, err + } + *validations = append(*validations, prepareAddUserMachineKey(machineKey, commands.machineKeySize)) } return pat, machineKey, nil } +func setupAdminMembers(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, userID string) { + *validations = append(*validations, + commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner), + commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner), + ) +} + func setupMessageTexts(validations *[]preparation.Validation, setupMessageTexts []*domain.CustomMessageText, instanceAgg *instance.Aggregate) { for _, msg := range setupMessageTexts { *validations = append(*validations, prepareSetInstanceCustomMessageTexts(instanceAgg, msg)) diff --git a/internal/command/instance_domain.go b/internal/command/instance_domain.go index d5f6808cfb..43e1aebbf8 100644 --- a/internal/command/instance_domain.go +++ b/internal/command/instance_domain.go @@ -74,7 +74,7 @@ func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain stri } func (c *Commands) addGeneratedInstanceDomain(ctx context.Context, a *instance.Aggregate, instanceName string) ([]preparation.Validation, error) { - domain, err := domain.NewGeneratedInstanceDomain(instanceName, authz.GetInstance(ctx).RequestedDomain()) + domain, err := c.GenerateDomain(instanceName, authz.GetInstance(ctx).RequestedDomain()) if err != nil { return nil, err } diff --git a/internal/command/instance_policy_label.go b/internal/command/instance_policy_label.go index 8392a7d564..3d047f65bc 100644 --- a/internal/command/instance_policy_label.go +++ b/internal/command/instance_policy_label.go @@ -12,38 +12,6 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddDefaultLabelPolicy( - ctx context.Context, - primaryColor, backgroundColor, warnColor, fontColor, primaryColorDark, backgroundColorDark, warnColorDark, fontColorDark string, - hideLoginNameSuffix, errorMsgPopup, disableWatermark bool, themeMode domain.LabelPolicyThemeMode, -) (*domain.ObjectDetails, error) { - instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, - prepareAddDefaultLabelPolicy( - instanceAgg, - primaryColor, - backgroundColor, - warnColor, - fontColor, - primaryColorDark, - backgroundColorDark, - warnColorDark, - fontColorDark, - hideLoginNameSuffix, - errorMsgPopup, - disableWatermark, - themeMode, - )) - if err != nil { - return nil, err - } - pushedEvents, err := c.eventstore.Push(ctx, cmds...) - if err != nil { - return nil, err - } - return pushedEventsToObjectDetails(pushedEvents), nil -} - func (c *Commands) ChangeDefaultLabelPolicy(ctx context.Context, policy *domain.LabelPolicy) (*domain.LabelPolicy, error) { if err := policy.IsValid(); err != nil { return nil, err @@ -373,6 +341,8 @@ func (c *Commands) getDefaultLabelPolicy(ctx context.Context) (*domain.LabelPoli return policy, nil } +// prepareAddDefaultLabelPolicy adds a default label policy, if none exists prior, and activates it directly +// this functions is only used on instance setup so the policy can be activated directly func prepareAddDefaultLabelPolicy( a *instance.Aggregate, primaryColor, @@ -417,6 +387,7 @@ func prepareAddDefaultLabelPolicy( disableWatermark, themeMode, ), + instance.NewLabelPolicyActivatedEvent(ctx, &a.Aggregate), }, nil }, nil } diff --git a/internal/command/instance_policy_label_test.go b/internal/command/instance_policy_label_test.go index ca508d414b..3720fba094 100644 --- a/internal/command/instance_policy_label_test.go +++ b/internal/command/instance_policy_label_test.go @@ -19,160 +19,6 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestCommandSide_AddDefaultLabelPolicy(t *testing.T) { - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - primaryColor string - backgroundColor string - warnColor string - fontColor string - primaryColorDark string - backgroundColorDark string - warnColorDark string - fontColorDark string - hideLoginNameSuffix bool - errorMsgPopup bool - disableWatermark bool - themeMode domain.LabelPolicyThemeMode - } - type res struct { - want *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - name: "labelpolicy already existing, already exists error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - instance.NewLabelPolicyAddedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, - "#ffffff", - "#ffffff", - "#ffffff", - "#ffffff", - "#ffffff", - "#ffffff", - "#ffffff", - "#ffffff", - true, - true, - true, - domain.LabelPolicyThemeAuto, - ), - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - primaryColor: "#ffffff", - backgroundColor: "#ffffff", - warnColor: "#ffffff", - fontColor: "#ffffff", - primaryColorDark: "#ffffff", - backgroundColorDark: "#ffffff", - warnColorDark: "#ffffff", - fontColorDark: "#ffffff", - hideLoginNameSuffix: true, - errorMsgPopup: true, - disableWatermark: true, - themeMode: domain.LabelPolicyThemeAuto, - }, - res: res{ - err: zerrors.IsErrorAlreadyExists, - }, - }, - { - name: "add policy,ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - expectPush( - instance.NewLabelPolicyAddedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, - "#ffffff", - "#ffffff", - "#ffffff", - "#ffffff", - "#ffffff", - "#ffffff", - "#ffffff", - "#ffffff", - true, - true, - true, - domain.LabelPolicyThemeDark, - ), - ), - ), - }, - args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - primaryColor: "#ffffff", - backgroundColor: "#ffffff", - warnColor: "#ffffff", - fontColor: "#ffffff", - primaryColorDark: "#ffffff", - backgroundColorDark: "#ffffff", - warnColorDark: "#ffffff", - fontColorDark: "#ffffff", - hideLoginNameSuffix: true, - errorMsgPopup: true, - disableWatermark: true, - themeMode: domain.LabelPolicyThemeDark, - }, - res: res{ - want: &domain.ObjectDetails{ - ResourceOwner: "INSTANCE", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - } - got, err := r.AddDefaultLabelPolicy( - tt.args.ctx, - tt.args.primaryColor, - tt.args.backgroundColor, - tt.args.warnColor, - tt.args.fontColor, - tt.args.primaryColorDark, - tt.args.backgroundColorDark, - tt.args.warnColorDark, - tt.args.fontColorDark, - tt.args.hideLoginNameSuffix, - tt.args.errorMsgPopup, - tt.args.disableWatermark, - tt.args.themeMode, - ) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) - } - }) - } -} - func TestCommandSide_ChangeDefaultLabelPolicy(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go index 8f4f5b68e3..9cfcbb6926 100644 --- a/internal/command/instance_test.go +++ b/internal/command/instance_test.go @@ -2,17 +2,1224 @@ package command import ( "context" + "encoding/json" + "slices" + "strings" "testing" + "time" "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/id" + id_mock "github.com/zitadel/zitadel/internal/id/mock" "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/project" + "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) +func instanceSetupZitadelIDs() ZitadelConfig { + return ZitadelConfig{ + instanceID: "INSTANCE", + orgID: "ORG", + projectID: "PROJECT", + consoleAppID: "console-id", + authAppID: "auth-id", + mgmtAppID: "mgmt-id", + adminAppID: "admin-id", + } +} + +func projectAddedEvents(ctx context.Context, instanceID, orgID, id, owner string, externalSecure bool) []eventstore.Command { + events := []eventstore.Command{ + project.NewProjectAddedEvent(ctx, + &project.NewAggregate(id, orgID).Aggregate, + "ZITADEL", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + project.NewProjectMemberAddedEvent(ctx, + &project.NewAggregate(id, orgID).Aggregate, + owner, + domain.RoleProjectOwner, + ), + instance.NewIAMProjectSetEvent(ctx, + &instance.NewAggregate(instanceID).Aggregate, + id, + ), + } + events = append(events, apiAppEvents(ctx, orgID, id, "mgmt-id", "Management-API")...) + events = append(events, apiAppEvents(ctx, orgID, id, "admin-id", "Admin-API")...) + events = append(events, apiAppEvents(ctx, orgID, id, "auth-id", "Auth-API")...) + + consoleAppID := "console-id" + consoleClientID := "clientID@zitadel" + events = append(events, oidcAppEvents(ctx, orgID, id, consoleAppID, "Console", consoleClientID, externalSecure)...) + events = append(events, + instance.NewIAMConsoleSetEvent(ctx, + &instance.NewAggregate(instanceID).Aggregate, + &consoleClientID, + &consoleAppID, + ), + ) + return events +} + +func projectClientIDs() []string { + return []string{"clientID", "clientID", "clientID", "clientID"} +} + +func apiAppEvents(ctx context.Context, orgID, projectID, id, name string) []eventstore.Command { + return []eventstore.Command{ + project.NewApplicationAddedEvent( + ctx, + &project.NewAggregate(projectID, orgID).Aggregate, + id, + name, + ), + project.NewAPIConfigAddedEvent(ctx, + &project.NewAggregate(projectID, orgID).Aggregate, + id, + "clientID@zitadel", + "", + domain.APIAuthMethodTypePrivateKeyJWT, + ), + } +} + +func oidcAppEvents(ctx context.Context, orgID, projectID, id, name, clientID string, externalSecure bool) []eventstore.Command { + return []eventstore.Command{ + project.NewApplicationAddedEvent( + ctx, + &project.NewAggregate(projectID, orgID).Aggregate, + id, + name, + ), + project.NewOIDCConfigAddedEvent(ctx, + &project.NewAggregate(projectID, orgID).Aggregate, + domain.OIDCVersionV1, + id, + clientID, + "", + []string{}, + []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + []string{}, + !externalSecure, + domain.OIDCTokenTypeBearer, + false, + false, + false, + 0, + nil, + false, + ), + } +} + +func orgFilters(orgID string, machine, human bool) []expect { + filters := []expect{ + expectFilter(), + expectFilter( + org.NewOrgAddedEvent(context.Background(), &org.NewAggregate(orgID).Aggregate, ""), + ), + } + if machine { + filters = append(filters, machineFilters(orgID, true)...) + filters = append(filters, adminMemberFilters(orgID, "USER-MACHINE")...) + } + if human { + filters = append(filters, humanFilters(orgID)...) + filters = append(filters, adminMemberFilters(orgID, "USER")...) + } + + return append(filters, + projectFilters()..., + ) +} + +func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultDomain string, externalSecure bool, machine, human bool) []eventstore.Command { + instanceAgg := instance.NewAggregate(instanceID) + orgAgg := org.NewAggregate(orgID) + domain := strings.ToLower(name + "." + defaultDomain) + events := []eventstore.Command{ + org.NewOrgAddedEvent(ctx, &orgAgg.Aggregate, name), + org.NewDomainAddedEvent(ctx, &orgAgg.Aggregate, domain), + org.NewDomainVerifiedEvent(ctx, &orgAgg.Aggregate, domain), + org.NewDomainPrimarySetEvent(ctx, &orgAgg.Aggregate, domain), + instance.NewDefaultOrgSetEventEvent(ctx, &instanceAgg.Aggregate, orgID), + } + + owner := "" + if machine { + machineID := "USER-MACHINE" + events = append(events, machineEvents(ctx, instanceID, orgID, machineID, "PAT")...) + owner = machineID + } + if human { + userID := "USER" + events = append(events, humanEvents(ctx, instanceID, orgID, userID)...) + owner = userID + } + + events = append(events, projectAddedEvents(ctx, instanceID, orgID, projectID, owner, externalSecure)...) + return events +} + +func orgIDs() []string { + return slices.Concat([]string{"USER-MACHINE", "PAT", "USER"}, projectClientIDs()) +} + +func instancePoliciesFilters(instanceID string) []expect { + return []expect{ + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + } +} + +func instancePoliciesEvents(ctx context.Context, instanceID string) []eventstore.Command { + instanceAgg := instance.NewAggregate(instanceID) + return []eventstore.Command{ + instance.NewPasswordComplexityPolicyAddedEvent(ctx, &instanceAgg.Aggregate, 8, true, true, true, true), + instance.NewPasswordAgePolicyAddedEvent(ctx, &instanceAgg.Aggregate, 0, 0), + instance.NewDomainPolicyAddedEvent(ctx, &instanceAgg.Aggregate, false, false, false), + instance.NewLoginPolicyAddedEvent(ctx, &instanceAgg.Aggregate, true, true, true, false, false, false, false, true, false, false, domain.PasswordlessTypeAllowed, "", 240*time.Hour, 240*time.Hour, 720*time.Hour, 18*time.Hour, 12*time.Hour), + instance.NewLoginPolicySecondFactorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecondFactorTypeTOTP), + instance.NewLoginPolicySecondFactorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecondFactorTypeU2F), + instance.NewLoginPolicyMultiFactorAddedEvent(ctx, &instanceAgg.Aggregate, domain.MultiFactorTypeU2FWithPIN), + instance.NewPrivacyPolicyAddedEvent(ctx, &instanceAgg.Aggregate, "", "", "", "", "", "", ""), + instance.NewNotificationPolicyAddedEvent(ctx, &instanceAgg.Aggregate, true), + instance.NewLockoutPolicyAddedEvent(ctx, &instanceAgg.Aggregate, 0, 0, true), + instance.NewLabelPolicyAddedEvent(ctx, &instanceAgg.Aggregate, "#5469d4", "#fafafa", "#cd3d56", "#000000", "#2073c4", "#111827", "#ff3b5b", "#ffffff", false, false, false, domain.LabelPolicyThemeAuto), + instance.NewLabelPolicyActivatedEvent(ctx, &instanceAgg.Aggregate), + } +} + +func instanceSetupPoliciesConfig() *InstanceSetup { + return &InstanceSetup{ + PasswordComplexityPolicy: struct { + MinLength uint64 + HasLowercase bool + HasUppercase bool + HasNumber bool + HasSymbol bool + }{8, true, true, true, true}, + PasswordAgePolicy: struct { + ExpireWarnDays uint64 + MaxAgeDays uint64 + }{0, 0}, + DomainPolicy: struct { + UserLoginMustBeDomain bool + ValidateOrgDomains bool + SMTPSenderAddressMatchesInstanceDomain bool + }{false, false, false}, + LoginPolicy: struct { + AllowUsernamePassword bool + AllowRegister bool + AllowExternalIDP bool + ForceMFA bool + ForceMFALocalOnly bool + HidePasswordReset bool + IgnoreUnknownUsername bool + AllowDomainDiscovery bool + DisableLoginWithEmail bool + DisableLoginWithPhone bool + PasswordlessType domain.PasswordlessType + DefaultRedirectURI string + PasswordCheckLifetime time.Duration + ExternalLoginCheckLifetime time.Duration + MfaInitSkipLifetime time.Duration + SecondFactorCheckLifetime time.Duration + MultiFactorCheckLifetime time.Duration + }{true, true, true, false, false, false, false, true, false, false, domain.PasswordlessTypeAllowed, "", 240 * time.Hour, 240 * time.Hour, 720 * time.Hour, 18 * time.Hour, 12 * time.Hour}, + NotificationPolicy: struct { + PasswordChange bool + }{true}, + PrivacyPolicy: struct { + TOSLink string + PrivacyLink string + HelpLink string + SupportEmail domain.EmailAddress + DocsLink string + CustomLink string + CustomLinkText string + }{"", "", "", "", "", "", ""}, + LabelPolicy: struct { + PrimaryColor string + BackgroundColor string + WarnColor string + FontColor string + PrimaryColorDark string + BackgroundColorDark string + WarnColorDark string + FontColorDark string + HideLoginNameSuffix bool + ErrorMsgPopup bool + DisableWatermark bool + ThemeMode domain.LabelPolicyThemeMode + }{"#5469d4", "#fafafa", "#cd3d56", "#000000", "#2073c4", "#111827", "#ff3b5b", "#ffffff", false, false, false, domain.LabelPolicyThemeAuto}, + LockoutPolicy: struct { + MaxPasswordAttempts uint64 + MaxOTPAttempts uint64 + ShouldShowLockoutFailure bool + }{0, 0, true}, + } +} + +func setupInstanceElementsFilters(instanceID string) []expect { + return slices.Concat( + instanceElementsFilters(), + instancePoliciesFilters(instanceID), + // email template + []expect{expectFilter()}, + ) +} + +func setupInstanceElementsConfig() *InstanceSetup { + conf := instanceSetupPoliciesConfig() + conf.InstanceName = "ZITADEL" + conf.DefaultLanguage = language.English + conf.zitadel = instanceSetupZitadelIDs() + conf.SecretGenerators = instanceElementsConfig() + conf.EmailTemplate = []byte("something") + return conf +} + +func setupInstanceElementsEvents(ctx context.Context, instanceID, instanceName string, defaultLanguage language.Tag) []eventstore.Command { + instanceAgg := instance.NewAggregate(instanceID) + return slices.Concat( + instanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage), + instancePoliciesEvents(ctx, instanceID), + []eventstore.Command{instance.NewMailTemplateAddedEvent(ctx, &instanceAgg.Aggregate, []byte("something"))}, + ) +} + +func instanceElementsFilters() []expect { + return []expect{ + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + } +} + +func instanceElementsEvents(ctx context.Context, instanceID, instanceName string, defaultLanguage language.Tag) []eventstore.Command { + instanceAgg := instance.NewAggregate(instanceID) + return []eventstore.Command{ + instance.NewInstanceAddedEvent(ctx, &instanceAgg.Aggregate, instanceName), + instance.NewDefaultLanguageSetEvent(ctx, &instanceAgg.Aggregate, defaultLanguage), + instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeAppSecret, 64, 0, true, true, true, false), + instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeInitCode, 6, 72*time.Hour, false, true, true, false), + instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeVerifyEmailCode, 6, time.Hour, false, true, true, false), + instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeVerifyPhoneCode, 6, time.Hour, false, true, true, false), + instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypePasswordResetCode, 6, time.Hour, false, true, true, false), + instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypePasswordlessInitCode, 12, time.Hour, true, true, true, false), + instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeVerifyDomain, 32, 0, true, true, true, false), + instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeOTPSMS, 8, 5*time.Minute, false, false, true, false), + instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeOTPEmail, 8, 5*time.Minute, false, false, true, false), + } +} +func instanceElementsConfig() *SecretGenerators { + return &SecretGenerators{ + ClientSecret: &crypto.GeneratorConfig{Length: 64, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true}, + InitializeUserCode: &crypto.GeneratorConfig{Length: 6, Expiry: 72 * time.Hour, IncludeUpperLetters: true, IncludeDigits: true}, + EmailVerificationCode: &crypto.GeneratorConfig{Length: 6, Expiry: time.Hour, IncludeUpperLetters: true, IncludeDigits: true}, + PhoneVerificationCode: &crypto.GeneratorConfig{Length: 6, Expiry: time.Hour, IncludeUpperLetters: true, IncludeDigits: true}, + PasswordVerificationCode: &crypto.GeneratorConfig{Length: 6, Expiry: time.Hour, IncludeUpperLetters: true, IncludeDigits: true}, + PasswordlessInitCode: &crypto.GeneratorConfig{Length: 12, Expiry: time.Hour, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true}, + DomainVerification: &crypto.GeneratorConfig{Length: 32, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true}, + OTPSMS: &crypto.GeneratorConfig{Length: 8, Expiry: 5 * time.Minute, IncludeDigits: true}, + OTPEmail: &crypto.GeneratorConfig{Length: 8, Expiry: 5 * time.Minute, IncludeDigits: true}, + } +} + +func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) []expect { + return slices.Concat( + setupInstanceElementsFilters(instanceID), + orgFilters(orgID, true, true), + generatedDomainFilters(instanceID, orgID, projectID, appID, domain), + ) +} + +func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appID, instanceName, orgName string, defaultLanguage language.Tag, domain string, externalSecure bool) []eventstore.Command { + return slices.Concat( + setupInstanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage), + orgEvents(ctx, instanceID, orgID, orgName, projectID, domain, externalSecure, true, true), + generatedDomainEvents(ctx, instanceID, orgID, projectID, appID, domain), + ) +} + +func setupInstanceConfig() *InstanceSetup { + conf := setupInstanceElementsConfig() + conf.Org = InstanceOrgSetup{ + Name: "ZITADEL", + Machine: instanceSetupMachineConfig(), + Human: instanceSetupHumanConfig(), + } + conf.CustomDomain = "" + return conf +} + +func generatedDomainEvents(ctx context.Context, instanceID, orgID, projectID, appID, defaultDomain string) []eventstore.Command { + instanceAgg := instance.NewAggregate(instanceID) + changed, _ := project.NewOIDCConfigChangedEvent(ctx, &project.NewAggregate(projectID, orgID).Aggregate, appID, + []project.OIDCConfigChanges{ + project.ChangeRedirectURIs([]string{"http://" + defaultDomain + "/ui/console/auth/callback"}), + project.ChangePostLogoutRedirectURIs([]string{"http://" + defaultDomain + "/ui/console/signedout"}), + }, + ) + return []eventstore.Command{ + instance.NewDomainAddedEvent(ctx, &instanceAgg.Aggregate, defaultDomain, true), + changed, + instance.NewDomainPrimarySetEvent(ctx, &instanceAgg.Aggregate, defaultDomain), + } +} + +func generatedDomainFilters(instanceID, orgID, projectID, appID, generatedDomain string) []expect { + return []expect{ + expectFilter(), + expectFilter( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate(projectID, orgID).Aggregate, + appID, + "console", + ), + project.NewOIDCConfigAddedEvent(context.Background(), + &project.NewAggregate(projectID, orgID).Aggregate, + domain.OIDCVersionV1, + appID, + "clientID", + "", + []string{}, + []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + []string{}, + true, + domain.OIDCTokenTypeBearer, + false, + false, + false, + 0, + nil, + false, + ), + ), + expectFilter( + func() eventstore.Event { + event := instance.NewDomainAddedEvent(context.Background(), + &instance.NewAggregate(instanceID).Aggregate, + generatedDomain, + true, + ) + event.Data, _ = json.Marshal(event) + return event + }(), + ), + } +} + +func humanFilters(orgID string) []expect { + return []expect{ + expectFilter(), + expectFilter( + org.NewDomainPolicyAddedEvent( + context.Background(), + &org.NewAggregate(orgID).Aggregate, + true, + true, + true, + ), + ), + expectFilter( + org.NewPasswordComplexityPolicyAddedEvent( + context.Background(), + &org.NewAggregate(orgID).Aggregate, + 2, + false, + false, + false, + false, + ), + ), + } +} + +func instanceSetupHumanConfig() *AddHuman { + return &AddHuman{ + Username: "zitadel-admin", + FirstName: "ZITADEL", + LastName: "Admin", + Email: Email{ + Address: domain.EmailAddress("admin@zitadel.test"), + Verified: true, + }, + PreferredLanguage: language.English, + Password: "password", + PasswordChangeRequired: false, + } +} + +func machineFilters(orgID string, pat bool) []expect { + filters := []expect{ + expectFilter(), + expectFilter( + org.NewDomainPolicyAddedEvent( + context.Background(), + &org.NewAggregate(orgID).Aggregate, + true, + true, + true, + ), + ), + } + if pat { + filters = append(filters, + expectFilter(), + expectFilter(), + ) + } + return filters +} + +func instanceSetupMachineConfig() *AddMachine { + return &AddMachine{ + Machine: &Machine{ + Username: "zitadel-admin-machine", + Name: "ZITADEL-machine", + Description: "Admin", + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + Pat: &AddPat{ + ExpirationDate: time.Time{}, + Scopes: nil, + }, + /* not predictable with the key value in the events + MachineKey: &AddMachineKey{ + Type: domain.AuthNKeyTypeJSON, + ExpirationDate: time.Time{}, + }, + */ + } +} + +func projectFilters() []expect { + return []expect{ + expectFilter(), + expectFilter(), + expectFilter(), + expectFilter(), + } +} + +func adminMemberFilters(orgID, userID string) []expect { + return []expect{ + expectFilter( + addHumanEvent(context.Background(), orgID, userID), + ), + expectFilter(), + expectFilter( + addHumanEvent(context.Background(), orgID, userID), + ), + expectFilter(), + } +} + +func humanEvents(ctx context.Context, instanceID, orgID, userID string) []eventstore.Command { + agg := user.NewAggregate(userID, orgID) + instanceAgg := instance.NewAggregate(instanceID) + orgAgg := org.NewAggregate(orgID) + return []eventstore.Command{ + addHumanEvent(ctx, orgID, userID), + user.NewHumanEmailVerifiedEvent(ctx, &agg.Aggregate), + org.NewMemberAddedEvent(ctx, &orgAgg.Aggregate, userID, domain.RoleOrgOwner), + instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMOwner), + } +} + +func addHumanEvent(ctx context.Context, orgID, userID string) *user.HumanAddedEvent { + agg := user.NewAggregate(userID, orgID) + return func() *user.HumanAddedEvent { + event := user.NewHumanAddedEvent( + ctx, + &agg.Aggregate, + "zitadel-admin", + "ZITADEL", + "Admin", + "", + "ZITADEL Admin", + language.English, + 0, + "admin@zitadel.test", + false, + ) + event.AddPasswordData("$plain$x$password", false) + return event + }() +} + +// machineEvents all events from setup to create the machine user, machinekey can't be tested here, as the public key is not provided and as such the value in the event can't be expected +func machineEvents(ctx context.Context, instanceID, orgID, userID, patID string) []eventstore.Command { + agg := user.NewAggregate(userID, orgID) + instanceAgg := instance.NewAggregate(instanceID) + orgAgg := org.NewAggregate(orgID) + events := []eventstore.Command{addMachineEvent(ctx, orgID, userID)} + if patID != "" { + events = append(events, + user.NewPersonalAccessTokenAddedEvent( + ctx, + &agg.Aggregate, + patID, + time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC), + nil, + ), + ) + } + return append(events, + org.NewMemberAddedEvent(ctx, &orgAgg.Aggregate, userID, domain.RoleOrgOwner), + instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMOwner), + ) +} + +func addMachineEvent(ctx context.Context, orgID, userID string) *user.MachineAddedEvent { + agg := user.NewAggregate(userID, orgID) + return user.NewMachineAddedEvent(ctx, + &agg.Aggregate, + "zitadel-admin-machine", + "ZITADEL-machine", + "Admin", + false, + domain.OIDCTokenTypeBearer, + ) +} + +func testSetup(ctx context.Context, c *Commands, validations []preparation.Validation) error { + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) + if err != nil { + return err + } + + _, err = c.eventstore.Push(ctx, cmds...) + return err +} + +func TestCommandSide_setupMinimalInterfaces(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + } + type args struct { + ctx context.Context + instanceAgg *instance.Aggregate + orgAgg *org.Aggregate + owner string + ids ZitadelConfig + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "create, ok", + fields: fields{ + eventstore: expectEventstore( + slices.Concat( + projectFilters(), + []expect{expectPush( + projectAddedEvents(context.Background(), + "INSTANCE", + "ORG", + "PROJECT", + "owner", + false, + )..., + ), + }, + )..., + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, projectClientIDs()...), + }, + args: args{ + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + instanceAgg: instance.NewAggregate("INSTANCE"), + orgAgg: org.NewAggregate("ORG"), + owner: "owner", + ids: instanceSetupZitadelIDs(), + }, + res: res{ + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + } + validations := make([]preparation.Validation, 0) + setupMinimalInterfaces(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.owner, tt.args.ids) + + err := testSetup(tt.args.ctx, r, validations) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func TestCommandSide_setupAdmins(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + userPasswordHasher *crypto.Hasher + roles []authz.RoleMapping + keyAlgorithm crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + instanceAgg *instance.Aggregate + orgAgg *org.Aggregate + machine *AddMachine + human *AddHuman + } + type res struct { + owner string + pat bool + machineKey bool + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "human, ok", + fields: fields{ + eventstore: expectEventstore( + slices.Concat( + humanFilters("ORG"), + adminMemberFilters("ORG", "USER"), + []expect{ + expectPush( + humanEvents(context.Background(), + "INSTANCE", + "ORG", + "USER", + )..., + ), + }, + )..., + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER"), + userPasswordHasher: mockPasswordHasher("x"), + roles: []authz.RoleMapping{ + {Role: domain.RoleOrgOwner, Permissions: []string{""}}, + {Role: domain.RoleIAMOwner, Permissions: []string{""}}, + }, + }, + args: args{ + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + instanceAgg: instance.NewAggregate("INSTANCE"), + orgAgg: org.NewAggregate("ORG"), + human: instanceSetupHumanConfig(), + }, + res: res{ + owner: "USER", + pat: false, + machineKey: false, + err: nil, + }, + }, + { + name: "machine, ok", + fields: fields{ + eventstore: expectEventstore( + slices.Concat( + machineFilters("ORG", true), + adminMemberFilters("ORG", "USER-MACHINE"), + []expect{ + expectPush( + machineEvents(context.Background(), + "INSTANCE", + "ORG", + "USER-MACHINE", + "PAT", + )..., + ), + }, + )..., + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT"), + roles: []authz.RoleMapping{ + {Role: domain.RoleOrgOwner, Permissions: []string{""}}, + {Role: domain.RoleIAMOwner, Permissions: []string{""}}, + }, + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + instanceAgg: instance.NewAggregate("INSTANCE"), + orgAgg: org.NewAggregate("ORG"), + machine: instanceSetupMachineConfig(), + }, + res: res{ + owner: "USER-MACHINE", + pat: true, + machineKey: false, + err: nil, + }, + }, + { + name: "human and machine, ok", + fields: fields{ + eventstore: expectEventstore( + slices.Concat( + machineFilters("ORG", true), + adminMemberFilters("ORG", "USER-MACHINE"), + humanFilters("ORG"), + adminMemberFilters("ORG", "USER"), + []expect{ + expectPush( + slices.Concat( + machineEvents(context.Background(), + "INSTANCE", + "ORG", + "USER-MACHINE", + "PAT", + ), + humanEvents(context.Background(), + "INSTANCE", + "ORG", + "USER", + ), + )..., + ), + }, + )..., + ), + userPasswordHasher: mockPasswordHasher("x"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER"), + roles: []authz.RoleMapping{ + {Role: domain.RoleOrgOwner, Permissions: []string{""}}, + {Role: domain.RoleIAMOwner, Permissions: []string{""}}, + }, + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + instanceAgg: instance.NewAggregate("INSTANCE"), + orgAgg: org.NewAggregate("ORG"), + machine: instanceSetupMachineConfig(), + human: instanceSetupHumanConfig(), + }, + res: res{ + owner: "USER", + pat: true, + machineKey: false, + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + zitadelRoles: tt.fields.roles, + userPasswordHasher: tt.fields.userPasswordHasher, + keyAlgorithm: tt.fields.keyAlgorithm, + } + validations := make([]preparation.Validation, 0) + owner, pat, mk, err := setupAdmins(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.machine, tt.args.human) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + + err = testSetup(tt.args.ctx, r, validations) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + + if tt.res.err == nil { + assert.Equal(t, owner, tt.res.owner) + if tt.res.pat { + assert.NotNil(t, pat) + } + if tt.res.machineKey { + assert.NotNil(t, mk) + } + } + }) + } +} + +func TestCommandSide_setupDefaultOrg(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + userPasswordHasher *crypto.Hasher + roles []authz.RoleMapping + keyAlgorithm crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + instanceAgg *instance.Aggregate + orgName string + machine *AddMachine + human *AddHuman + ids ZitadelConfig + } + type res struct { + pat bool + machineKey bool + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "human and machine, ok", + fields: fields{ + eventstore: expectEventstore( + slices.Concat( + orgFilters( + "ORG", + true, + true, + ), + []expect{ + expectPush( + slices.Concat( + orgEvents(context.Background(), + "INSTANCE", + "ORG", + "ZITADEL", + "PROJECT", + "DOMAIN", + false, + true, + true, + ), + )..., + ), + }, + )..., + ), + userPasswordHasher: mockPasswordHasher("x"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...), + roles: []authz.RoleMapping{ + {Role: domain.RoleOrgOwner, Permissions: []string{""}}, + {Role: domain.RoleIAMOwner, Permissions: []string{""}}, + }, + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + instanceAgg: instance.NewAggregate("INSTANCE"), + orgName: "ZITADEL", + machine: &AddMachine{ + Machine: &Machine{ + Username: "zitadel-admin-machine", + Name: "ZITADEL-machine", + Description: "Admin", + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + Pat: &AddPat{ + ExpirationDate: time.Time{}, + Scopes: nil, + }, + /* not predictable with the key value in the events + MachineKey: &AddMachineKey{ + Type: domain.AuthNKeyTypeJSON, + ExpirationDate: time.Time{}, + }, + */ + }, + human: &AddHuman{ + Username: "zitadel-admin", + FirstName: "ZITADEL", + LastName: "Admin", + Email: Email{ + Address: domain.EmailAddress("admin@zitadel.test"), + Verified: true, + }, + PreferredLanguage: language.English, + Password: "password", + PasswordChangeRequired: false, + }, + ids: ZitadelConfig{ + instanceID: "INSTANCE", + orgID: "ORG", + projectID: "PROJECT", + consoleAppID: "console-id", + authAppID: "auth-id", + mgmtAppID: "mgmt-id", + adminAppID: "admin-id", + }, + }, + res: res{ + pat: true, + machineKey: false, + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + zitadelRoles: tt.fields.roles, + userPasswordHasher: tt.fields.userPasswordHasher, + keyAlgorithm: tt.fields.keyAlgorithm, + } + validations := make([]preparation.Validation, 0) + pat, mk, err := setupDefaultOrg(tt.args.ctx, r, &validations, tt.args.instanceAgg, tt.args.orgName, tt.args.machine, tt.args.human, tt.args.ids) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + + err = testSetup(context.Background(), r, validations) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + + if tt.res.err == nil { + if tt.res.pat { + assert.NotNil(t, pat) + } + if tt.res.machineKey { + assert.NotNil(t, mk) + } + } + }) + } +} + +func TestCommandSide_setupInstanceElements(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + } + type args struct { + ctx context.Context + instanceAgg *instance.Aggregate + setup *InstanceSetup + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "ok", + fields: fields{ + eventstore: expectEventstore( + slices.Concat( + setupInstanceElementsFilters("INSTANCE"), + []expect{ + expectPush( + setupInstanceElementsEvents(context.Background(), + "INSTANCE", + "ZITADEL", + language.English, + )..., + ), + }, + )..., + ), + }, + args: args{ + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + instanceAgg: instance.NewAggregate("INSTANCE"), + setup: setupInstanceElementsConfig(), + }, + res: res{ + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + } + validations := setupInstanceElements(tt.args.instanceAgg, tt.args.setup) + + err := testSetup(context.Background(), r, validations) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func TestCommandSide_setUpInstance(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + userPasswordHasher *crypto.Hasher + roles []authz.RoleMapping + keyAlgorithm crypto.EncryptionAlgorithm + generateDomain func(string, string) (string, error) + } + type args struct { + ctx context.Context + setup *InstanceSetup + } + type res struct { + pat bool + machineKey bool + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "ok", + fields: fields{ + eventstore: expectEventstore( + slices.Concat( + setupInstanceFilters("INSTANCE", "ORG", "PROJECT", "console-id", "DOMAIN"), + []expect{ + expectPush( + setupInstanceEvents(context.Background(), + "INSTANCE", + "ORG", + "PROJECT", + "console-id", + "ZITADEL", + "ZITADEL", + language.English, + "DOMAIN", + false, + )..., + ), + }, + )..., + ), + userPasswordHasher: mockPasswordHasher("x"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...), + roles: []authz.RoleMapping{ + {Role: domain.RoleOrgOwner, Permissions: []string{""}}, + {Role: domain.RoleIAMOwner, Permissions: []string{""}}, + }, + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + generateDomain: func(string, string) (string, error) { + return "DOMAIN", nil + }, + }, + args: args{ + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + setup: setupInstanceConfig(), + }, + res: res{ + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + zitadelRoles: tt.fields.roles, + userPasswordHasher: tt.fields.userPasswordHasher, + keyAlgorithm: tt.fields.keyAlgorithm, + GenerateDomain: tt.fields.generateDomain, + } + + validations, pat, mk, err := setUpInstance(tt.args.ctx, r, tt.args.setup) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + + err = testSetup(tt.args.ctx, r, validations) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + + if tt.res.err == nil { + if tt.res.pat { + assert.NotNil(t, pat) + } + if tt.res.machineKey { + assert.NotNil(t, mk) + } + } + }) + } +} + func TestCommandSide_UpdateInstance(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore diff --git a/internal/command/org.go b/internal/command/org.go index 5f997183af..db963762b1 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -63,7 +63,7 @@ type CreatedOrgAdmin struct { } func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, allowInitialMail bool, userIDs ...string) (_ *CreatedOrg, err error) { - cmds := c.newOrgSetupCommands(ctx, orgID, o, userIDs) + cmds := c.newOrgSetupCommands(ctx, orgID, o) for _, admin := range o.Admins { if err = cmds.setupOrgAdmin(admin, allowInitialMail); err != nil { return nil, err @@ -76,10 +76,10 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID strin return cmds.push(ctx) } -func (c *Commands) newOrgSetupCommands(ctx context.Context, orgID string, orgSetup *OrgSetup, userIDs []string) *orgSetupCommands { +func (c *Commands) newOrgSetupCommands(ctx context.Context, orgID string, orgSetup *OrgSetup) *orgSetupCommands { orgAgg := org.NewAggregate(orgID) validations := []preparation.Validation{ - AddOrgCommand(ctx, orgAgg, orgSetup.Name, userIDs...), + AddOrgCommand(ctx, orgAgg, orgSetup.Name), } return &orgSetupCommands{ validations: validations, @@ -233,7 +233,7 @@ func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail b // AddOrgCommand defines the commands to create a new org, // this includes the verified default domain -func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string, userIDs ...string) preparation.Validation { +func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string) preparation.Validation { return func() (preparation.CreateCommands, error) { if name = strings.TrimSpace(name); name == "" { return nil, zerrors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument")