feat(cli): setup (#3267)

* commander

* commander

* selber!

* move to packages

* fix(errors): implement Is interface

* test: command

* test: commands

* add init steps

* setup tenant

* add default step yaml

* possibility to set password

* merge v2 into v2-commander

* fix: rename iam command side to instance

* fix: rename iam command side to instance

* fix: rename iam command side to instance

* fix: rename iam command side to instance

* fix: search query builder can filter events in memory

* fix: filters for add member

* fix(setup): add `ExternalSecure` to config

* chore: name iam to instance

* fix: matching

* remove unsued func

* base url

* base url

* test(command): filter funcs

* test: commands

* fix: rename orgiampolicy to domain policy

* start from init

* commands

* config

* fix indexes and add constraints

* fixes

* fix: merge conflicts

* fix: protos

* fix: md files

* setup

* add deprecated org iam policy again

* typo

* fix search query

* fix filter

* Apply suggestions from code review

* remove custom org from org setup

* add todos for verification

* change apps creation

* simplify package structure

* fix error

* move preparation helper for tests

* fix unique constraints

* fix config mapping in setup

* fix error handling in encryption_keys.go

* fix projection config

* fix query from old views to projection

* fix setup of mgmt api

* set iam project and fix instance projection

* imports

Co-authored-by: Livio Amstutz <[email protected]>
Co-authored-by: fabi <[email protected]>
This commit is contained in:
Silvan
2022-03-28 10:05:09 +02:00
committed by GitHub
co-authored by Livio Amstutz fabi
parent 9d4f296c62
commit c5b99274d7
175 changed files with 5213 additions and 2212 deletions
+36
View File
@@ -0,0 +1,36 @@
package command
import (
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/action"
iam_repo "github.com/caos/zitadel/internal/repository/instance"
"github.com/caos/zitadel/internal/repository/keypair"
"github.com/caos/zitadel/internal/repository/org"
proj_repo "github.com/caos/zitadel/internal/repository/project"
usr_repo "github.com/caos/zitadel/internal/repository/user"
usr_grant_repo "github.com/caos/zitadel/internal/repository/usergrant"
)
type Command struct {
es *eventstore.Eventstore
userPasswordAlg crypto.HashAlgorithm
iamDomain string
}
func New(es *eventstore.Eventstore, iamDomain string, defaults sd.SystemDefaults) *Command {
iam_repo.RegisterEventMappers(es)
org.RegisterEventMappers(es)
usr_repo.RegisterEventMappers(es)
usr_grant_repo.RegisterEventMappers(es)
proj_repo.RegisterEventMappers(es)
keypair.RegisterEventMappers(es)
action.RegisterEventMappers(es)
return &Command{
es: es,
iamDomain: iamDomain,
userPasswordAlg: crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost),
}
}
+290
View File
@@ -0,0 +1,290 @@
package command
import (
"context"
"time"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/api/ui/console"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/repository/instance"
"github.com/caos/zitadel/internal/repository/org"
"github.com/caos/zitadel/internal/repository/project"
"github.com/caos/zitadel/internal/repository/user"
)
const (
zitadelProjectName = "ZITADEL"
mgmtAppName = "Management-API"
adminAppName = "Admin-API"
authAppName = "Auth-API"
consoleAppName = "Console"
consoleRedirectPath = console.HandlerPrefix + "/auth/callback"
consolePostLogoutPath = console.HandlerPrefix + "/signedout"
)
type InstanceSetup struct {
Org OrgSetup
Zitadel ZitadelConfig
PasswordComplexityPolicy struct {
MinLength uint64
HasLowercase bool
HasUppercase bool
HasNumber bool
HasSymbol bool
}
PasswordAgePolicy struct {
ExpireWarnDays uint64
MaxAgeDays uint64
}
DomainPolicy struct {
UserLoginMustBeDomain bool
}
LoginPolicy struct {
AllowUsernamePassword bool
AllowRegister bool
AllowExternalIDP bool
ForceMFA bool
HidePasswordReset bool
PasswordlessType domain.PasswordlessType
PasswordCheckLifetime time.Duration
ExternalLoginCheckLifetime time.Duration
MfaInitSkipLifetime time.Duration
SecondFactorCheckLifetime time.Duration
MultiFactorCheckLifetime time.Duration
}
PrivacyPolicy struct {
TOSLink string
PrivacyLink string
HelpLink string
}
LockoutPolicy struct {
MaxAttempts uint64
ShouldShowLockoutFailure bool
}
EmailTemplate []byte
MessageTexts []*domain.CustomMessageText
}
type ZitadelConfig struct {
IsDevMode bool
BaseURL string
projectID string
mgmtID string
mgmtClientID string
adminID string
adminClientID string
authID string
authClientID string
consoleID string
consoleClientID string
}
func (s *InstanceSetup) generateIDs() (err error) {
s.Zitadel.projectID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.mgmtID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.mgmtClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName)
if err != nil {
return err
}
s.Zitadel.adminID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.adminClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName)
if err != nil {
return err
}
s.Zitadel.authID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.authClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName)
if err != nil {
return err
}
s.Zitadel.consoleID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.consoleClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName)
if err != nil {
return err
}
return nil
}
func (command *Command) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*domain.ObjectDetails, error) {
// TODO
// instanceID, err := id.SonyFlakeGenerator.Next()
// if err != nil {
// return nil, err
// }
ctx = authz.SetCtxData(authz.WithInstance(ctx, authz.Instance{ID: "system"}), authz.CtxData{OrgID: domain.IAMID, ResourceOwner: domain.IAMID})
orgID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
}
userID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
}
if err = setup.generateIDs(); err != nil {
return nil, err
}
setup.Org.Human.PasswordChangeRequired = true
instanceAgg := instance.NewAggregate()
orgAgg := org.NewAggregate(orgID, orgID)
userAgg := user.NewAggregate(userID, orgID)
projectAgg := project.NewAggregate(setup.Zitadel.projectID, orgID)
validations := []preparation.Validation{
AddPasswordComplexityPolicy(
instanceAgg,
setup.PasswordComplexityPolicy.MinLength,
setup.PasswordComplexityPolicy.HasLowercase,
setup.PasswordComplexityPolicy.HasUppercase,
setup.PasswordComplexityPolicy.HasNumber,
setup.PasswordComplexityPolicy.HasSymbol,
),
AddPasswordAgePolicy(
instanceAgg,
setup.PasswordAgePolicy.ExpireWarnDays,
setup.PasswordAgePolicy.MaxAgeDays,
),
AddDefaultDomainPolicy(
instanceAgg,
setup.DomainPolicy.UserLoginMustBeDomain,
),
AddDefaultLoginPolicy(
instanceAgg,
setup.LoginPolicy.AllowUsernamePassword,
setup.LoginPolicy.AllowRegister,
setup.LoginPolicy.AllowExternalIDP,
setup.LoginPolicy.ForceMFA,
setup.LoginPolicy.HidePasswordReset,
setup.LoginPolicy.PasswordlessType,
setup.LoginPolicy.PasswordCheckLifetime,
setup.LoginPolicy.ExternalLoginCheckLifetime,
setup.LoginPolicy.MfaInitSkipLifetime,
setup.LoginPolicy.SecondFactorCheckLifetime,
setup.LoginPolicy.MultiFactorCheckLifetime,
),
AddSecondFactorToDefaultLoginPolicy(instanceAgg, domain.SecondFactorTypeOTP),
AddSecondFactorToDefaultLoginPolicy(instanceAgg, domain.SecondFactorTypeU2F),
AddMultiFactorToDefaultLoginPolicy(instanceAgg, domain.MultiFactorTypeU2FWithPIN),
AddPrivacyPolicy(instanceAgg, setup.PrivacyPolicy.TOSLink, setup.PrivacyPolicy.PrivacyLink, setup.PrivacyPolicy.HelpLink),
AddDefaultLockoutPolicy(instanceAgg, setup.LockoutPolicy.MaxAttempts, setup.LockoutPolicy.ShouldShowLockoutFailure),
AddEmailTemplate(instanceAgg, setup.EmailTemplate),
}
for _, msg := range setup.MessageTexts {
validations = append(validations, SetInstanceCustomTexts(instanceAgg, msg))
}
validations = append(validations,
AddOrg(orgAgg, setup.Org.Name, command.iamDomain),
AddHumanCommand(userAgg, &setup.Org.Human, command.userPasswordAlg),
AddOrgMember(orgAgg, userID, domain.RoleOrgOwner),
AddProject(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified),
SetIAMProject(instanceAgg, projectAgg.ID),
AddAPIApp(
*projectAgg,
setup.Zitadel.mgmtID,
mgmtAppName,
setup.Zitadel.mgmtClientID,
nil,
domain.APIAuthMethodTypePrivateKeyJWT,
),
AddAPIApp(
*projectAgg,
setup.Zitadel.adminID,
adminAppName,
setup.Zitadel.adminClientID,
nil,
domain.APIAuthMethodTypePrivateKeyJWT,
),
AddAPIApp(
*projectAgg,
setup.Zitadel.authID,
authAppName,
setup.Zitadel.authClientID,
nil,
domain.APIAuthMethodTypePrivateKeyJWT,
),
AddOIDCApp(
*projectAgg,
domain.OIDCVersionV1,
setup.Zitadel.consoleID,
consoleAppName,
setup.Zitadel.consoleClientID,
nil,
[]string{setup.Zitadel.BaseURL + consoleRedirectPath},
[]domain.OIDCResponseType{domain.OIDCResponseTypeCode},
[]domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
domain.OIDCApplicationTypeUserAgent,
domain.OIDCAuthMethodTypeNone,
[]string{setup.Zitadel.BaseURL + consolePostLogoutPath},
setup.Zitadel.IsDevMode,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
),
)
cmds, err := preparation.PrepareCommands(ctx, command.es.Filter, validations...)
if err != nil {
return nil, err
}
events, err := command.es.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: orgID,
}, nil
}
//SetIAMProject defines the commands to set the id of the IAM project onto the instance
func SetIAMProject(a *instance.Aggregate, projectID string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{
instance.NewIAMProjectSetEvent(ctx, &a.Aggregate, projectID),
}, nil
}, nil
}
}
@@ -0,0 +1,25 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func AddDefaultDomainPolicy(
a *instance.Aggregate,
userLoginMustBeDomain bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{
instance.NewDomainPolicyAddedEvent(ctx, &a.Aggregate,
userLoginMustBeDomain,
),
}, nil
}, nil
}
}
@@ -0,0 +1,107 @@
package command
import (
"context"
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func AddEmailTemplate(
a *instance.Aggregate,
tempalte []byte,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{
instance.NewMailTemplateAddedEvent(ctx, &a.Aggregate,
tempalte,
),
}, nil
}, nil
}
}
func SetInstanceCustomTexts(
a *instance.Aggregate,
msg *domain.CustomMessageText,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
existing, err := existingInstanceCustomMessageText(ctx, filter, msg.MessageTextType, msg.Language)
if err != nil {
return nil, err
}
cmds := make([]eventstore.Command, 0, 7)
if existing.Greeting != msg.Greeting {
if msg.Greeting != "" {
cmds = append(cmds, instance.NewCustomTextSetEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageGreeting, msg.Greeting, msg.Language))
} else {
cmds = append(cmds, instance.NewCustomTextRemovedEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageGreeting, msg.Language))
}
}
if existing.Subject != msg.Subject {
if msg.Subject != "" {
cmds = append(cmds, instance.NewCustomTextSetEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageSubject, msg.Subject, msg.Language))
} else {
cmds = append(cmds, instance.NewCustomTextRemovedEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageSubject, msg.Language))
}
}
if existing.Title != msg.Title {
if msg.Title != "" {
cmds = append(cmds, instance.NewCustomTextSetEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageTitle, msg.Title, msg.Language))
} else {
cmds = append(cmds, instance.NewCustomTextRemovedEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageTitle, msg.Language))
}
}
if existing.PreHeader != msg.PreHeader {
if msg.PreHeader != "" {
cmds = append(cmds, instance.NewCustomTextSetEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessagePreHeader, msg.PreHeader, msg.Language))
} else {
cmds = append(cmds, instance.NewCustomTextRemovedEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessagePreHeader, msg.Language))
}
}
if existing.Text != msg.Text {
if msg.Text != "" {
cmds = append(cmds, instance.NewCustomTextSetEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageText, msg.Text, msg.Language))
} else {
cmds = append(cmds, instance.NewCustomTextRemovedEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageText, msg.Language))
}
}
if existing.ButtonText != msg.ButtonText {
if msg.ButtonText != "" {
cmds = append(cmds, instance.NewCustomTextSetEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageButtonText, msg.ButtonText, msg.Language))
} else {
cmds = append(cmds, instance.NewCustomTextRemovedEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageButtonText, msg.Language))
}
}
if existing.FooterText != msg.FooterText {
if msg.FooterText != "" {
cmds = append(cmds, instance.NewCustomTextSetEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageFooterText, msg.FooterText, msg.Language))
} else {
cmds = append(cmds, instance.NewCustomTextRemovedEvent(ctx, &a.Aggregate, msg.MessageTextType, domain.MessageFooterText, msg.Language))
}
}
// TODO: what if no text changed? len(events) == 0
return cmds, nil
}, nil
}
}
func existingInstanceCustomMessageText(ctx context.Context, filter preparation.FilterToQueryReducer, textType string, lang language.Tag) (*command.InstanceCustomMessageTextWriteModel, error) {
writeModel := command.NewInstanceCustomMessageTextWriteModel(textType, lang)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
writeModel.Reduce()
return writeModel, nil
}
@@ -0,0 +1,45 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func AddDefaultLabelPolicy(
a *instance.Aggregate,
primaryColor,
backgroundColor,
warnColor,
fontColor,
primaryColorDark,
backgroundColorDark,
warnColorDark,
fontColorDark string,
hideLoginNameSuffix,
errorMsgPopup,
disableWatermark bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{
instance.NewLabelPolicyAddedEvent(ctx, &a.Aggregate,
primaryColor,
backgroundColor,
warnColor,
fontColor,
primaryColorDark,
backgroundColorDark,
warnColorDark,
fontColorDark,
hideLoginNameSuffix,
errorMsgPopup,
disableWatermark,
),
}, nil
}, nil
}
}
@@ -0,0 +1,24 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func AddDefaultLockoutPolicy(
a *instance.Aggregate,
maxAttempts uint64,
showLockoutFailure bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{
instance.NewLockoutPolicyAddedEvent(ctx, &a.Aggregate, maxAttempts, showLockoutFailure),
}, nil
}, nil
}
}
@@ -0,0 +1,69 @@
package command
import (
"context"
"time"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func AddDefaultLoginPolicy(
a *instance.Aggregate,
allowUsernamePassword bool,
allowRegister bool,
allowExternalIDP bool,
forceMFA bool,
hidePasswordReset bool,
passwordlessType domain.PasswordlessType,
passwordCheckLifetime time.Duration,
externalLoginCheckLifetime time.Duration,
mfaInitSkipLifetime time.Duration,
secondFactorCheckLifetime time.Duration,
multiFactorCheckLifetime time.Duration,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{
instance.NewLoginPolicyAddedEvent(ctx, &a.Aggregate,
allowUsernamePassword,
allowRegister,
allowExternalIDP,
forceMFA,
hidePasswordReset,
passwordlessType,
passwordCheckLifetime,
externalLoginCheckLifetime,
mfaInitSkipLifetime,
secondFactorCheckLifetime,
multiFactorCheckLifetime,
),
}, nil
}, nil
}
}
func AddSecondFactorToDefaultLoginPolicy(a *instance.Aggregate, factor domain.SecondFactorType) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{
instance.NewLoginPolicySecondFactorAddedEvent(ctx, &a.Aggregate, factor),
}, nil
}, nil
}
}
func AddMultiFactorToDefaultLoginPolicy(a *instance.Aggregate, factor domain.MultiFactorType) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{
instance.NewLoginPolicyMultiFactorAddedEvent(ctx, &a.Aggregate, factor),
}, nil
}, nil
}
}
@@ -0,0 +1,27 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func AddPasswordAgePolicy(
a *instance.Aggregate,
expireWarnDays,
maxAgeDays uint64,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{
instance.NewPasswordAgePolicyAddedEvent(ctx, &a.Aggregate,
expireWarnDays,
maxAgeDays,
),
}, nil
}, nil
}
}
@@ -0,0 +1,33 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func AddPasswordComplexityPolicy(
a *instance.Aggregate,
minLength uint64,
hasLowercase,
hasUppercase,
hasNumber,
hasSymbol bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{
instance.NewPasswordComplexityPolicyAddedEvent(ctx, &a.Aggregate,
minLength,
hasLowercase,
hasUppercase,
hasNumber,
hasSymbol,
),
}, nil
}, nil
}
}
@@ -0,0 +1,25 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func AddPrivacyPolicy(
a *instance.Aggregate,
tosLink,
privacyLink,
helpLink string,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{
instance.NewPrivacyPolicyAddedEvent(ctx, &a.Aggregate, tosLink, privacyLink, helpLink),
}, nil
}, nil
}
}
+72
View File
@@ -0,0 +1,72 @@
package command
import (
"context"
"strings"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/repository/org"
user_repo "github.com/caos/zitadel/internal/repository/user"
)
type OrgSetup struct {
Name string
Human AddHuman
}
func (command *Command) SetUpOrg(ctx context.Context, o *OrgSetup) (*domain.ObjectDetails, error) {
orgID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
}
userID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
}
orgAgg := org.NewAggregate(orgID, orgID)
userAgg := user_repo.NewAggregate(userID, orgID)
cmds, err := preparation.PrepareCommands(ctx, command.es.Filter,
AddOrg(orgAgg, o.Name, command.iamDomain),
AddHumanCommand(userAgg, &o.Human, command.userPasswordAlg),
AddOrgMember(orgAgg, userID, domain.RoleOrgOwner),
)
if err != nil {
return nil, err
}
events, err := command.es.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: orgID,
}, nil
}
//AddOrg defines the commands to create a new org,
// this includes the verified default domain
func AddOrg(a *org.Aggregate, name, iamDomain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument")
}
defaultDomain := domain.NewIAMDomainName(name, iamDomain)
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{
org.NewOrgAddedEvent(ctx, &a.Aggregate, name),
org.NewDomainAddedEvent(ctx, &a.Aggregate, defaultDomain),
org.NewDomainVerifiedEvent(ctx, &a.Aggregate, defaultDomain),
org.NewDomainPrimarySetEvent(ctx, &a.Aggregate, defaultDomain),
}, nil
}, nil
}
}
+46
View File
@@ -0,0 +1,46 @@
package command
import (
"context"
"strings"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
)
func AddOrgDomain(a *org.Aggregate, domain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if domain = strings.TrimSpace(domain); domain == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{org.NewDomainAddedEvent(ctx, &a.Aggregate, domain)}, nil
}, nil
}
}
func VerifyOrgDomain(a *org.Aggregate, domain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if domain = strings.TrimSpace(domain); domain == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-yqlVQ", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{org.NewDomainVerifiedEvent(ctx, &a.Aggregate, domain)}, nil
}, nil
}
}
func SetPrimaryOrgDomain(a *org.Aggregate, domain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if domain = strings.TrimSpace(domain); domain == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-gmNqY", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists and verified
return []eventstore.Command{org.NewDomainPrimarySetEvent(ctx, &a.Aggregate, domain)}, nil
}, nil
}
}
+133
View File
@@ -0,0 +1,133 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
)
func TestAddDomain(t *testing.T) {
type args struct {
a *org.Aggregate
domain string
}
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: org.NewAggregate("test", "test"),
domain: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: org.NewAggregate("test", "test"),
domain: "domain",
},
want: Want{
Commands: []eventstore.Command{
org.NewDomainAddedEvent(context.Background(), &org.NewAggregate("test", "test").Aggregate, "domain"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddOrgDomain(tt.args.a, tt.args.domain), nil, tt.want)
})
}
}
func TestVerifyDomain(t *testing.T) {
type args struct {
a *org.Aggregate
domain string
}
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: org.NewAggregate("test", "test"),
domain: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-yqlVQ", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: org.NewAggregate("test", "test"),
domain: "domain",
},
want: Want{
Commands: []eventstore.Command{
org.NewDomainVerifiedEvent(context.Background(), &org.NewAggregate("test", "test").Aggregate, "domain"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, VerifyOrgDomain(tt.args.a, tt.args.domain), nil, tt.want)
})
}
}
func TestSetDomainPrimary(t *testing.T) {
type args struct {
a *org.Aggregate
domain string
}
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: org.NewAggregate("test", "test"),
domain: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-gmNqY", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: org.NewAggregate("test", "test"),
domain: "domain",
},
want: Want{
Commands: []eventstore.Command{
org.NewDomainPrimarySetEvent(context.Background(), &org.NewAggregate("test", "test").Aggregate, "domain"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, SetPrimaryOrgDomain(tt.args.a, tt.args.domain), nil, tt.want)
})
}
}
+65
View File
@@ -0,0 +1,65 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
)
func AddOrgMember(a *org.Aggregate, userID string, roles ...string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if userID == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument")
}
// TODO: check roles
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if exists, err := ExistsUser(ctx, filter, userID, a.ID); err != nil || !exists {
return nil, errors.ThrowNotFound(err, "ORG-GoXOn", "Errors.User.NotFound")
}
if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember {
return nil, errors.ThrowAlreadyExists(err, "ORG-poWwe", "Errors.Org.Member.AlreadyExists")
}
return []eventstore.Command{org.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil
},
nil
}
}
func IsOrgMember(ctx context.Context, filter preparation.FilterToQueryReducer, orgID, userID string) (isMember bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(orgID).
OrderAsc().
AddQuery().
AggregateIDs(orgID).
AggregateTypes(org.AggregateType).
EventTypes(
org.MemberAddedEventType,
org.MemberRemovedEventType,
org.MemberCascadeRemovedEventType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch e := event.(type) {
case *org.MemberAddedEvent:
if e.UserID == userID {
isMember = true
}
case *org.MemberRemovedEvent:
if e.UserID == userID {
isMember = false
}
case *org.MemberCascadeRemovedEvent:
if e.UserID == userID {
isMember = false
}
}
}
return isMember, nil
}
+249
View File
@@ -0,0 +1,249 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
"github.com/caos/zitadel/internal/repository/user"
)
func TestAddMember(t *testing.T) {
type args struct {
a *org.Aggregate
userID string
roles []string
filter preparation.FilterToQueryReducer
}
ctx := context.Background()
agg := org.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "no user id",
args: args{
a: agg,
userID: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"),
},
},
// {
// name: "TODO: invalid roles",
// args: args{
// a: agg,
// userID: "",
// roles: []string{""},
// },
// want: preparation.Want{
// ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"),
// },
// },
{
name: "user not exists",
args: args{
a: agg,
userID: "userID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowNotFound(nil, "ORG-GoXOn", "Errors.User.NotFound"),
},
},
{
name: "already member",
args: args{
a: agg,
userID: "userID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
ctx,
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
}, nil
}).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
ctx,
&org.NewAggregate("id", "ro").Aggregate,
"userID",
),
}, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowAlreadyExists(nil, "ORG-poWwe", "Errors.Org.Member.AlreadyExists"),
},
},
{
name: "correct",
args: args{
a: agg,
userID: "userID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
ctx,
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
}, nil
}).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Filter(),
},
want: Want{
Commands: []eventstore.Command{
org.NewMemberAddedEvent(ctx, &agg.Aggregate, "userID"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddOrgMember(tt.args.a, tt.args.userID, tt.args.roles...), tt.args.filter, tt.want)
})
}
}
func TestIsMember(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
orgID string
userID string
}
tests := []struct {
name string
args args
wantExists bool
wantErr bool
}{
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: false,
},
{
name: "member added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: true,
wantErr: false,
},
{
name: "member removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
org.NewMemberRemovedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: false,
},
{
name: "member cascade removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
org.NewMemberCascadeRemovedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: false,
},
{
name: "error durring filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal")
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotExists, err := IsOrgMember(context.Background(), tt.args.filter, tt.args.orgID, tt.args.userID)
if (err != nil) != tt.wantErr {
t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotExists != tt.wantExists {
t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists)
}
})
}
}
+57
View File
@@ -0,0 +1,57 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
)
func TestAddOrg(t *testing.T) {
type args struct {
a *org.Aggregate
name string
}
ctx := context.Background()
agg := org.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: agg,
name: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: agg,
name: "caos ag",
},
want: Want{
Commands: []eventstore.Command{
org.NewOrgAddedEvent(ctx, &agg.Aggregate, "caos ag"),
org.NewDomainAddedEvent(ctx, &agg.Aggregate, "caos-ag.localhost"),
org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "caos-ag.localhost"),
org.NewDomainPrimarySetEvent(ctx, &agg.Aggregate, "caos-ag.localhost"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddOrg(tt.args.a, tt.args.name, "localhost"), nil, tt.want)
})
}
}
@@ -0,0 +1,80 @@
package preparation
import (
"context"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
)
// Validation of the input values of the command and if correct returns
// the function to create commands or if not valid an error
type Validation func() (CreateCommands, error)
// CreateCommands builds the commands
// the filter param is an extended version of the eventstore filter method
// it filters for events including the commands on the current context
type CreateCommands func(context.Context, FilterToQueryReducer) ([]eventstore.Command, error)
// FilterToQueryReducer is an abstraction of the eventstore method
type FilterToQueryReducer func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error)
var (
//ErrNotExecutable is thrown if no command creator was created
ErrNotExecutable = errors.ThrowInvalidArgument(nil, "PREPA-pH70n", "Errors.Internal")
)
// PrepareCommands checks the passed validations and if ok creates the commands
func PrepareCommands(ctx context.Context, filter FilterToQueryReducer, validations ...Validation) (cmds []eventstore.Command, err error) {
commanders, err := validate(validations)
if err != nil {
return nil, err
}
return create(ctx, filter, commanders)
}
func validate(validations []Validation) ([]CreateCommands, error) {
creators := make([]CreateCommands, 0, len(validations))
for _, validate := range validations {
cmds, err := validate()
if err != nil {
return nil, err
}
creators = append(creators, cmds)
}
if len(creators) == 0 {
return nil, ErrNotExecutable
}
return creators, nil
}
func create(ctx context.Context, filter FilterToQueryReducer, commanders []CreateCommands) (cmds []eventstore.Command, err error) {
for _, command := range commanders {
cmd, err := command(ctx, transactionFilter(filter, cmds))
if err != nil {
return nil, err
}
cmds = append(cmds, cmd...)
}
return cmds, nil
}
func transactionFilter(filter FilterToQueryReducer, commands []eventstore.Command) FilterToQueryReducer {
return func(ctx context.Context, query *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
events, err := filter(ctx, query)
if err != nil {
return nil, err
}
for _, command := range commands {
event := command.(eventstore.Event)
if !query.Matches(event, len(events)) {
continue
}
events = append(events, event)
}
return events, nil
}
}
@@ -0,0 +1,176 @@
package preparation
import (
"context"
"errors"
"reflect"
"testing"
"github.com/caos/zitadel/internal/eventstore"
)
var errTest = errors.New("test")
func Test_validate(t *testing.T) {
type args struct {
validations []Validation
}
type want struct {
len int
err error
}
tests := []struct {
name string
args args
want want
}{
{
name: "no validations",
args: args{},
want: want{
err: ErrNotExecutable,
},
},
{
name: "error in validation",
args: args{
validations: []Validation{
func() (CreateCommands, error) {
return nil, errTest
},
},
},
want: want{
err: errTest,
},
},
{
name: "correct",
args: args{
validations: []Validation{
func() (CreateCommands, error) {
return func(_ context.Context, _ FilterToQueryReducer) ([]eventstore.Command, error) {
return nil, nil
}, nil
},
},
},
want: want{
len: 1,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := validate(tt.args.validations)
if !errors.Is(err, tt.want.err) {
t.Errorf("validate() error = %v, wantErr %v", err, tt.want.err)
return
}
if len(got) != tt.want.len {
t.Errorf("validate() len = %v, want %v", len(got), tt.want.len)
}
})
}
}
func Test_create(t *testing.T) {
type args struct {
commanders []CreateCommands
}
type want struct {
err error
len int
}
tests := []struct {
name string
args args
want want
}{
{
name: "error in command",
want: want{
err: errTest,
},
args: args{
commanders: []CreateCommands{
func(_ context.Context, _ FilterToQueryReducer) ([]eventstore.Command, error) {
return nil, errTest
},
},
},
},
{
name: "no commands",
want: want{},
args: args{
commanders: []CreateCommands{
func(_ context.Context, _ FilterToQueryReducer) ([]eventstore.Command, error) {
return nil, nil
},
},
},
},
{
name: "multiple commands",
want: want{
len: 3,
},
args: args{
commanders: []CreateCommands{
func(_ context.Context, _ FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{new(testCommand), new(testCommand)}, nil
},
func(_ context.Context, _ FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{new(testCommand)}, nil
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotCmds, err := create(context.Background(), nil, tt.args.commanders)
if !errors.Is(err, tt.want.err) {
t.Errorf("create() error = %v, wantErr %v", err, tt.want.err)
return
}
if len(gotCmds) != tt.want.len {
t.Errorf("create() len = %d, want %d", len(gotCmds), tt.want.len)
}
})
}
}
func Test_transactionFilter(t *testing.T) {
type args struct {
filter FilterToQueryReducer
commands []eventstore.Command
}
tests := []struct {
name string
args args
want FilterToQueryReducer
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := transactionFilter(tt.args.filter, tt.args.commands); !reflect.DeepEqual(got, tt.want) {
t.Errorf("transactionFilter() = %v, want %v", got, tt.want)
}
})
}
}
type testCommand struct {
eventstore.BaseEvent
}
func (c *testCommand) Data() interface{} {
return nil
}
func (c *testCommand) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
+81
View File
@@ -0,0 +1,81 @@
// this is a helper file for tests
package command
import (
"context"
"errors"
"reflect"
"testing"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/eventstore"
)
//Want represents the expected values for each step
type Want struct {
ValidationErr error
CreateErr error
Commands []eventstore.Command
}
//AssertValidation checks if the validation works as inteded
func AssertValidation(t *testing.T, validation preparation.Validation, filter preparation.FilterToQueryReducer, want Want) {
t.Helper()
creates, err := validation()
if !errors.Is(err, want.ValidationErr) {
t.Errorf("wrong validation err = %v, want %v", err, want.ValidationErr)
return
}
if err != nil {
return
}
cmds, err := creates(context.Background(), filter)
if !errors.Is(err, want.CreateErr) {
t.Errorf("wrong create err = %v, want %v", err, want.CreateErr)
return
}
if err != nil {
return
}
if len(cmds) != len(want.Commands) {
t.Errorf("wrong length of commands = %v, want %v", eventTypes(cmds), eventTypes(want.Commands))
return
}
for i, cmd := range want.Commands {
if !reflect.DeepEqual(cmd, cmds[i]) {
t.Errorf("unexpected command: = %v, want %v", cmds[i], cmd)
}
}
}
func eventTypes(cmds []eventstore.Command) []eventstore.EventType {
types := make([]eventstore.EventType, len(cmds))
for i, cmd := range cmds {
types[i] = cmd.Type()
}
return types
}
type MultiFilter struct {
count int
filters []preparation.FilterToQueryReducer
}
func NewMultiFilter() *MultiFilter {
return new(MultiFilter)
}
func (mf *MultiFilter) Append(filter preparation.FilterToQueryReducer) *MultiFilter {
mf.filters = append(mf.filters, filter)
return mf
}
func (mf *MultiFilter) Filter() preparation.FilterToQueryReducer {
return func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
mf.count++
return mf.filters[mf.count-1](ctx, queryFactory)
}
}
+75
View File
@@ -0,0 +1,75 @@
package command
import (
"context"
"strings"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/project"
)
func AddProject(
a *project.Aggregate,
name string,
owner string,
projectRoleAssertion bool,
projectRoleCheck bool,
hasProjectCheck bool,
privateLabelingSetting domain.PrivateLabelingSetting,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-C01yo", "Errors.Invalid.Argument")
}
if !privateLabelingSetting.Valid() {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-AO52V", "Errors.Invalid.Argument")
}
if owner == "" {
return nil, errors.ThrowPreconditionFailed(nil, "PROJE-hzxwo", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{
project.NewProjectAddedEvent(ctx, &a.Aggregate,
name,
projectRoleAssertion,
projectRoleCheck,
hasProjectCheck,
privateLabelingSetting,
),
project.NewProjectMemberAddedEvent(ctx, &a.Aggregate,
owner,
domain.RoleProjectOwner),
}, nil
}, nil
}
}
func ExistsProject(ctx context.Context, filter preparation.FilterToQueryReducer, projectID, resourceOwner string) (exists bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(resourceOwner).
OrderAsc().
AddQuery().
AggregateTypes(project.AggregateType).
AggregateIDs(projectID).
EventTypes(
project.ProjectAddedType,
project.ProjectRemovedType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch event.(type) {
case *project.ProjectAddedEvent:
exists = true
case *project.ProjectRemovedEvent:
exists = false
}
}
return exists, nil
}
+155
View File
@@ -0,0 +1,155 @@
package command
import (
"context"
"strings"
"time"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/project"
)
func AddOIDCApp(
a project.Aggregate,
version domain.OIDCVersion,
appID,
name,
clientID string,
clientSecret *crypto.CryptoValue,
redirectUris []string,
responseTypes []domain.OIDCResponseType,
grantTypes []domain.OIDCGrantType,
applicationType domain.OIDCApplicationType,
authMethodType domain.OIDCAuthMethodType,
postLogoutRedirectUris []string,
devMode bool,
accessTokenType domain.OIDCTokenType,
accessTokenRoleAssertion bool,
idTokenRoleAssertion bool,
idTokenUserinfoAssertion bool,
clockSkew time.Duration,
additionalOrigins []string,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if appID == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-NnavI", "Errors.Invalid.Argument")
}
if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-Fef31", "Errors.Invalid.Argument")
}
if clientID == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-ghTsJ", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if exists, err := ExistsProject(ctx, filter, a.ID, a.ResourceOwner); !exists || err != nil {
return nil, errors.ThrowNotFound(err, "PROJE-5LQ0U", "Errors.Project.NotFound")
}
return []eventstore.Command{
project.NewApplicationAddedEvent(
ctx,
&a.Aggregate,
appID,
name,
),
project.NewOIDCConfigAddedEvent(
ctx,
&a.Aggregate,
version,
appID,
clientID,
clientSecret,
redirectUris,
responseTypes,
grantTypes,
applicationType,
authMethodType,
postLogoutRedirectUris,
devMode,
accessTokenType,
accessTokenRoleAssertion,
idTokenRoleAssertion,
idTokenUserinfoAssertion,
clockSkew,
additionalOrigins,
),
}, nil
}, nil
}
}
func AddAPIApp(
a project.Aggregate,
appID,
name,
clientID string,
clientSecret *crypto.CryptoValue,
authMethodType domain.APIAuthMethodType,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if appID == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-XHsKt", "Errors.Invalid.Argument")
}
if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-F7g21", "Errors.Invalid.Argument")
}
if clientID == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-XXED5", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if exists, err := ExistsProject(ctx, filter, a.ID, a.ResourceOwner); !exists || err != nil {
return nil, errors.ThrowNotFound(err, "PROJE-Sf2gb", "Errors.Project.NotFound")
}
return []eventstore.Command{
project.NewApplicationAddedEvent(
ctx,
&a.Aggregate,
appID,
name,
),
project.NewAPIConfigAddedEvent(
ctx,
&a.Aggregate,
appID,
clientID,
clientSecret,
authMethodType,
),
}, nil
}, nil
}
}
func ExistsApp(ctx context.Context, filter preparation.FilterToQueryReducer, projectID, appID, resourceOwner string) (exists bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(resourceOwner).
OrderAsc().
AddQuery().
AggregateTypes(project.AggregateType).
AggregateIDs(projectID).
EventTypes(
project.ApplicationAddedType,
project.ApplicationRemovedType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch e := event.(type) {
case *project.ApplicationAddedEvent:
if e.AppID == appID {
exists = true
}
case *project.ApplicationRemovedEvent:
if e.AppID == appID {
exists = false
}
}
}
return exists, nil
}
+386
View File
@@ -0,0 +1,386 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/project"
)
func TestAddOIDCApp(t *testing.T) {
type args struct {
a *project.Aggregate
appID string
name string
clientID string
filter preparation.FilterToQueryReducer
}
ctx := context.Background()
agg := project.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid appID",
args: args{
a: agg,
appID: "",
name: "name",
clientID: "clientID",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-NnavI", "Errors.Invalid.Argument"),
},
},
{
name: "invalid name",
args: args{
a: agg,
appID: "appID",
name: "",
clientID: "clientID",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-Fef31", "Errors.Invalid.Argument"),
},
},
{
name: "invalid clientID",
args: args{
a: agg,
appID: "appID",
name: "name",
clientID: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-ghTsJ", "Errors.Invalid.Argument"),
},
},
{
name: "project not exists",
args: args{
a: agg,
appID: "id",
name: "name",
clientID: "clientID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowNotFound(nil, "PROJE-5LQ0U", "Errors.Project.NotFound"),
},
},
{
name: "correct",
args: args{
a: agg,
appID: "appID",
name: "name",
clientID: "clientID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewProjectAddedEvent(
ctx,
&agg.Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
}, nil
}).
Filter(),
},
want: Want{
Commands: []eventstore.Command{
project.NewApplicationAddedEvent(ctx, &agg.Aggregate,
"appID",
"name",
),
project.NewOIDCConfigAddedEvent(ctx, &agg.Aggregate,
domain.OIDCVersionV1,
"appID",
"clientID",
nil,
nil,
nil,
nil,
domain.OIDCApplicationTypeWeb,
domain.OIDCAuthMethodTypeBasic,
nil,
false,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t,
AddOIDCApp(*tt.args.a,
domain.OIDCVersionV1,
tt.args.appID,
tt.args.name,
tt.args.clientID,
nil,
nil,
nil,
nil,
domain.OIDCApplicationTypeWeb,
domain.OIDCAuthMethodTypeBasic,
nil,
false,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
), tt.args.filter, tt.want)
})
}
}
func TestAddAPIConfig(t *testing.T) {
type args struct {
a *project.Aggregate
appID string
name string
clientID string
filter preparation.FilterToQueryReducer
}
ctx := context.Background()
agg := project.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid appID",
args: args{
a: agg,
appID: "",
name: "name",
clientID: "clientID",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-XHsKt", "Errors.Invalid.Argument"),
},
},
{
name: "invalid name",
args: args{
a: agg,
appID: "appID",
name: "",
clientID: "clientID",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-F7g21", "Errors.Invalid.Argument"),
},
},
{
name: "invalid clientID",
args: args{
a: agg,
appID: "appID",
name: "name",
clientID: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-XXED5", "Errors.Invalid.Argument"),
},
},
{
name: "project not exists",
args: args{
a: agg,
appID: "id",
name: "name",
clientID: "clientID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowNotFound(nil, "PROJE-Sf2gb", "Errors.Project.NotFound"),
},
},
{
name: "correct",
args: args{
a: agg,
appID: "appID",
name: "name",
clientID: "clientID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewProjectAddedEvent(
ctx,
&agg.Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
}, nil
}).
Filter(),
},
want: Want{
Commands: []eventstore.Command{
project.NewApplicationAddedEvent(
ctx,
&agg.Aggregate,
"appID",
"name",
),
project.NewAPIConfigAddedEvent(ctx, &agg.Aggregate,
"appID",
"clientID",
nil,
domain.APIAuthMethodTypeBasic,
),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t,
AddAPIApp(*tt.args.a,
tt.args.appID,
tt.args.name,
tt.args.clientID,
nil,
domain.APIAuthMethodTypeBasic,
), tt.args.filter, tt.want)
})
}
}
func TestExistsApp(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
appID string
projectID string
resourceOwner string
}
tests := []struct {
name string
args args
wantExists bool
wantErr bool
}{
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
appID: "appID",
projectID: "projectID",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "app added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewApplicationAddedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"appID",
"name",
),
}, nil
},
appID: "appID",
projectID: "projectID",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "app removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewApplicationAddedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"appID",
"name",
),
project.NewApplicationRemovedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"appID",
"name",
),
}, nil
},
appID: "appID",
projectID: "projectID",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "error durring filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal")
},
appID: "appID",
projectID: "projectID",
resourceOwner: "ro",
},
wantExists: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotExists, err := ExistsApp(context.Background(), tt.args.filter, tt.args.projectID, tt.args.appID, tt.args.resourceOwner)
if (err != nil) != tt.wantErr {
t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotExists != tt.wantExists {
t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists)
}
})
}
}
+195
View File
@@ -0,0 +1,195 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/project"
)
func TestAddProject(t *testing.T) {
type args struct {
a *project.Aggregate
name string
owner string
privateLabelingSetting domain.PrivateLabelingSetting
}
ctx := context.Background()
agg := project.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid name",
args: args{
a: agg,
name: "",
owner: "owner",
privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-C01yo", "Errors.Invalid.Argument"),
},
},
{
name: "invalid private labeling setting",
args: args{
a: agg,
name: "name",
owner: "owner",
privateLabelingSetting: -1,
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-AO52V", "Errors.Invalid.Argument"),
},
},
{
name: "invalid owner",
args: args{
a: agg,
name: "name",
owner: "",
privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
},
want: Want{
ValidationErr: errors.ThrowPreconditionFailed(nil, "PROJE-hzxwo", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: agg,
name: "ZITADEL",
owner: "CAOS AG",
privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
},
want: Want{
Commands: []eventstore.Command{
project.NewProjectAddedEvent(ctx, &agg.Aggregate,
"ZITADEL",
false,
false,
false,
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
),
project.NewProjectMemberAddedEvent(ctx, &agg.Aggregate,
"CAOS AG",
domain.RoleProjectOwner),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddProject(tt.args.a, tt.args.name, tt.args.owner, false, false, false, tt.args.privateLabelingSetting), nil, tt.want)
})
}
}
func TestExistsProject(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
id string
resourceOwner string
}
tests := []struct {
name string
args args
wantExists bool
wantErr bool
}{
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "project added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"name",
false,
false,
false,
domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "project removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"name",
false,
false,
false,
domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy,
),
project.NewProjectRemovedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"name",
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "error durring filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal")
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotExists, err := ExistsProject(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner)
if (err != nil) != tt.wantErr {
t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotExists != tt.wantExists {
t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists)
}
})
}
}
+40
View File
@@ -0,0 +1,40 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/user"
)
func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string) (exists bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(resourceOwner).
OrderAsc().
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(id).
EventTypes(
user.HumanRegisteredType,
user.UserV1RegisteredType,
user.HumanAddedType,
user.UserV1AddedType,
user.MachineAddedEventType,
user.UserRemovedType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch event.(type) {
case *user.HumanRegisteredEvent, *user.HumanAddedEvent, *user.MachineAddedEvent:
exists = true
case *user.UserRemovedEvent:
exists = false
}
}
return exists, nil
}
+50
View File
@@ -0,0 +1,50 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/errors"
)
func domainPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PolicyDomainWriteModel, error) {
wm, err := orgDomainPolicy(ctx, filter)
if err != nil || wm != nil && wm.State.Exists() {
return wm, err
}
wm, err = instanceDomainPolicy(ctx, filter)
if err != nil || wm != nil {
return wm, err
}
return nil, errors.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal")
}
func orgDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PolicyDomainWriteModel, error) {
policy := command.NewOrgDomainPolicyWriteModel(authz.GetCtxData(ctx).OrgID)
events, err := filter(ctx, policy.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return nil, nil
}
policy.AppendEvents(events...)
err = policy.Reduce()
return &policy.PolicyDomainWriteModel, err
}
func instanceDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PolicyDomainWriteModel, error) {
policy := command.NewInstanceDomainPolicyWriteModel()
events, err := filter(ctx, policy.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return nil, nil
}
policy.AppendEvents(events...)
err = policy.Reduce()
return &policy.PolicyDomainWriteModel, err
}
@@ -0,0 +1,266 @@
package command
import (
"context"
"reflect"
"testing"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
"github.com/caos/zitadel/internal/repository/org"
)
func Test_customDomainPolicy(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
}
tests := []struct {
name string
args args
want *command.PolicyDomainWriteModel
wantErr bool
}{
{
name: "err from filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "USER-IgYlN", "Errors.Internal")
},
},
want: nil,
wantErr: true,
},
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
},
want: nil,
wantErr: false,
},
{
name: "policy found",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
true,
),
}, nil
},
},
want: &command.PolicyDomainWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "id",
ResourceOwner: "ro",
Events: []eventstore.Event{},
},
UserLoginMustBeDomain: true,
State: domain.PolicyStateActive,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := orgDomainPolicy(context.Background(), tt.args.filter)
if (err != nil) != tt.wantErr {
t.Errorf("customDomainPolicy() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("customDomainPolicy() = %v, want %v", got, tt.want)
}
})
}
}
func Test_defaultDomainPolicy(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
}
tests := []struct {
name string
args args
want *command.PolicyDomainWriteModel
wantErr bool
}{
{
name: "err from filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "USER-IgYlN", "Errors.Internal")
},
},
want: nil,
wantErr: true,
},
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
},
want: nil,
wantErr: false,
},
{
name: "policy found",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
instance.NewDomainPolicyAddedEvent(
context.Background(),
&instance.NewAggregate().Aggregate,
true,
),
}, nil
},
},
want: &command.PolicyDomainWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "IAM",
ResourceOwner: "IAM",
Events: []eventstore.Event{},
},
UserLoginMustBeDomain: true,
State: domain.PolicyStateActive,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := instanceDomainPolicy(context.Background(), tt.args.filter)
if (err != nil) != tt.wantErr {
t.Errorf("defaultDomainPolicy() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("defaultDomainPolicy() = %v, want %v", got, tt.want)
}
})
}
}
func Test_DomainPolicy(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
}
tests := []struct {
name string
args args
want *command.PolicyDomainWriteModel
wantErr bool
}{
{
name: "err from filter custom",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "USER-IgYlN", "Errors.Internal")
},
},
want: nil,
wantErr: true,
},
{
name: "custom found",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
true,
),
}, nil
},
},
want: &command.PolicyDomainWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "id",
ResourceOwner: "ro",
Events: []eventstore.Event{},
},
UserLoginMustBeDomain: true,
State: domain.PolicyStateActive,
},
wantErr: false,
},
{
name: "err from filter default",
args: args{
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "USER-6HnsD", "Errors.Internal")
}).
Filter(),
},
want: nil,
wantErr: true,
},
{
name: "default found",
args: args{
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Append(func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
instance.NewDomainPolicyAddedEvent(
context.Background(),
&instance.NewAggregate().Aggregate,
true,
),
}, nil
}).
Filter(),
},
want: &command.PolicyDomainWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "IAM",
ResourceOwner: "IAM",
Events: []eventstore.Event{},
},
UserLoginMustBeDomain: true,
State: domain.PolicyStateActive,
},
wantErr: false,
},
{
name: "no policy found",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := domainPolicyWriteModel(context.Background(), tt.args.filter)
if (err != nil) != tt.wantErr {
t.Errorf("defaultDomainPolicy() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("defaultDomainPolicy() = %v, want %v", got, tt.want)
}
})
}
}
+97
View File
@@ -0,0 +1,97 @@
package command
import (
"context"
"strings"
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/user"
)
type AddHuman struct {
// Username is required
Username string
// FirstName is required
FirstName string
// LastName is required
LastName string
// NickName is required
NickName string
// DisplayName is required
DisplayName string
// Email is required
Email string
// PreferredLang is required
PreferredLang language.Tag
// Gender is required
Gender domain.Gender
//TODO: can it also be verified?
Phone string
//Password is optional
Password string
//PasswordChangeRequired is used if the `Password`-field is set
PasswordChangeRequired bool
}
func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if !domain.EmailRegex.MatchString(human.Email) {
return nil, errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument")
}
if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" {
return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument")
}
if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" {
return nil, errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument")
}
human.Phone = strings.TrimSpace(human.Phone)
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
domainPolicy, err := domainPolicyWriteModel(ctx, filter)
if err != nil {
return nil, err
}
cmd := user.NewHumanAddedEvent(
ctx,
&a.Aggregate,
human.Username,
human.FirstName,
human.LastName,
human.NickName,
human.DisplayName,
human.PreferredLang,
human.Gender,
human.Email, //TODO: pass if verified
domainPolicy.UserLoginMustBeDomain,
)
if human.Phone != "" {
cmd.AddPhoneData(human.Phone) //TODO: pass if verified
}
if human.Password != "" {
passwordComplexity, err := passwordComplexityPolicyWriteModel(ctx, filter)
if err != nil {
return nil, err
}
if err = passwordComplexity.Validate(human.Password); err != nil {
return nil, err
}
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
if err != nil {
return nil, err
}
cmd.AddPasswordData(secret, human.PasswordChangeRequired)
}
return []eventstore.Command{cmd}, nil
}, nil
}
}
+169
View File
@@ -0,0 +1,169 @@
package command
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
"github.com/caos/zitadel/internal/repository/user"
)
func TestAddHumanCommand(t *testing.T) {
type args struct {
a *user.Aggregate
human *AddHuman
passwordAlg crypto.HashAlgorithm
filter preparation.FilterToQueryReducer
}
agg := user.NewAggregate("id", "ro")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid email",
args: args{
a: agg,
human: &AddHuman{
Email: "invalid",
},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument"),
},
},
{
name: "invalid first name",
args: args{
a: agg,
human: &AddHuman{
Email: "[email protected]",
},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument"),
},
},
{
name: "invalid last name",
args: args{
a: agg,
human: &AddHuman{
Email: "[email protected]",
FirstName: "hurst",
},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument"),
},
},
{
name: "invalid password",
args: args{
a: agg,
human: &AddHuman{
Email: "[email protected]",
FirstName: "gigi",
LastName: "giraffe",
Password: "short",
},
filter: NewMultiFilter().Append(
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
true,
),
}, nil
}).
Append(
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
8,
true,
true,
true,
true,
),
}, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"),
},
},
{
name: "correct",
args: args{
a: agg,
human: &AddHuman{
Email: "[email protected]",
FirstName: "gigi",
LastName: "giraffe",
Password: "",
},
passwordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
filter: NewMultiFilter().Append(
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
true,
),
}, nil
}).
Append(
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
2,
false,
false,
false,
false,
),
}, nil
}).
Filter(),
},
want: Want{
Commands: []eventstore.Command{
user.NewHumanAddedEvent(
context.Background(),
&agg.Aggregate,
"",
"gigi",
"giraffe",
"",
"",
language.Und,
0,
"[email protected]",
true,
),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddHumanCommand(tt.args.a, tt.args.human, tt.args.passwordAlg), tt.args.filter, tt.want)
})
}
}
@@ -0,0 +1,50 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/errors"
)
func passwordComplexityPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PasswordComplexityPolicyWriteModel, error) {
wm, err := customPasswordComplexityPolicy(ctx, filter)
if err != nil || wm != nil && wm.State.Exists() {
return wm, err
}
wm, err = defaultPasswordComplexityPolicy(ctx, filter)
if err != nil || wm != nil {
return wm, err
}
return nil, errors.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal")
}
func customPasswordComplexityPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PasswordComplexityPolicyWriteModel, error) {
policy := command.NewOrgPasswordComplexityPolicyWriteModel(authz.GetCtxData(ctx).OrgID)
events, err := filter(ctx, policy.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return nil, nil
}
policy.AppendEvents(events...)
err = policy.Reduce()
return &policy.PasswordComplexityPolicyWriteModel, err
}
func defaultPasswordComplexityPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PasswordComplexityPolicyWriteModel, error) {
policy := command.NewInstancePasswordComplexityPolicyWriteModel()
events, err := filter(ctx, policy.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return nil, nil
}
policy.AppendEvents(events...)
err = policy.Reduce()
return &policy.PasswordComplexityPolicyWriteModel, err
}
@@ -0,0 +1,298 @@
package command
import (
"context"
"reflect"
"testing"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
"github.com/caos/zitadel/internal/repository/org"
)
func Test_customPasswordComplexityPolicy(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
}
tests := []struct {
name string
args args
want *command.PasswordComplexityPolicyWriteModel
wantErr bool
}{
{
name: "err from filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "USER-IgYlN", "Errors.Internal")
},
},
want: nil,
wantErr: true,
},
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
},
want: nil,
wantErr: false,
},
{
name: "policy found",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
8,
true,
true,
true,
true,
),
}, nil
},
},
want: &command.PasswordComplexityPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "id",
ResourceOwner: "ro",
Events: []eventstore.Event{},
},
MinLength: 8,
HasLowercase: true,
HasUppercase: true,
HasNumber: true,
HasSymbol: true,
State: domain.PolicyStateActive,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := customPasswordComplexityPolicy(context.Background(), tt.args.filter)
if (err != nil) != tt.wantErr {
t.Errorf("customPasswordComplexityPolicy() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("customPasswordComplexityPolicy() = %v, want %v", got, tt.want)
}
})
}
}
func Test_defaultPasswordComplexityPolicy(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
}
tests := []struct {
name string
args args
want *command.PasswordComplexityPolicyWriteModel
wantErr bool
}{
{
name: "err from filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "USER-IgYlN", "Errors.Internal")
},
},
want: nil,
wantErr: true,
},
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
},
want: nil,
wantErr: false,
},
{
name: "policy found",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
instance.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&instance.NewAggregate().Aggregate,
8,
true,
true,
true,
true,
),
}, nil
},
},
want: &command.PasswordComplexityPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "IAM",
ResourceOwner: "IAM",
Events: []eventstore.Event{},
},
MinLength: 8,
HasLowercase: true,
HasUppercase: true,
HasNumber: true,
HasSymbol: true,
State: domain.PolicyStateActive,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := defaultPasswordComplexityPolicy(context.Background(), tt.args.filter)
if (err != nil) != tt.wantErr {
t.Errorf("defaultPasswordComplexityPolicy() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("defaultPasswordComplexityPolicy() = %v, want %v", got, tt.want)
}
})
}
}
func Test_passwordComplexityPolicy(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
}
tests := []struct {
name string
args args
want *command.PasswordComplexityPolicyWriteModel
wantErr bool
}{
{
name: "err from filter custom",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "USER-IgYlN", "Errors.Internal")
},
},
want: nil,
wantErr: true,
},
{
name: "custom found",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
8,
true,
true,
true,
true,
),
}, nil
},
},
want: &command.PasswordComplexityPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "id",
ResourceOwner: "ro",
Events: []eventstore.Event{},
},
MinLength: 8,
HasLowercase: true,
HasUppercase: true,
HasNumber: true,
HasSymbol: true,
State: domain.PolicyStateActive,
},
wantErr: false,
},
{
name: "err from filter default",
args: args{
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "USER-6HnsD", "Errors.Internal")
}).
Filter(),
},
want: nil,
wantErr: true,
},
{
name: "default found",
args: args{
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Append(func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
instance.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&instance.NewAggregate().Aggregate,
8,
true,
true,
true,
true,
),
}, nil
}).
Filter(),
},
want: &command.PasswordComplexityPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "IAM",
ResourceOwner: "IAM",
Events: []eventstore.Event{},
},
MinLength: 8,
HasLowercase: true,
HasUppercase: true,
HasNumber: true,
HasSymbol: true,
State: domain.PolicyStateActive,
},
wantErr: false,
},
{
name: "no policy found",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := passwordComplexityPolicyWriteModel(context.Background(), tt.args.filter)
if (err != nil) != tt.wantErr {
t.Errorf("defaultPasswordComplexityPolicy() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("defaultPasswordComplexityPolicy() = %v, want %v", got, tt.want)
}
})
}
}
+166
View File
@@ -0,0 +1,166 @@
package command
import (
"context"
"testing"
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/user"
)
func TestExistsUser(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
id string
resourceOwner string
}
tests := []struct {
name string
args args
wantExists bool
wantErr bool
}{
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "human registered",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewHumanRegisteredEvent(
context.Background(),
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"firstName",
"lastName",
"nickName",
"displayName",
language.German,
domain.GenderFemale,
"[email protected]",
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "human added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewHumanAddedEvent(
context.Background(),
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"firstName",
"lastName",
"nickName",
"displayName",
language.German,
domain.GenderFemale,
"[email protected]",
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "machine added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
context.Background(),
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "user removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
context.Background(),
&user.NewAggregate("removed", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
user.NewUserRemovedEvent(
context.Background(),
&user.NewAggregate("removed", "ro").Aggregate,
"userName",
nil,
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "error durring filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "USER-Drebn", "Errors.Internal")
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner)
if (err != nil) != tt.wantErr {
t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotExists != tt.wantExists {
t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists)
}
})
}
}