diff --git a/backend/v3/domain/errors.go b/backend/v3/domain/errors.go index 77ed61312..76c588c54 100644 --- a/backend/v3/domain/errors.go +++ b/backend/v3/domain/errors.go @@ -1,12 +1,9 @@ package domain import ( - "errors" "fmt" ) -var ErrNoAdminSpecified = errors.New("at least one admin must be specified") - type wrongIDPTypeError struct { expected IDPType got string diff --git a/backend/v3/domain/settings.go b/backend/v3/domain/settings.go new file mode 100644 index 000000000..2edc06c2d --- /dev/null +++ b/backend/v3/domain/settings.go @@ -0,0 +1,430 @@ +package domain + +import ( + "context" + "net/url" + "time" + + "github.com/zitadel/zitadel/backend/v3/storage/database" + db_json "github.com/zitadel/zitadel/backend/v3/storage/database/json" +) + +//go:generate enumer -type SettingType -transform snake -trimprefix SettingType -sql +type SettingType uint8 + +const ( + SettingTypeLogin SettingType = iota + SettingTypeBranding + SettingTypePasswordComplexity + SettingTypePasswordExpiry + SettingTypeDomain + SettingTypeLockout + SettingTypeSecurity + SettingTypeOrganization + SettingTypeNotification + SettingTypeLegalAndSupport +) + +//go:generate enumer -type SettingState -transform snake -trimprefix SettingState -sql +type SettingState int32 + +const ( + SettingStateActive SettingState = iota + SettingStatePreview +) + +type Settings struct { + ID string `json:"id,omitempty" db:"id"` + InstanceID string `json:"instanceId,omitempty" db:"instance_id"` + OrganizationID *string `json:"organizationId,omitempty" db:"organization_id"` + Type SettingType `json:"type,omitempty" db:"type"` + State SettingState `json:"state,omitempty" db:"state"` + Settings []byte `json:"settings,omitempty" db:"settings"` + CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"` +} + +type settingsColumns interface { + IDColumn() database.Column + InstanceIDColumn() database.Column + OrganizationIDColumn() database.Column + TypeColumn() database.Column + StateColumn() database.Column + SettingsColumn() database.Column + CreatedAtColumn() database.Column + UpdatedAtColumn() database.Column + PrimaryKeyColumns() []database.Column + UniqueColumns() []database.Column +} + +type settingsConditions interface { + InstanceIDCondition(id string) database.Condition + OrganizationIDCondition(id *string) database.Condition + IDCondition(id string) database.Condition + PrimaryKeyCondition(instanceID, id string) database.Condition + UniqueCondition(instanceID string, orgID *string, typ SettingType, state SettingState) database.Condition +} + +type settingsChanges interface { + // SetSettings(settings string) database.Change + SetUpdatedAt(updatedAt *time.Time) database.Change +} + +type settingsRepository[T any] interface { + settingsColumns + settingsConditions + settingsChanges + + Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*T, error) + List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*T, error) + + Set(ctx context.Context, client database.QueryExecutor, settings *T) error + Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) +} + +type PasswordlessType int32 + +const ( + PasswordlessTypeNotAllowed PasswordlessType = iota + PasswordlessTypeAllowed +) + +type MultiFactorType int32 + +const ( + MultiFactorTypeUnspecified MultiFactorType = iota + MultiFactorTypeU2FWithPIN +) + +type SecondFactorType int32 + +const ( + SecondFactorTypeUnspecified SecondFactorType = iota + SecondFactorTypeTOTP + SecondFactorTypeU2F + SecondFactorTypeOTPEmail + SecondFactorTypeOTPSMS +) + +type loginSettingsJSONChanges interface { + SetSettingFields(value LoginSettingsAttributes) database.Change + + SetAllowUserNamePassword(value bool) db_json.JsonUpdate + SetAllowRegister(value bool) db_json.JsonUpdate + SetAllowExternalIDP(value bool) db_json.JsonUpdate + SetForceMFA(value bool) db_json.JsonUpdate + SetForceMFALocalOnly(value bool) db_json.JsonUpdate + SetHidePasswordReset(value bool) db_json.JsonUpdate + SetIgnoreUnknownUsernames(value bool) db_json.JsonUpdate + SetAllowDomainDiscovery(value bool) db_json.JsonUpdate + SetDisableLoginWithEmail(value bool) db_json.JsonUpdate + SetDisableLoginWithPhone(value bool) db_json.JsonUpdate + SetPasswordlessType(value PasswordlessType) db_json.JsonUpdate + SetDefaultRedirectURI(value string) db_json.JsonUpdate + SetPasswordCheckLifetime(value time.Duration) db_json.JsonUpdate + SetExternalLoginCheckLifetime(value time.Duration) db_json.JsonUpdate + SetMFAInitSkipLifetime(value time.Duration) db_json.JsonUpdate + SetSecondFactorCheckLifetime(value time.Duration) db_json.JsonUpdate + SetMultiFactorCheckLifetime(value time.Duration) db_json.JsonUpdate + + AddMFAType(value MultiFactorType) database.Change + RemoveMFAType(value MultiFactorType) database.Change + AddSecondFactorTypes(value SecondFactorType) database.Change + RemoveSecondFactorTypes(value SecondFactorType) database.Change +} + +type LoginSettings struct { + Settings + LoginSettingsAttributes +} + +type LoginSettingsAttributes struct { + AllowUserNamePassword *bool `json:"allowUsernamePassword,omitempty"` + AllowRegister *bool `json:"allowRegister,omitempty"` + AllowExternalIDP *bool `json:"allowExternalIdp,omitempty"` + ForceMFA *bool `json:"forceMfa,omitempty"` + ForceMFALocalOnly *bool `json:"forceMfaLocalOnly,omitempty"` + HidePasswordReset *bool `json:"hidePasswordReset,omitempty"` + IgnoreUnknownUsernames *bool `json:"ignoreUnknownUsernames,omitempty"` + AllowDomainDiscovery *bool `json:"allowDomainDiscovery,omitempty"` + DisableLoginWithEmail *bool `json:"disableLoginWithEmail,omitempty"` + DisableLoginWithPhone *bool `json:"disableLoginWithPhone,omitempty"` + PasswordlessType *PasswordlessType `json:"passwordlessType,omitempty"` + DefaultRedirectURI *string `json:"defaultRedirectUri,omitempty"` + PasswordCheckLifetime *time.Duration `json:"passwordCheckLifetime,omitempty"` + ExternalLoginCheckLifetime *time.Duration `json:"externalLoginCheckLifetime,omitempty"` + MFAInitSkipLifetime *time.Duration `json:"mfaInitSkipLifetime,omitempty"` + SecondFactorCheckLifetime *time.Duration `json:"secondFactorCheckLifetime,omitempty"` + MultiFactorCheckLifetime *time.Duration `json:"multiFactorCheckLifetime,omitempty"` + + MFAType []MultiFactorType `json:"mfaType,omitempty"` + SecondFactorTypes []SecondFactorType `json:"secondFactors,omitempty"` +} + +type LoginSettingsRepository interface { + settingsRepository[LoginSettings] + loginSettingsJSONChanges + + SetColumns(ctx context.Context, client database.QueryExecutor, settings *Settings, changes ...database.Change) error +} + +type BrandingPolicyThemeMode int32 + +const ( + BrandingPolicyThemeAuto BrandingPolicyThemeMode = iota + BrandingPolicyThemeLight + BrandingPolicyThemeDark +) + +type brandingSettingsJSONChanges interface { + SetSettingFields(value BrandingSettingsAttributes) database.Change + + SetPrimaryColorLight(value string) db_json.JsonUpdate + SetBackgroundColorLight(value string) db_json.JsonUpdate + SetWarnColorLight(value string) db_json.JsonUpdate + SetFontColorLight(value string) db_json.JsonUpdate + SetLogoURLLight(value url.URL) db_json.JsonUpdate + SetIconURLLight(value url.URL) db_json.JsonUpdate + + SetPrimaryColorDark(value string) db_json.JsonUpdate + SetBackgroundColorDark(value string) db_json.JsonUpdate + SetWarnColorDark(value string) db_json.JsonUpdate + SetFontColorDark(value string) db_json.JsonUpdate + SetLogoURLDark(value url.URL) db_json.JsonUpdate + SetIconURLDark(value url.URL) db_json.JsonUpdate + + SetHideLoginNameSuffix(value bool) db_json.JsonUpdate + SetErrorMsgPopup(value bool) db_json.JsonUpdate + SetDisableWatermark(value bool) db_json.JsonUpdate + SetThemeMode(value BrandingPolicyThemeMode) db_json.JsonUpdate + SetFontURL(value url.URL) db_json.JsonUpdate +} + +type BrandingSettings struct { + Settings + BrandingSettingsAttributes +} + +type BrandingSettingsAttributes struct { + PrimaryColorLight *string `json:"primaryColorLight,omitempty"` + BackgroundColorLight *string `json:"backgroundColorLight,omitempty"` + WarnColorLight *string `json:"warnColorLight,omitempty"` + FontColorLight *string `json:"fontColorLight,omitempty"` + LogoURLLight *url.URL `json:"logoUrlLight,omitempty"` + IconURLLight *url.URL `json:"iconUrlLight,omitempty"` + + PrimaryColorDark *string `json:"primaryColorDark,omitempty"` + BackgroundColorDark *string `json:"backgroundColorDark,omitempty"` + WarnColorDark *string `json:"warnColorDark,omitempty"` + FontColorDark *string `json:"fontColorDark,omitempty"` + LogoURLDark *url.URL `json:"logoUrlDark,omitempty"` + IconURLDark *url.URL `json:"iconUrlDark,omitempty"` + + HideLoginNameSuffix *bool `json:"hideLoginNameSuffix,omitempty"` + ErrorMsgPopup *bool `json:"errorMsgPopup,omitempty"` + DisableWatermark *bool `json:"disableMsgPopup,omitempty"` + ThemeMode *BrandingPolicyThemeMode `json:"themeMode,omitempty"` + FontURL *url.URL `json:"fontUrl,omitempty"` +} + +type BrandingSettingsRepository interface { + settingsRepository[BrandingSettings] + brandingSettingsJSONChanges + + StateCondition(typ SettingState) database.Condition + Activate(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) + ActivateAt(ctx context.Context, client database.QueryExecutor, condition database.Condition, updatedAt time.Time) (int64, error) + SetColumns(ctx context.Context, client database.QueryExecutor, settings *Settings, changes ...database.Change) error +} + +type passwordComplexitySettingsJSONChanges interface { + SetSettingFields(value PasswordComplexitySettingsAttributes) database.Change + + SetMinLength(value uint64) db_json.JsonUpdate + SetHasLowercase(value bool) db_json.JsonUpdate + SetHasUppercase(value bool) db_json.JsonUpdate + SetHasNumber(value bool) db_json.JsonUpdate + SetHasSymbol(value bool) db_json.JsonUpdate +} + +type PasswordComplexitySettings struct { + Settings + PasswordComplexitySettingsAttributes +} + +type PasswordComplexitySettingsAttributes struct { + MinLength *uint64 `json:"minLength,omitempty"` + HasLowercase *bool `json:"hasLowercase,omitempty"` + HasUppercase *bool `json:"hasUppercase,omitempty"` + HasNumber *bool `json:"hasNumber,omitempty"` + HasSymbol *bool `json:"hasSymbol,omitempty"` +} + +type PasswordComplexitySettingsRepository interface { + settingsRepository[PasswordComplexitySettings] + passwordComplexitySettingsJSONChanges +} + +type passwordExpirySettingsJSONChanges interface { + SetSettingFields(value PasswordExpirySettingsAttributes) database.Change + + SetExpireWarnDays(value uint64) db_json.JsonUpdate + SetMaxAgeDays(value uint64) db_json.JsonUpdate +} + +type PasswordExpirySettings struct { + Settings + PasswordExpirySettingsAttributes +} + +type PasswordExpirySettingsAttributes struct { + ExpireWarnDays *uint64 `json:"expireWarnDays,omitempty"` + MaxAgeDays *uint64 `json:"maxAgeDays,omitempty"` +} + +type PasswordExpirySettingsRepository interface { + settingsRepository[PasswordExpirySettings] + passwordExpirySettingsJSONChanges +} + +type lockoutSettingsJSONChanges interface { + SetSettingFields(value LockoutSettingsAttributes) database.Change + + SetMaxPasswordAttempts(value uint64) db_json.JsonUpdate + SetMaxOTPAttempts(value uint64) db_json.JsonUpdate + SetShowLockOutFailures(value bool) db_json.JsonUpdate +} + +type LockoutSettings struct { + Settings + LockoutSettingsAttributes +} + +type LockoutSettingsAttributes struct { + MaxPasswordAttempts *uint64 `json:"maxPasswordAttempts,omitempty"` + MaxOTPAttempts *uint64 `json:"maxOtpAttempts,omitempty"` + ShowLockOutFailures *bool `json:"showLockOutFailures,omitempty"` +} + +type LockoutSettingsRepository interface { + settingsRepository[LockoutSettings] + lockoutSettingsJSONChanges +} + +type securitySettingsJSONChanges interface { + SetSettingFields(value SecuritySettingsAttributes) database.Change + + SetEnableIframeEmbedding(value bool) db_json.JsonUpdate + SetAllowedOrigins(values []string) db_json.JsonUpdate + SetEnableImpersonation(value bool) db_json.JsonUpdate +} + +type SecuritySettings struct { + Settings + SecuritySettingsAttributes +} + +type SecuritySettingsAttributes struct { + EnableIframeEmbedding *bool `json:"enableIframeEmbedding,omitempty"` + AllowedOrigins []string `json:"allowedOrigins,omitempty"` + EnableImpersonation *bool `json:"enableImpersonation,omitempty"` +} + +type SecuritySettingsRepository interface { + settingsRepository[SecuritySettings] + securitySettingsJSONChanges +} + +type domainSettingsJSONChanges interface { + SetSettingFields(value DomainSettingsAttributes) database.Change + + SetLoginNameIncludesDomain(value bool) db_json.JsonUpdate + SetRequireOrgDomainVerification(value bool) db_json.JsonUpdate + SetSMTPSenderAddressMatchesInstanceDomain(value bool) db_json.JsonUpdate +} + +type DomainSettings struct { + Settings + DomainSettingsAttributes +} +type DomainSettingsAttributes struct { + LoginNameIncludesDomain *bool `json:"loginNameIncludesDomain,omitempty"` + RequireOrgDomainVerification *bool `json:"requireOrgDomainVerification,omitempty"` + SMTPSenderAddressMatchesInstanceDomain *bool `json:"smtpSenderAddressMatchesInstanceDomain,omitempty"` +} + +type DomainSettingsRepository interface { + settingsRepository[DomainSettings] + domainSettingsJSONChanges +} + +type organizationSettingsJSONChanges interface { + SetSettingFields(value OrganizationSettingsAttributes) database.Change + + SetOrganizationScopedUsernames(value bool) db_json.JsonUpdate +} + +type OrganizationSettings struct { + Settings + OrganizationSettingsAttributes +} +type OrganizationSettingsAttributes struct { + OrganizationScopedUsernames *bool `json:"organizationScopedUsernames,omitempty"` +} + +type OrganizationSettingsRepository interface { + settingsRepository[OrganizationSettings] + organizationSettingsJSONChanges +} + +type notificationSettingsJSONChanges interface { + SetSettingFields(value NotificationSettingsAttributes) database.Change + + SetPasswordChange(value bool) db_json.JsonUpdate +} + +type NotificationSettings struct { + Settings + NotificationSettingsAttributes +} + +type NotificationSettingsAttributes struct { + PasswordChange *bool `json:"passwordChange,omitempty"` +} + +type NotificationSettingsRepository interface { + settingsRepository[NotificationSettings] + notificationSettingsJSONChanges +} + +type legalAndSupportSettingsJSONChanges interface { + SetSettingFields(value LegalAndSupportSettingsAttributes) database.Change + + SetTOSLink(value string) db_json.JsonUpdate + SetPrivacyPolicyLink(value string) db_json.JsonUpdate + SetHelpLink(value string) db_json.JsonUpdate + SetSupportEmail(value string) db_json.JsonUpdate + SetDocsLink(value string) db_json.JsonUpdate + SetCustomLink(value string) db_json.JsonUpdate + SetCustomLinkText(value string) db_json.JsonUpdate +} + +type LegalAndSupportSettings struct { + Settings + LegalAndSupportSettingsAttributes +} + +type LegalAndSupportSettingsAttributes struct { + TOSLink *string `json:"tosLink,omitempty"` + PrivacyPolicyLink *string `json:"privacyPolicyLink,omitempty"` + HelpLink *string `json:"helpLink,omitempty"` + SupportEmail *string `json:"supportEmail,omitempty"` + DocsLink *string `json:"docsLink,omitempty"` + CustomLink *string `json:"customLink,omitempty"` + CustomLinkText *string `json:"customLinkText,omitempty"` +} + +type LegalAndSupportSettingsRepository interface { + settingsRepository[LegalAndSupportSettings] + legalAndSupportSettingsJSONChanges +} diff --git a/backend/v3/domain/settingstate_enumer.go b/backend/v3/domain/settingstate_enumer.go new file mode 100644 index 000000000..e86c0045c --- /dev/null +++ b/backend/v3/domain/settingstate_enumer.go @@ -0,0 +1,109 @@ +// Code generated by "enumer -type SettingState -transform snake -trimprefix SettingState -sql"; DO NOT EDIT. + +package domain + +import ( + "database/sql/driver" + "fmt" + "strings" +) + +const _SettingStateName = "activepreview" + +var _SettingStateIndex = [...]uint8{0, 6, 13} + +const _SettingStateLowerName = "activepreview" + +func (i SettingState) String() string { + if i < 0 || i >= SettingState(len(_SettingStateIndex)-1) { + return fmt.Sprintf("SettingState(%d)", i) + } + return _SettingStateName[_SettingStateIndex[i]:_SettingStateIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _SettingStateNoOp() { + var x [1]struct{} + _ = x[SettingStateActive-(0)] + _ = x[SettingStatePreview-(1)] +} + +var _SettingStateValues = []SettingState{SettingStateActive, SettingStatePreview} + +var _SettingStateNameToValueMap = map[string]SettingState{ + _SettingStateName[0:6]: SettingStateActive, + _SettingStateLowerName[0:6]: SettingStateActive, + _SettingStateName[6:13]: SettingStatePreview, + _SettingStateLowerName[6:13]: SettingStatePreview, +} + +var _SettingStateNames = []string{ + _SettingStateName[0:6], + _SettingStateName[6:13], +} + +// SettingStateString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func SettingStateString(s string) (SettingState, error) { + if val, ok := _SettingStateNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _SettingStateNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to SettingState values", s) +} + +// SettingStateValues returns all values of the enum +func SettingStateValues() []SettingState { + return _SettingStateValues +} + +// SettingStateStrings returns a slice of all String values of the enum +func SettingStateStrings() []string { + strs := make([]string, len(_SettingStateNames)) + copy(strs, _SettingStateNames) + return strs +} + +// IsASettingState returns "true" if the value is listed in the enum definition. "false" otherwise +func (i SettingState) IsASettingState() bool { + for _, v := range _SettingStateValues { + if i == v { + return true + } + } + return false +} + +func (i SettingState) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *SettingState) Scan(value interface{}) error { + if value == nil { + return nil + } + + var str string + switch v := value.(type) { + case []byte: + str = string(v) + case string: + str = v + case fmt.Stringer: + str = v.String() + default: + return fmt.Errorf("invalid value of SettingState: %[1]T(%[1]v)", value) + } + + val, err := SettingStateString(str) + if err != nil { + return err + } + + *i = val + return nil +} diff --git a/backend/v3/domain/settingtype_enumer.go b/backend/v3/domain/settingtype_enumer.go new file mode 100644 index 000000000..cb668be68 --- /dev/null +++ b/backend/v3/domain/settingtype_enumer.go @@ -0,0 +1,141 @@ +// Code generated by "enumer -type SettingType -transform snake -trimprefix SettingType -sql"; DO NOT EDIT. + +package domain + +import ( + "database/sql/driver" + "fmt" + "strings" +) + +const _SettingTypeName = "loginbrandingpassword_complexitypassword_expirydomainlockoutsecurityorganizationnotificationlegal_and_support" + +var _SettingTypeIndex = [...]uint8{0, 5, 13, 32, 47, 53, 60, 68, 80, 92, 109} + +const _SettingTypeLowerName = "loginbrandingpassword_complexitypassword_expirydomainlockoutsecurityorganizationnotificationlegal_and_support" + +func (i SettingType) String() string { + if i >= SettingType(len(_SettingTypeIndex)-1) { + return fmt.Sprintf("SettingType(%d)", i) + } + return _SettingTypeName[_SettingTypeIndex[i]:_SettingTypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _SettingTypeNoOp() { + var x [1]struct{} + _ = x[SettingTypeLogin-(0)] + _ = x[SettingTypeBranding-(1)] + _ = x[SettingTypePasswordComplexity-(2)] + _ = x[SettingTypePasswordExpiry-(3)] + _ = x[SettingTypeDomain-(4)] + _ = x[SettingTypeLockout-(5)] + _ = x[SettingTypeSecurity-(6)] + _ = x[SettingTypeOrganization-(7)] + _ = x[SettingTypeNotification-(8)] + _ = x[SettingTypeLegalAndSupport-(9)] +} + +var _SettingTypeValues = []SettingType{SettingTypeLogin, SettingTypeBranding, SettingTypePasswordComplexity, SettingTypePasswordExpiry, SettingTypeDomain, SettingTypeLockout, SettingTypeSecurity, SettingTypeOrganization, SettingTypeNotification, SettingTypeLegalAndSupport} + +var _SettingTypeNameToValueMap = map[string]SettingType{ + _SettingTypeName[0:5]: SettingTypeLogin, + _SettingTypeLowerName[0:5]: SettingTypeLogin, + _SettingTypeName[5:13]: SettingTypeBranding, + _SettingTypeLowerName[5:13]: SettingTypeBranding, + _SettingTypeName[13:32]: SettingTypePasswordComplexity, + _SettingTypeLowerName[13:32]: SettingTypePasswordComplexity, + _SettingTypeName[32:47]: SettingTypePasswordExpiry, + _SettingTypeLowerName[32:47]: SettingTypePasswordExpiry, + _SettingTypeName[47:53]: SettingTypeDomain, + _SettingTypeLowerName[47:53]: SettingTypeDomain, + _SettingTypeName[53:60]: SettingTypeLockout, + _SettingTypeLowerName[53:60]: SettingTypeLockout, + _SettingTypeName[60:68]: SettingTypeSecurity, + _SettingTypeLowerName[60:68]: SettingTypeSecurity, + _SettingTypeName[68:80]: SettingTypeOrganization, + _SettingTypeLowerName[68:80]: SettingTypeOrganization, + _SettingTypeName[80:92]: SettingTypeNotification, + _SettingTypeLowerName[80:92]: SettingTypeNotification, + _SettingTypeName[92:109]: SettingTypeLegalAndSupport, + _SettingTypeLowerName[92:109]: SettingTypeLegalAndSupport, +} + +var _SettingTypeNames = []string{ + _SettingTypeName[0:5], + _SettingTypeName[5:13], + _SettingTypeName[13:32], + _SettingTypeName[32:47], + _SettingTypeName[47:53], + _SettingTypeName[53:60], + _SettingTypeName[60:68], + _SettingTypeName[68:80], + _SettingTypeName[80:92], + _SettingTypeName[92:109], +} + +// SettingTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func SettingTypeString(s string) (SettingType, error) { + if val, ok := _SettingTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _SettingTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to SettingType values", s) +} + +// SettingTypeValues returns all values of the enum +func SettingTypeValues() []SettingType { + return _SettingTypeValues +} + +// SettingTypeStrings returns a slice of all String values of the enum +func SettingTypeStrings() []string { + strs := make([]string, len(_SettingTypeNames)) + copy(strs, _SettingTypeNames) + return strs +} + +// IsASettingType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i SettingType) IsASettingType() bool { + for _, v := range _SettingTypeValues { + if i == v { + return true + } + } + return false +} + +func (i SettingType) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *SettingType) Scan(value interface{}) error { + if value == nil { + return nil + } + + var str string + switch v := value.(type) { + case []byte: + str = string(v) + case string: + str = v + case fmt.Stringer: + str = v.String() + default: + return fmt.Errorf("invalid value of SettingType: %[1]T(%[1]v)", value) + } + + val, err := SettingTypeString(str) + if err != nil { + return err + } + + *i = val + return nil +} diff --git a/backend/v3/storage/database/change.go b/backend/v3/storage/database/change.go index 30d6dbf5e..849dd90d7 100644 --- a/backend/v3/storage/database/change.go +++ b/backend/v3/storage/database/change.go @@ -12,7 +12,7 @@ import ( type Change interface { gomock.Matcher // Write writes the change to the given statement builder. - Write(builder *StatementBuilder) + Write(builder *StatementBuilder) error // IsOnColumn checks if the change is on the given column. IsOnColumn(col Column) bool } @@ -62,10 +62,11 @@ func NewChangePtr[V Value](col Column, value *V) Change { } // Write implements [Change]. -func (c change[V]) Write(builder *StatementBuilder) { +func (c change[V]) Write(builder *StatementBuilder) error { c.column.WriteUnqualified(builder) builder.WriteString(" = ") builder.WriteArg(c.value) + return nil } // IsOnColumn implements [Change]. @@ -87,13 +88,17 @@ func (c Changes) IsOnColumn(col Column) bool { } // Write implements [Change]. -func (m Changes) Write(builder *StatementBuilder) { +func (m Changes) Write(builder *StatementBuilder) error { for i, change := range m { if i > 0 { builder.WriteString(", ") } - change.Write(builder) + err := change.Write(builder) + if err != nil { + return err + } } + return nil } // Matches implements [gomock.Matcher]. @@ -153,10 +158,11 @@ func (c *changeToColumn) String() string { } // Write implements [Change]. -func (c *changeToColumn) Write(builder *StatementBuilder) { +func (c *changeToColumn) Write(builder *StatementBuilder) error { c.to.WriteUnqualified(builder) builder.WriteString(" = ") c.from.WriteQualified(builder) + return nil } var _ Change = (*changeToColumn)(nil) @@ -191,11 +197,12 @@ func (i *incrementColumnChange) String() string { } // Write implements [Change]. -func (i *incrementColumnChange) Write(builder *StatementBuilder) { +func (i *incrementColumnChange) Write(builder *StatementBuilder) error { i.column.WriteUnqualified(builder) builder.WriteString(" = ") i.column.WriteUnqualified(builder) builder.WriteString(" + 1") + return nil } var _ Change = (*incrementColumnChange)(nil) @@ -242,7 +249,7 @@ func (c *changeToStatement) String() string { } // Write implements [Change]. -func (c *changeToStatement) Write(builder *StatementBuilder) { +func (c *changeToStatement) Write(builder *StatementBuilder) error { _, ok := c.column.(Columns) if ok { builder.WriteRune('(') @@ -254,6 +261,7 @@ func (c *changeToStatement) Write(builder *StatementBuilder) { builder.WriteString(" = (") c.stmt(builder) builder.WriteString(")") + return nil } var _ Change = (*changeToStatement)(nil) @@ -322,11 +330,12 @@ func (c *cteChange) String() string { } // Write implements [CTEChange]. -func (c *cteChange) Write(builder *StatementBuilder) { +func (c *cteChange) Write(builder *StatementBuilder) error { if c.change == nil { - return + return nil } c.change(c.name).Write(builder) + return nil } // WriteCTE implements [CTEChange]. diff --git a/backend/v3/storage/database/change_test.go b/backend/v3/storage/database/change_test.go index eea71e036..beaf2aef9 100644 --- a/backend/v3/storage/database/change_test.go +++ b/backend/v3/storage/database/change_test.go @@ -64,7 +64,8 @@ func TestChangeWrite(t *testing.T) { } { t.Run(test.name, func(t *testing.T) { var builder StatementBuilder - test.change.Write(&builder) + err := test.change.Write(&builder) + require.NoError(t, err) assert.Equal(t, test.want.stmt, builder.String()) require.Len(t, builder.Args(), len(test.want.args)) for i, arg := range test.want.args { diff --git a/backend/v3/storage/database/condition.go b/backend/v3/storage/database/condition.go index 877c10601..b000018d0 100644 --- a/backend/v3/storage/database/condition.go +++ b/backend/v3/storage/database/condition.go @@ -157,9 +157,8 @@ func IsNull(column Column) *isNull { } // IsRestrictingColumn implements [Condition]. -// It returns false because it cannot be used for restricting a column. func (i isNull) IsRestrictingColumn(col Column) bool { - return false + return true } var _ Condition = (*isNull)(nil) @@ -194,9 +193,8 @@ func IsNotNull(column Column) *isNotNull { } // IsRestrictingColumn implements [Condition]. -// It returns false because it cannot be used for restricting a column. func (i isNotNull) IsRestrictingColumn(col Column) bool { - return false + return true } var _ Condition = (*isNotNull)(nil) diff --git a/backend/v3/storage/database/dialect/postgres/error.go b/backend/v3/storage/database/dialect/postgres/error.go index 9710c1e53..433db5adc 100644 --- a/backend/v3/storage/database/dialect/postgres/error.go +++ b/backend/v3/storage/database/dialect/postgres/error.go @@ -51,6 +51,9 @@ func wrapPgError(err *pgconn.PgError) error { // 23502: not_null_violation - A value violates a NOT NULL constraint. case "23502": return database.NewNotNullError(err.TableName, err.ConstraintName, err) + // 22P02: integrity violation - A value is being inserted into a enum that is not valid + case "22P02": + return database.NewCheckError(err.TableName, err.ConstraintName, err) default: return database.NewUnknownError(err) } diff --git a/backend/v3/storage/database/dialect/postgres/migration/009_settings_table.go b/backend/v3/storage/database/dialect/postgres/migration/009_settings_table.go new file mode 100644 index 000000000..653ebd2f2 --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/009_settings_table.go @@ -0,0 +1,16 @@ +package migration + +import ( + _ "embed" +) + +var ( + //go:embed 009_settings_table/up.sql + up009SettingsTable string + //go:embed 009_settings_table/down.sql + down009SettingsTable string +) + +func init() { + registerSQLMigration(9, up009SettingsTable, down009SettingsTable) +} diff --git a/backend/v3/storage/database/dialect/postgres/migration/009_settings_table/down.sql b/backend/v3/storage/database/dialect/postgres/migration/009_settings_table/down.sql new file mode 100644 index 000000000..cd15acc90 --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/009_settings_table/down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS zitadel.settings; +DROP TYPE IF EXISTS zitadel.settings_type; +DROP TYPE IF EXISTS zitadel.settings_state; diff --git a/backend/v3/storage/database/dialect/postgres/migration/009_settings_table/up.sql b/backend/v3/storage/database/dialect/postgres/migration/009_settings_table/up.sql new file mode 100644 index 000000000..17b16fd92 --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/009_settings_table/up.sql @@ -0,0 +1,95 @@ +CREATE TYPE zitadel.settings_type AS ENUM ( + 'login', + 'branding', + 'password_complexity', + 'password_expiry', + 'domain', + 'lockout', + 'security', + 'organization', + 'notification', + 'legal_and_support' +); + +CREATE TYPE zitadel.settings_state AS ENUM ( + 'active', + 'preview' +); + +CREATE TABLE zitadel.settings ( + instance_id TEXT NOT NULL + , organization_id TEXT + , id TEXT NOT NULL DEFAULT gen_random_uuid() + , type zitadel.settings_type NOT NULL + , state zitadel.settings_state NOT NULL + , settings JSONB -- the storage does not really care about what is configured so we store it as json + + , created_at TIMESTAMPTZ NOT NULL DEFAULT now() + , updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + + , PRIMARY KEY (instance_id, id, type, state) + , FOREIGN KEY (instance_id) REFERENCES zitadel.instances(id) ON DELETE CASCADE + , FOREIGN KEY (instance_id, organization_id) REFERENCES zitadel.organizations(instance_id, id) ON DELETE CASCADE + + , UNIQUE (instance_id, organization_id, type, state) +); + +CREATE UNIQUE INDEX idx_settings_unique_type ON zitadel.settings (instance_id, organization_id, type, state) NULLS NOT DISTINCT; + +CREATE TRIGGER trigger_set_updated_at +BEFORE UPDATE ON zitadel.settings +FOR EACH ROW +WHEN (NEW.updated_at IS NULL) +EXECUTE FUNCTION zitadel.set_updated_at(); + +CREATE OR REPLACE FUNCTION zitadel.jsonb_array_remove( + source JSONB + , path TEXT[] + , value anyelement + + , outt out jsonb +) + language 'plpgsql' +AS $$ + BEGIN + outt = jsonb_set(source, path, + (CASE WHEN (SELECT source ?& path) then + coalesce( + (SELECT jsonb_agg(v) + FROM jsonb_array_elements(source #>path) AS elem(v) + WHERE v::text <> value::text), + jsonb_build_array()) + ELSe + jsonb_build_array() + END)::jsonb, TRUE); + END; +$$; + +CREATE OR REPLACE FUNCTION zitadel.jsonb_array_append( + source jsonb + , path text[] + , value anyelement + + , outt out jsonb +) + language 'plpgsql' +AS $$ + BEGIN + outt := jsonb_insert( + source, + (CASE WHEN (SELECT source ?& path) then + array_append(path, '-1') + ELSE + path + END)::text[], + (CASE WHEN (select source ?& path) then + value::TEXT::jsonb + ELSE + jsonb_build_array(value) + END), + TRUE + ); + + END; +$$; + diff --git a/backend/v3/storage/database/events_testing/font.otf b/backend/v3/storage/database/events_testing/font.otf new file mode 100644 index 000000000..9651a3f8a Binary files /dev/null and b/backend/v3/storage/database/events_testing/font.otf differ diff --git a/backend/v3/storage/database/events_testing/picture.png b/backend/v3/storage/database/events_testing/picture.png new file mode 100644 index 000000000..0d67cacb4 --- /dev/null +++ b/backend/v3/storage/database/events_testing/picture.png @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/v3/storage/database/events_testing/settings_instance_test.go b/backend/v3/storage/database/events_testing/settings_instance_test.go new file mode 100644 index 000000000..87a9ce595 --- /dev/null +++ b/backend/v3/storage/database/events_testing/settings_instance_test.go @@ -0,0 +1,1389 @@ +//go:build integration + +package events_test + +import ( + "bytes" + "context" + _ "embed" + "io" + "mime/multipart" + "net/http" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + http_util "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/admin" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/policy" +) + +//go:embed picture.png +var picture []byte + +//go:embed font.otf +var font []byte + +func uploadInstanceAsset(ctx context.Context, t *testing.T, instance *integration.Instance, path string, asset io.Reader) { + url := http_util.BuildOrigin(instance.Host(), instance.Config.Secure) + "/assets/v1" + path + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", "filename") + require.NoError(t, err) + + _, err = io.Copy(part, asset) + require.NoError(t, err) + + err = writer.Close() + require.NoError(t, err) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) + require.NoError(t, err) + md, ok := metadata.FromOutgoingContext(ctx) + require.True(t, ok) + // context information has to be HTTP headers, as the asset API is only HTTP + req.Header.Set("authorization", md.Get("authorization")[0]) + req.Header.Set("Content-Type", writer.FormDataContentType()) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestServer_TestInstanceLoginSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.LoginSettingsRepository() + before := time.Now() + newInstance := integration.NewInstance(t.Context()) + after := time.Now() + + IAMCTX := newInstance.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner) + + t.Run("test adding login settings reduces", func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.login.added + // these values are found in default.yaml + assert.Equal(collect, true, *setting.AllowRegister) + assert.Equal(collect, true, *setting.AllowExternalIDP) + assert.Equal(collect, domain.PasswordlessTypeAllowed, *setting.PasswordlessType) + assert.Equal(collect, true, *setting.AllowDomainDiscovery) + assert.Equal(collect, true, *setting.AllowUserNamePassword) + assert.Equal(collect, time.Duration(time.Hour*240), *setting.PasswordCheckLifetime) + assert.Equal(collect, time.Duration(time.Hour*12), *setting.MultiFactorCheckLifetime) + assert.Equal(collect, time.Duration(time.Hour*18), *setting.SecondFactorCheckLifetime) + assert.Equal(collect, time.Duration(time.Hour*240), *setting.ExternalLoginCheckLifetime) + assert.WithinRange(collect, setting.CreatedAt, before, after) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test login settings change reduces", func(t *testing.T) { + _, err := newInstance.Client.Admin.UpdateLoginPolicy(IAMCTX, &admin.UpdateLoginPolicyRequest{ + AllowUsernamePassword: false, + AllowRegister: false, + AllowExternalIdp: true, + ForceMfa: true, + PasswordlessType: policy.PasswordlessType_PASSWORDLESS_TYPE_NOT_ALLOWED, + HidePasswordReset: true, + IgnoreUnknownUsernames: true, + DefaultRedirectUri: "http://www.example.com", + PasswordCheckLifetime: durationpb.New(time.Second * 20 * 20), + ExternalLoginCheckLifetime: durationpb.New(time.Second * 20 * 21), + MfaInitSkipLifetime: durationpb.New(time.Second * 20 * 22), + SecondFactorCheckLifetime: durationpb.New(time.Second * 20 * 23), + MultiFactorCheckLifetime: durationpb.New(time.Second * 20 * 24), + AllowDomainDiscovery: false, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, + ForceMfaLocalOnly: true, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + require.NotNil(collect, setting.ForceMFA) + + // event instance.policy.login.changed + assert.Equal(collect, false, *setting.AllowRegister) + assert.Equal(collect, true, *setting.AllowExternalIDP) + assert.Equal(collect, true, *setting.ForceMFA) + assert.Equal(collect, domain.PasswordlessTypeNotAllowed, *setting.PasswordlessType) + assert.Equal(collect, true, *setting.HidePasswordReset) + assert.Equal(collect, true, *setting.IgnoreUnknownUsernames) + assert.Equal(collect, "http://www.example.com", *setting.DefaultRedirectURI) + assert.Equal(collect, false, *setting.AllowDomainDiscovery) + assert.Equal(collect, false, *setting.AllowUserNamePassword) + assert.Equal(collect, time.Duration(time.Second*20*20), *setting.PasswordCheckLifetime) + assert.Equal(collect, time.Duration(time.Second*20*21), *setting.ExternalLoginCheckLifetime) + assert.Equal(collect, time.Duration(time.Second*20*22), *setting.MFAInitSkipLifetime) + assert.Equal(collect, time.Duration(time.Second*20*23), *setting.SecondFactorCheckLifetime) + assert.Equal(collect, time.Duration(time.Second*20*24), *setting.MultiFactorCheckLifetime) + assert.Equal(collect, true, *setting.DisableLoginWithEmail) + assert.Equal(collect, true, *setting.DisableLoginWithPhone) + assert.Equal(collect, true, *setting.ForceMFALocalOnly) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test added/remove login multifactor type reduces", func(t *testing.T) { + // check inital MFAType value + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + assert.Equal(collect, []domain.MultiFactorType{domain.MultiFactorType(policy.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION)}, setting.MFAType) + }, retryDuration, tick) + + // remove MFAType + _, err := newInstance.Client.Admin.RemoveMultiFactorFromLoginPolicy(IAMCTX, &admin.RemoveMultiFactorFromLoginPolicyRequest{ + Type: policy.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION, + }) + require.NoError(t, err) + + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.login.multifactor.remove + assert.Equal(collect, []domain.MultiFactorType{}, setting.MFAType) + }, retryDuration, tick) + + before := time.Now() + _, err = newInstance.Client.Admin.AddMultiFactorToLoginPolicy(IAMCTX, &admin.AddMultiFactorToLoginPolicyRequest{ + Type: policy.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION, + }) + require.NoError(t, err) + after := time.Now() + + // add MFAType + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.login.multifactor.added + assert.Equal(collect, []domain.MultiFactorType{domain.MultiFactorType(policy.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION)}, setting.MFAType) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test added/removed second multifactor reduces", func(t *testing.T) { + before := time.Now() + // get current second factor types + var secondFactorTypes []domain.SecondFactorType + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + secondFactorTypes = setting.SecondFactorTypes + }, retryDuration, tick) + + // add new second factor type + before = time.Now() + _, err := newInstance.Client.Admin.AddSecondFactorToLoginPolicy(IAMCTX, &admin.AddSecondFactorToLoginPolicyRequest{ + Type: policy.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS, + }) + require.NoError(t, err) + after := time.Now() + + secondFactorTypes = append(secondFactorTypes, domain.SecondFactorType(policy.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS)) + + // check new second factor type is added + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.login.multifactor.secondfactor.added + assert.Equal(collect, secondFactorTypes, setting.SecondFactorTypes) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + + // remove second factor type + before = time.Now() + _, err = newInstance.Client.Admin.RemoveSecondFactorFromLoginPolicy(IAMCTX, &admin.RemoveSecondFactorFromLoginPolicyRequest{ + Type: policy.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS, + }) + require.NoError(t, err) + after = time.Now() + + secondFactorTypes = secondFactorTypes[0 : len(secondFactorTypes)-1] + + // check new second factor type is removed + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.login.multifactor.secondfactor.removed + assert.Equal(collect, secondFactorTypes, setting.SecondFactorTypes) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test delete instance reduces", func(t *testing.T) { + // check login settings exist + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + require.NotNil(collect, setting) + }, retryDuration, tick) + + // delete instance + _, err := newInstance.Client.InstanceV2Beta.DeleteInstance(CTX, &instance.DeleteInstanceRequest{ + InstanceId: newInstance.ID(), + }) + require.NoError(t, err) + + // check login settings removed + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + + // event instance.removed + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestInstanceLabelSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.BrandingSettingsRepository() + + before := time.Now() + newInstance := integration.NewInstance(t.Context()) + after := time.Now() + + IAMCTX := newInstance.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner) + + t.Run("test adding label settings reduces", func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.added + // these values are found in default.yaml + assert.Equal(collect, "#5469d4", *setting.PrimaryColorLight) + assert.Equal(collect, "#fafafa", *setting.BackgroundColorLight) + assert.Equal(collect, "#cd3d56", *setting.WarnColorLight) + assert.Equal(collect, "#000000", *setting.FontColorLight) + assert.Equal(collect, "#2073c4", *setting.PrimaryColorDark) + assert.Equal(collect, "#111827", *setting.BackgroundColorDark) + assert.Equal(collect, "#ff3b5b", *setting.WarnColorDark) + assert.Equal(collect, "#ff3b5b", *setting.WarnColorDark) + assert.Equal(collect, "#ffffff", *setting.FontColorDark) + assert.Equal(collect, false, *setting.HideLoginNameSuffix) + assert.Equal(collect, false, *setting.ErrorMsgPopup) + assert.Equal(collect, false, *setting.DisableWatermark) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.CreatedAt, before, after) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label change", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Admin.UpdateLabelPolicy(IAMCTX, &admin.UpdateLabelPolicyRequest{ + PrimaryColor: "#055000", + HideLoginNameSuffix: true, + WarnColor: "#055000", + BackgroundColor: "#055000", + FontColor: "#055000", + PrimaryColorDark: "#055000", + BackgroundColorDark: "#055000", + WarnColorDark: "#055000", + FontColorDark: "#055000", + DisableWatermark: true, + ThemeMode: policy.ThemeMode_THEME_MODE_LIGHT, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.change + assert.Equal(collect, "#055000", *setting.PrimaryColorLight) + assert.Equal(collect, "#055000", *setting.BackgroundColorLight) + assert.Equal(collect, "#055000", *setting.WarnColorLight) + assert.Equal(collect, "#055000", *setting.FontColorLight) + assert.Equal(collect, "#055000", *setting.PrimaryColorDark) + assert.Equal(collect, "#055000", *setting.BackgroundColorDark) + assert.Equal(collect, "#055000", *setting.WarnColorDark) + assert.Equal(collect, "#055000", *setting.WarnColorDark) + assert.Equal(collect, "#055000", *setting.FontColorDark) + assert.Equal(collect, true, *setting.HideLoginNameSuffix) + assert.Equal(collect, false, *setting.ErrorMsgPopup) + assert.Equal(collect, true, *setting.DisableWatermark) + assert.Equal(collect, domain.BrandingPolicyThemeLight, *setting.ThemeMode) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test label settings activated", func(t *testing.T) { + // activate label + before := time.Now() + _, err := newInstance.Client.Admin.ActivateLabelPolicy(IAMCTX, &admin.ActivateLabelPolicyRequest{}) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.activated + assert.Equal(collect, domain.SettingStateActive, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label logo light added", func(t *testing.T) { + // set logo light + before := time.Now() + uploadInstanceAsset(IAMCTX, t, newInstance, "/instance/policy/label/logo", bytes.NewReader(picture)) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.logo.added + assert.NotNil(collect, setting.LogoURLLight) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label logo light removed", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Admin.RemoveLabelPolicyLogo(IAMCTX, &admin.RemoveLabelPolicyLogoRequest{}) + require.NoError(t, err) + after := time.Now() + + // check light logo removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.logo.removed + assert.Equal(collect, url.URL{}, *setting.LogoURLLight) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label logo dark added", func(t *testing.T) { + before := time.Now() + uploadInstanceAsset(IAMCTX, t, newInstance, "/instance/policy/label/logo/dark", bytes.NewReader(picture)) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.logo.dark.added + assert.NotNil(collect, setting.LogoURLDark) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label logo dark removed", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Admin.RemoveLabelPolicyLogoDark(IAMCTX, &admin.RemoveLabelPolicyLogoDarkRequest{}) + require.NoError(t, err) + after := time.Now() + + // check dark logo removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.logo.dark.removed + assert.Equal(collect, url.URL{}, *setting.LogoURLDark) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label icon light added", func(t *testing.T) { + before := time.Now() + uploadInstanceAsset(IAMCTX, t, newInstance, "/instance/policy/label/icon", bytes.NewReader(picture)) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.icon.added + assert.NotNil(collect, setting.IconURLLight) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label icon dark added", func(t *testing.T) { + before := time.Now() + uploadInstanceAsset(IAMCTX, t, newInstance, "/instance/policy/label/icon/dark", bytes.NewReader(picture)) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.icon.dark.added + assert.NotNil(collect, setting.IconURLDark) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label icon light removed", func(t *testing.T) { + // remote logo light + before := time.Now() + _, err := newInstance.Client.Admin.RemoveLabelPolicyIcon(IAMCTX, &admin.RemoveLabelPolicyIconRequest{}) + require.NoError(t, err) + after := time.Now() + + // check light icon removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.icon.removed + assert.Equal(collect, url.URL{}, *setting.IconURLLight) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label icon dark removed", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Admin.RemoveLabelPolicyIconDark(IAMCTX, &admin.RemoveLabelPolicyIconDarkRequest{}) + require.NoError(t, err) + after := time.Now() + + // check dark icon removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.icon.dark.removed + assert.Equal(collect, url.URL{}, *setting.IconURLDark) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy font added", func(t *testing.T) { + before := time.Now() + uploadInstanceAsset(IAMCTX, t, newInstance, "/instance/policy/label/font", bytes.NewReader(font)) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.font.added + assert.NotNil(collect, setting.FontURL) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy font removed", func(t *testing.T) { + // remote logo light + before := time.Now() + _, err := newInstance.Client.Admin.RemoveLabelPolicyFont(IAMCTX, &admin.RemoveLabelPolicyFontRequest{}) + require.NoError(t, err) + after := time.Now() + + // check font policy removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(collect, err) + + // event instance.policy.label.font.removed + assert.Equal(collect, url.URL{}, *setting.FontURL) + assert.Equal(collect, domain.SettingStatePreview, setting.State) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test delete instance reduces", func(t *testing.T) { + // check label preview settings exist + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + require.NotNil(collect, setting) + }, retryDuration, tick) + + // check label activated settings exist + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*50) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + require.NotNil(collect, setting) + }, retryDuration, tick) + + // delete instance + _, err := newInstance.Client.InstanceV2Beta.DeleteInstance(CTX, &instance.DeleteInstanceRequest{ + InstanceId: newInstance.ID(), + }) + + // check label preview settings deleted + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStateActive), + ), + ) + + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + + // check label activated settings deleted + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeBranding, domain.SettingStateActive), + ), + ) + + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + require.NoError(t, err) + }) +} + +func TestServer_TestInstancePasswordComplexitySettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.PasswordComplexitySettingsRepository() + + before := time.Now() + newInstance := integration.NewInstance(t.Context()) + after := time.Now() + + IAMCTX := newInstance.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner) + t.Run("test password complexity added", func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.password.complexity.added + // these values are found in default.yaml + assert.Equal(collect, uint64(8), *setting.MinLength) + assert.Equal(collect, true, *setting.HasUppercase) + assert.Equal(collect, true, *setting.HasLowercase) + assert.Equal(collect, true, *setting.HasNumber) + assert.Equal(collect, true, *setting.HasSymbol) + assert.WithinRange(collect, setting.CreatedAt, before, after) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test password complexity change", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Admin.UpdatePasswordComplexityPolicy(IAMCTX, &admin.UpdatePasswordComplexityPolicyRequest{ + MinLength: 5, + HasUppercase: false, + HasLowercase: false, + HasNumber: false, + HasSymbol: false, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.password.complexity.changed + assert.Equal(collect, uint64(5), *setting.MinLength) + assert.Equal(collect, false, *setting.HasUppercase) + assert.Equal(collect, false, *setting.HasLowercase) + assert.Equal(collect, false, *setting.HasNumber) + assert.Equal(collect, false, *setting.HasSymbol) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test delete instance reduces", func(t *testing.T) { + // check login settings exist + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + require.NotNil(t, setting) + }, retryDuration, tick) + + // delete instance + _, err := newInstance.Client.InstanceV2Beta.DeleteInstance(CTX, &instance.DeleteInstanceRequest{ + InstanceId: newInstance.ID(), + }) + require.NoError(t, err) + + // check password complexity settings removed + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + ), + ) + + // event instance.removed + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestInstancePasswordPolicySettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.PasswordExpirySettingsRepository() + before := time.Now() + newInstance := integration.NewInstance(t.Context()) + after := time.Now() + + IAMCTX := newInstance.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner) + + t.Run("test password policy added", func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.password.age.added + assert.Equal(collect, uint64(0), *setting.ExpireWarnDays) + assert.Equal(collect, uint64(0), *setting.MaxAgeDays) + assert.WithinRange(collect, setting.CreatedAt, before, after) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test password policy changed", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Admin.UpdatePasswordAgePolicy(IAMCTX, &admin.UpdatePasswordAgePolicyRequest{ + MaxAgeDays: 30, + ExpireWarnDays: 30, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.password.age.changed + assert.Equal(collect, uint64(30), *setting.ExpireWarnDays) + assert.Equal(collect, uint64(30), *setting.MaxAgeDays) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test delete instance reduces", func(t *testing.T) { + // check login settings exist + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + require.NotNil(t, setting) + }, retryDuration, tick) + + // delete instance + _, err := newInstance.Client.InstanceV2Beta.DeleteInstance(CTX, &instance.DeleteInstanceRequest{ + InstanceId: newInstance.ID(), + }) + require.NoError(t, err) + + // check password expiry settings removed + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + ), + ) + + // event instance.removed + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestInstanceDomainSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.DomainSettingsRepository() + before := time.Now() + newInstance := integration.NewInstance(t.Context()) + after := time.Now() + + IAMCTX := newInstance.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner) + + t.Run("test domain policy added", func(t *testing.T) { + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeDomain, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.domain.added + assert.Equal(collect, false, *setting.SMTPSenderAddressMatchesInstanceDomain) + assert.Equal(collect, false, *setting.LoginNameIncludesDomain) + assert.Equal(collect, false, *setting.RequireOrgDomainVerification) + assert.WithinRange(collect, setting.CreatedAt, before, after) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test domain policy changed", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Admin.UpdateDomainPolicy(IAMCTX, &admin.UpdateDomainPolicyRequest{ + UserLoginMustBeDomain: true, + ValidateOrgDomains: true, + SmtpSenderAddressMatchesInstanceDomain: true, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeDomain, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.changed + assert.Equal(collect, true, *setting.SMTPSenderAddressMatchesInstanceDomain) + assert.Equal(collect, true, *setting.LoginNameIncludesDomain) + assert.Equal(collect, true, *setting.RequireOrgDomainVerification) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test delete instance reduces", func(t *testing.T) { + // check login settings exist + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeDomain, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + require.NotNil(t, setting) + }, retryDuration, tick) + + // delete instance + _, err := newInstance.Client.InstanceV2Beta.DeleteInstance(CTX, &instance.DeleteInstanceRequest{ + InstanceId: newInstance.ID(), + }) + require.NoError(t, err) + + // check domain settings removed + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeDomain, domain.SettingStateActive), + ), + ) + + // event instance.removed + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestInstanceLockoutSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.LockoutSettingsRepository() + + before := time.Now() + newInstance := integration.NewInstance(t.Context()) + after := time.Now() + + IAMCTX := newInstance.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner) + + t.Run("test lockout policy added", func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLockout, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.lockout.added + assert.Equal(collect, uint64(0), *setting.MaxOTPAttempts) + assert.Equal(collect, uint64(0), *setting.MaxPasswordAttempts) + assert.Equal(collect, true, *setting.ShowLockOutFailures) + assert.WithinRange(collect, setting.CreatedAt, before, after) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test password policy changed", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Admin.UpdateLockoutPolicy(IAMCTX, &admin.UpdateLockoutPolicyRequest{ + MaxPasswordAttempts: 5, + MaxOtpAttempts: 5, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLockout, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.lockout.changed + assert.Equal(collect, uint64(5), *setting.MaxOTPAttempts) + assert.Equal(collect, uint64(5), *setting.MaxPasswordAttempts) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test delete instance reduces", func(t *testing.T) { + // check login settings exist + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLockout, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + require.NotNil(t, setting) + }, retryDuration, tick) + + // delete instance + _, err := newInstance.Client.InstanceV2Beta.DeleteInstance(CTX, &instance.DeleteInstanceRequest{ + InstanceId: newInstance.ID(), + }) + require.NoError(t, err) + + // check password complexity settings removed + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLockout, domain.SettingStateActive), + ), + ) + + // event instance.removed + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestInstanceSecuritySettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.SecuritySettingsRepository() + + newInstance := integration.NewInstance(t.Context()) + IAMCTX := newInstance.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner) + + t.Run("test security policy set", func(t *testing.T) { + // 1. set security policy + before := time.Now() + _, err := newInstance.Client.Admin.SetSecurityPolicy(IAMCTX, &admin.SetSecurityPolicyRequest{ + EnableIframeEmbedding: true, + AllowedOrigins: []string{"value"}, + EnableImpersonation: true, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeSecurity, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.security.set + assert.Equal(collect, true, *setting.EnableIframeEmbedding) + assert.Equal(collect, []string{"value"}, setting.AllowedOrigins) + assert.Equal(collect, true, *setting.EnableImpersonation) + assert.WithinRange(collect, setting.CreatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test security policy re-set", func(t *testing.T) { + // 2. re-set security policy + before := time.Now() + _, err := newInstance.Client.Admin.SetSecurityPolicy(IAMCTX, &admin.SetSecurityPolicyRequest{ + EnableIframeEmbedding: false, + AllowedOrigins: []string{"new_value"}, + EnableImpersonation: false, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeSecurity, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + // event instance.policy.security.set + assert.Equal(collect, false, *setting.EnableIframeEmbedding) + assert.Equal(collect, []string{"new_value"}, setting.AllowedOrigins) + assert.Equal(collect, false, *setting.EnableImpersonation) + assert.WithinRange(collect, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test delete instance reduces", func(t *testing.T) { + // 3. delete instance + _, err := newInstance.Client.InstanceV2Beta.DeleteInstance(CTX, &instance.DeleteInstanceRequest{ + InstanceId: newInstance.ID(), + }) + require.NoError(t, err) + + // 4. check security settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*10) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeSecurity, domain.SettingStateActive), + ), + ) + + // event instance.removed + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestInstanceNotificationSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.NotificationSettingsRepository() + + before := time.Now() + newInstance := integration.NewInstance(t.Context()) + after := time.Now() + IAMCTX := newInstance.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner) + + t.Run("test add notification settings set", func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeNotification, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + assert.Equal(t, true, *setting.PasswordChange) + assert.WithinRange(t, setting.CreatedAt, before, after) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test add notification settings re-set", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Admin.UpdateNotificationPolicy(IAMCTX, &admin.UpdateNotificationPolicyRequest{ + PasswordChange: false, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeNotification, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + assert.Equal(t, false, *setting.PasswordChange) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test delete instance reduces", func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeNotification, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + require.NotNil(t, setting) + }, retryDuration, tick) + + _, err := newInstance.Client.InstanceV2Beta.DeleteInstance(CTX, &instance.DeleteInstanceRequest{ + InstanceId: newInstance.ID(), + }) + require.NoError(t, err) + + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeNotification, domain.SettingStateActive), + ), + ) + + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestInstanceLegalAndSupportSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.LegalAndSupportSettingsRepository() + + before := time.Now() + newInstance := integration.NewInstance(t.Context()) + after := time.Now() + IAMCTX := newInstance.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner) + + t.Run("test add legal and support settings set", func(t *testing.T) { + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + assert.Equal(t, "", *setting.TOSLink) + assert.Equal(t, "", *setting.PrivacyPolicyLink) + assert.Equal(t, "", *setting.HelpLink) + assert.Equal(t, "", *setting.SupportEmail) + assert.Equal(t, "https://zitadel.com/docs", *setting.DocsLink) + assert.Equal(t, "", *setting.CustomLink) + assert.Equal(t, "", *setting.CustomLinkText) + assert.WithinRange(t, setting.CreatedAt, before, after) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test add legal and support settings re-set", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Admin.UpdatePrivacyPolicy(IAMCTX, &admin.UpdatePrivacyPolicyRequest{ + TosLink: "https://tos.example2.com", + PrivacyLink: "https://privacy.example2.com", + HelpLink: "https://help.example2.com", + SupportEmail: "support@example2.com", + DocsLink: "https://docs.example2.com", + CustomLink: "https://custom.example2.com", + CustomLinkText: "Custom link text2", + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + assert.Equal(t, "https://tos.example2.com", *setting.TOSLink) + assert.Equal(t, "https://privacy.example2.com", *setting.PrivacyPolicyLink) + assert.Equal(t, "https://help.example2.com", *setting.HelpLink) + assert.Equal(t, "support@example2.com", *setting.SupportEmail) + assert.Equal(t, "https://docs.example2.com", *setting.DocsLink) + assert.Equal(t, "https://custom.example2.com", *setting.CustomLink) + assert.Equal(t, "Custom link text2", *setting.CustomLinkText) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test delete instance reduces", func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + ), + ) + require.NoError(collect, err) + + require.NotNil(t, setting) + }, retryDuration, tick) + + _, err := newInstance.Client.InstanceV2Beta.DeleteInstance(CTX, &instance.DeleteInstanceRequest{ + InstanceId: newInstance.ID(), + }) + require.NoError(t, err) + + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), nil, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + ), + ) + + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} diff --git a/backend/v3/storage/database/events_testing/settings_org_test.go b/backend/v3/storage/database/events_testing/settings_org_test.go new file mode 100644 index 000000000..f99c02c04 --- /dev/null +++ b/backend/v3/storage/database/events_testing/settings_org_test.go @@ -0,0 +1,1550 @@ +//go:build integration + +package events_test + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http" + "net/url" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + http_util "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/admin" + "github.com/zitadel/zitadel/pkg/grpc/management" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/policy" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/system" +) + +func createInstanceWithOrg(t *testing.T) (context.Context, *integration.Instance, string) { + newInstance := integration.NewInstance(t.Context()) + + IAMCTX := newInstance.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner) + + organization, err := newInstance.Client.OrgV2beta.CreateOrganization(IAMCTX, &v2beta_org.CreateOrganizationRequest{ + Name: integration.OrganizationName(), + }) + require.NoError(t, err) + orgId := organization.GetId() + IAMCTX = integration.SetOrgID(IAMCTX, orgId) + + cleanupInstance(t, newInstance) + return IAMCTX, newInstance, orgId +} + +func cleanupInstance(t *testing.T, instance *integration.Instance) { + t.Cleanup(func() { + _, err := SystemClient.RemoveInstance(CTX, &system.RemoveInstanceRequest{ + InstanceId: instance.ID(), + }) + if err != nil { + t.Logf("Failed to delete instance on cleanup: %v", err) + } + }) +} + +func uploadOrganizationAsset(ctx context.Context, t *testing.T, instance *integration.Instance, path string, asset io.Reader) { + url := http_util.BuildOrigin(instance.Host(), instance.Config.Secure) + "/assets/v1" + path + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", "filename") + require.NoError(t, err) + + _, err = io.Copy(part, asset) + require.NoError(t, err) + + err = writer.Close() + require.NoError(t, err) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) + require.NoError(t, err) + md, ok := metadata.FromOutgoingContext(ctx) + require.True(t, ok) + // context information has to be HTTP headers, as the asset API is only HTTP + req.Header.Set("x-zitadel-orgid", md.Get("x-zitadel-orgid")[0]) + req.Header.Set("authorization", md.Get("authorization")[0]) + req.Header.Set("Content-Type", writer.FormDataContentType()) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestServer_TestOrgLoginSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.LoginSettingsRepository() + IAMCTX, newInstance, orgId := createInstanceWithOrg(t) + + t.Run("test adding login settings reduces", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Mgmt.AddCustomLoginPolicy(IAMCTX, &management.AddCustomLoginPolicyRequest{ + AllowUsernamePassword: false, + AllowRegister: false, + AllowExternalIdp: false, + ForceMfa: false, + PasswordlessType: policy.PasswordlessType_PASSWORDLESS_TYPE_NOT_ALLOWED, + HidePasswordReset: false, + IgnoreUnknownUsernames: false, + DefaultRedirectUri: "http://www.example.com", + PasswordCheckLifetime: durationpb.New(time.Second * 10), + ExternalLoginCheckLifetime: durationpb.New(time.Second * 10), + MfaInitSkipLifetime: durationpb.New(time.Second * 10), + SecondFactorCheckLifetime: durationpb.New(time.Second * 10), + MultiFactorCheckLifetime: durationpb.New(time.Second * 10), + AllowDomainDiscovery: false, + DisableLoginWithEmail: false, + DisableLoginWithPhone: false, + ForceMfaLocalOnly: false, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.login.added + // these values are found in default.yaml + assert.Equal(t, false, *setting.AllowRegister) + assert.Equal(t, false, *setting.AllowExternalIDP) + assert.Equal(t, domain.PasswordlessTypeNotAllowed, *setting.PasswordlessType) + assert.Equal(t, false, *setting.AllowDomainDiscovery) + assert.Equal(t, false, *setting.AllowUserNamePassword) + assert.Equal(t, time.Duration(time.Second*10), *setting.PasswordCheckLifetime) + assert.Equal(t, time.Duration(time.Second*10), *setting.ExternalLoginCheckLifetime) + assert.Equal(t, time.Duration(time.Second*10), *setting.MFAInitSkipLifetime) + assert.Equal(t, time.Duration(time.Second*10), *setting.SecondFactorCheckLifetime) + assert.Equal(t, time.Duration(time.Second*10), *setting.MultiFactorCheckLifetime) + assert.WithinRange(t, setting.CreatedAt, before, after) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test change login settings reduces", func(t *testing.T) { + // update org policy + before := time.Now() + _, err := newInstance.Client.Mgmt.UpdateCustomLoginPolicy(IAMCTX, &management.UpdateCustomLoginPolicyRequest{ + AllowUsernamePassword: true, + AllowRegister: true, + AllowExternalIdp: true, + ForceMfa: true, + PasswordlessType: policy.PasswordlessType_PASSWORDLESS_TYPE_ALLOWED, + HidePasswordReset: true, + IgnoreUnknownUsernames: true, + DefaultRedirectUri: "http://www.new_example.com", + PasswordCheckLifetime: durationpb.New(time.Second * 5 * 20), + ExternalLoginCheckLifetime: durationpb.New(time.Second * 5 * 21), + MfaInitSkipLifetime: durationpb.New(time.Second * 5 * 22), + SecondFactorCheckLifetime: durationpb.New(time.Second * 5 * 23), + MultiFactorCheckLifetime: durationpb.New(time.Second * 5 * 24), + AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, + ForceMfaLocalOnly: true, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.login.changed + assert.Equal(t, true, *setting.AllowRegister) + assert.Equal(t, true, *setting.AllowExternalIDP) + assert.Equal(t, true, *setting.ForceMFA) + assert.Equal(t, domain.PasswordlessTypeAllowed, *setting.PasswordlessType) + assert.Equal(t, true, *setting.HidePasswordReset) + assert.Equal(t, true, *setting.IgnoreUnknownUsernames) + assert.Equal(t, "http://www.new_example.com", *setting.DefaultRedirectURI) + assert.Equal(t, true, *setting.AllowDomainDiscovery) + assert.Equal(t, true, *setting.AllowUserNamePassword) + assert.Equal(t, time.Duration(time.Second*5*20), *setting.PasswordCheckLifetime) + assert.Equal(t, time.Duration(time.Second*5*21), *setting.ExternalLoginCheckLifetime) + assert.Equal(t, time.Duration(time.Second*5*22), *setting.MFAInitSkipLifetime) + assert.Equal(t, time.Duration(time.Second*5*23), *setting.SecondFactorCheckLifetime) + assert.Equal(t, time.Duration(time.Second*5*24), *setting.MultiFactorCheckLifetime) + assert.Equal(t, true, *setting.DisableLoginWithEmail) + assert.Equal(t, true, *setting.DisableLoginWithPhone) + assert.Equal(t, true, *setting.ForceMFALocalOnly) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test added/remove multifactor type reduces", func(t *testing.T) { + // check initial MFAType value + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + assert.Nil(t, setting.MFAType) + }, retryDuration, tick) + + // add MFAType + before := time.Now() + _, err := newInstance.Client.Mgmt.AddMultiFactorToLoginPolicy(IAMCTX, &management.AddMultiFactorToLoginPolicyRequest{ + Type: policy.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.login.multifactor.added + assert.Equal(t, []domain.MultiFactorType{domain.MultiFactorType(policy.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION)}, setting.MFAType) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + + // remove MFAType + _, err = newInstance.Client.Mgmt.RemoveMultiFactorFromLoginPolicy(IAMCTX, &management.RemoveMultiFactorFromLoginPolicyRequest{ + Type: policy.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION, + }) + require.NoError(t, err) + + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.login.multifactor.remove + assert.Equal(t, []domain.MultiFactorType{}, setting.MFAType) + }, retryDuration, tick) + }) + + t.Run("test added/removed second multifactor reduces", func(t *testing.T) { + before := time.Now() + // get current second factor types + var secondFactorTypes []domain.SecondFactorType + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + secondFactorTypes = setting.SecondFactorTypes + }, retryDuration, tick) + + // add new second factor type + before = time.Now() + _, err := newInstance.Client.Mgmt.AddSecondFactorToLoginPolicy(IAMCTX, &management.AddSecondFactorToLoginPolicyRequest{ + Type: policy.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS, + }) + require.NoError(t, err) + after := time.Now() + + secondFactorTypes = append(secondFactorTypes, domain.SecondFactorType(policy.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS)) + + // check new second factor type is added + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.login.multifactor.secondfactor.added + assert.Equal(t, secondFactorTypes, setting.SecondFactorTypes) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + + // remove second factor type + before = time.Now() + _, err = newInstance.Client.Mgmt.RemoveSecondFactorFromLoginPolicy(IAMCTX, &management.RemoveSecondFactorFromLoginPolicyRequest{ + Type: policy.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS, + }) + require.NoError(t, err) + after = time.Now() + + secondFactorTypes = secondFactorTypes[0 : len(secondFactorTypes)-1] + + // check new second factor type is removed + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.login.multifactor.secondfactor.removed + assert.Equal(t, secondFactorTypes, setting.SecondFactorTypes) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test login settings removed reduces", func(t *testing.T) { + _, err := newInstance.Client.Mgmt.ResetLoginPolicyToDefault(IAMCTX, &management.ResetLoginPolicyToDefaultRequest{}) + require.NoError(t, err) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + + // event org.policy.login.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) + + t.Run("test delete org reduces", func(t *testing.T) { + // add delete org + _, err := newInstance.Client.OrgV2beta.DeleteOrganization(IAMCTX, &v2beta_org.DeleteOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + + // check login settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLogin, domain.SettingStateActive), + ), + ) + + // event org.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestOrgLabelSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.BrandingSettingsRepository() + + IAMCTX, newInstance, orgId := createInstanceWithOrg(t) + + t.Run("test adding label settings reduces", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Mgmt.AddCustomLabelPolicy(IAMCTX, &management.AddCustomLabelPolicyRequest{ + PrimaryColor: "#055090", + HideLoginNameSuffix: false, + WarnColor: "#055090", + BackgroundColor: "#055090", + FontColor: "#055090", + PrimaryColorDark: "#055090", + BackgroundColorDark: "#055090", + WarnColorDark: "#055090", + FontColorDark: "#055090", + DisableWatermark: false, + ThemeMode: policy.ThemeMode_THEME_MODE_DARK, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.added + assert.Equal(t, "#055090", *setting.PrimaryColorLight) + assert.Equal(t, "#055090", *setting.BackgroundColorLight) + assert.Equal(t, "#055090", *setting.WarnColorLight) + assert.Equal(t, "#055090", *setting.FontColorLight) + assert.Equal(t, "#055090", *setting.PrimaryColorDark) + assert.Equal(t, "#055090", *setting.BackgroundColorDark) + assert.Equal(t, "#055090", *setting.WarnColorDark) + assert.Equal(t, "#055090", *setting.WarnColorDark) + assert.Equal(t, "#055090", *setting.FontColorDark) + assert.Equal(t, false, *setting.HideLoginNameSuffix) + assert.Equal(t, false, *setting.ErrorMsgPopup) + assert.Equal(t, false, *setting.DisableWatermark) + assert.Equal(t, domain.BrandingPolicyThemeDark, *setting.ThemeMode) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.CreatedAt, before, after) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label change", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Mgmt.UpdateCustomLabelPolicy(IAMCTX, &management.UpdateCustomLabelPolicyRequest{ + PrimaryColor: "#055000", + HideLoginNameSuffix: true, + WarnColor: "#055000", + BackgroundColor: "#055000", + FontColor: "#055000", + PrimaryColorDark: "#055000", + BackgroundColorDark: "#055000", + WarnColorDark: "#055000", + FontColorDark: "#055000", + DisableWatermark: true, + ThemeMode: policy.ThemeMode_THEME_MODE_LIGHT, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.change + assert.Equal(t, "#055000", *setting.PrimaryColorLight) + assert.Equal(t, "#055000", *setting.BackgroundColorLight) + assert.Equal(t, "#055000", *setting.WarnColorLight) + assert.Equal(t, "#055000", *setting.FontColorLight) + assert.Equal(t, "#055000", *setting.PrimaryColorDark) + assert.Equal(t, "#055000", *setting.BackgroundColorDark) + assert.Equal(t, "#055000", *setting.WarnColorDark) + assert.Equal(t, "#055000", *setting.WarnColorDark) + assert.Equal(t, "#055000", *setting.FontColorDark) + assert.Equal(t, true, *setting.HideLoginNameSuffix) + assert.Equal(t, false, *setting.ErrorMsgPopup) + assert.Equal(t, true, *setting.DisableWatermark) + assert.Equal(t, domain.BrandingPolicyThemeLight, *setting.ThemeMode) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test label settings activated", func(t *testing.T) { + // activate label + before := time.Now() + _, err := newInstance.Client.Mgmt.ActivateCustomLabelPolicy(IAMCTX, &management.ActivateCustomLabelPolicyRequest{}) + require.NoError(t, err) + after := time.Now() + + _, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.label.activated + assert.Equal(t, domain.SettingStateActive, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, time.Minute*5, tick) + }) + + t.Run("test policy label logo light added", func(t *testing.T) { + // set logo light + before := time.Now() + uploadOrganizationAsset(IAMCTX, t, newInstance, "/org/policy/label/logo", bytes.NewReader(picture)) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.logo.added + assert.NotNil(t, setting.LogoURLLight) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label logo light removed", func(t *testing.T) { + // remote logo light + before := time.Now() + _, err := newInstance.Client.Mgmt.RemoveCustomLabelPolicyLogo(IAMCTX, &management.RemoveCustomLabelPolicyLogoRequest{}) + require.NoError(t, err) + after := time.Now() + + // check light logo removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.logo.removed + assert.Equal(t, url.URL{}, *setting.LogoURLLight) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label logo dark added", func(t *testing.T) { + // set logo dark + before := time.Now() + uploadOrganizationAsset(IAMCTX, t, newInstance, "/org/policy/label/logo/dark", bytes.NewReader(picture)) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.logo.dark.added + assert.NotNil(t, setting.LogoURLDark) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label logo dark removed", func(t *testing.T) { + // remove logo dark + before := time.Now() + _, err := newInstance.Client.Mgmt.RemoveCustomLabelPolicyLogoDark(IAMCTX, &management.RemoveCustomLabelPolicyLogoDarkRequest{}) + require.NoError(t, err) + after := time.Now() + + // check dark logo removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.logo.dark.removed + assert.Equal(t, url.URL{}, *setting.LogoURLDark) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label icon light added", func(t *testing.T) { + // set icon light + before := time.Now() + uploadOrganizationAsset(IAMCTX, t, newInstance, "/org/policy/label/icon", bytes.NewReader(picture)) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.icon.added + assert.NotNil(t, setting.IconURLLight) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label icon light removed", func(t *testing.T) { + // remote icon light + before := time.Now() + _, err := newInstance.Client.Mgmt.RemoveCustomLabelPolicyIcon(IAMCTX, &management.RemoveCustomLabelPolicyIconRequest{}) + require.NoError(t, err) + after := time.Now() + + // check light icon removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.icon.removed + assert.Equal(t, url.URL{}, *setting.IconURLLight) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label icon dark added", func(t *testing.T) { + // set icon dark + before := time.Now() + uploadOrganizationAsset(IAMCTX, t, newInstance, "/org/policy/label/icon/dark", bytes.NewReader(picture)) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.icon.dark.added + assert.NotNil(t, setting.IconURLDark) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy label icon dark removed", func(t *testing.T) { + // remove icon dark + before := time.Now() + _, err := newInstance.Client.Mgmt.RemoveCustomLabelPolicyIconDark(IAMCTX, &management.RemoveCustomLabelPolicyIconDarkRequest{}) + require.NoError(t, err) + after := time.Now() + + // check dark icon removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.icon.dark.removed + assert.Equal(t, url.URL{}, *setting.IconURLDark) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy font added", func(t *testing.T) { + // set font + before := time.Now() + uploadOrganizationAsset(IAMCTX, t, newInstance, "/org/policy/label/font", bytes.NewReader(font)) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.font.added + assert.NotNil(t, setting.FontURL) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test policy font removed", func(t *testing.T) { + // remove font + before := time.Now() + _, err := newInstance.Client.Mgmt.RemoveCustomLabelPolicyFont(IAMCTX, &management.RemoveCustomLabelPolicyFontRequest{}) + require.NoError(t, err) + after := time.Now() + + // check font policy removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + require.NoError(t, err) + + // event org.policy.label.font.removed + assert.Equal(t, url.URL{}, *setting.FontURL) + assert.Equal(t, domain.SettingStatePreview, setting.State) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test label settings remove reduces", func(t *testing.T) { + // remove label policy + _, err := newInstance.Client.Mgmt.ResetLabelPolicyToDefault(IAMCTX, &management.ResetLabelPolicyToDefaultRequest{}) + require.NoError(t, err) + + // check label settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStateActive), + ), + ) + + // event org.policy.label.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) + + t.Run("test org delete reduces", func(t *testing.T) { + // delete org + _, err := newInstance.Client.OrgV2beta.DeleteOrganization(IAMCTX, &v2beta_org.DeleteOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + + // check label settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeBranding, domain.SettingStatePreview), + ), + ) + + // event org.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestOrgPasswordComplexitySettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.PasswordComplexitySettingsRepository() + + IAMCTX, newInstance, orgId := createInstanceWithOrg(t) + + t.Run("test password complexity added", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Mgmt.AddCustomPasswordComplexityPolicy(IAMCTX, &management.AddCustomPasswordComplexityPolicyRequest{ + MinLength: 10, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.password.complexity.added + assert.Equal(t, uint64(10), *setting.MinLength) + assert.Equal(t, true, *setting.HasUppercase) + assert.Equal(t, true, *setting.HasLowercase) + assert.Equal(t, true, *setting.HasNumber) + assert.Equal(t, true, *setting.HasSymbol) + assert.WithinRange(t, setting.CreatedAt, before, after) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test password complexity change", func(t *testing.T) { + // update password compexity + before := time.Now() + _, err := newInstance.Client.Mgmt.UpdateCustomPasswordComplexityPolicy(IAMCTX, &management.UpdateCustomPasswordComplexityPolicyRequest{ + MinLength: 5, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.password.complexity.changed + assert.Equal(t, uint64(5), *setting.MinLength) + assert.Equal(t, true, *setting.HasUppercase) + assert.Equal(t, true, *setting.HasLowercase) + assert.Equal(t, true, *setting.HasNumber) + assert.Equal(t, true, *setting.HasSymbol) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test password complexity removed", func(t *testing.T) { + // delete password complexity policy + _, err := newInstance.Client.Mgmt.ResetPasswordComplexityPolicyToDefault(IAMCTX, &management.ResetPasswordComplexityPolicyToDefaultRequest{}) + require.NoError(t, err) + + // check login settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + ), + ) + + // event org.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) + + t.Run("test delete org reduces", func(t *testing.T) { + // create password complexity + _, err := newInstance.Client.Mgmt.AddCustomPasswordComplexityPolicy(IAMCTX, &management.AddCustomPasswordComplexityPolicyRequest{ + MinLength: 10, + HasUppercase: false, + HasLowercase: false, + HasNumber: false, + HasSymbol: false, + }) + require.NoError(t, err) + + // check password complexity settings exist + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + require.NotNil(t, setting) + }, retryDuration, tick) + + // add delete org + _, err = newInstance.Client.OrgV2beta.DeleteOrganization(IAMCTX, &v2beta_org.DeleteOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + + // check login settings removed + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + ), + ) + + // event org.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestOrgPasswordPolicySettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.PasswordExpirySettingsRepository() + + IAMCTX, newInstance, orgId := createInstanceWithOrg(t) + + t.Run("test password policy added", func(t *testing.T) { + // add password policy + before := time.Now() + _, err := newInstance.Client.Mgmt.AddCustomPasswordAgePolicy(IAMCTX, &management.AddCustomPasswordAgePolicyRequest{ + MaxAgeDays: 10, + ExpireWarnDays: 10, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.password.age.added + assert.Equal(t, uint64(10), *setting.ExpireWarnDays) + assert.Equal(t, uint64(10), *setting.MaxAgeDays) + assert.WithinRange(t, setting.CreatedAt, before, after) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test password policy change reduces", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Mgmt.UpdateCustomPasswordAgePolicy(IAMCTX, &management.UpdateCustomPasswordAgePolicyRequest{ + MaxAgeDays: 40, + ExpireWarnDays: 40, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.password.age.changed + assert.Equal(t, uint64(40), *setting.ExpireWarnDays) + assert.Equal(t, uint64(40), *setting.MaxAgeDays) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test password policy removed reduces", func(t *testing.T) { + // remove password policy delete org + _, err := newInstance.Client.Mgmt.ResetPasswordAgePolicyToDefault(IAMCTX, &management.ResetPasswordAgePolicyToDefaultRequest{}) + require.NoError(t, err) + + // check password complexity settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + ), + ) + + // event org.policy.password.age.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) + + t.Run("test delete org reduces", func(t *testing.T) { + // add delete org + _, err := newInstance.Client.OrgV2beta.DeleteOrganization(IAMCTX, &v2beta_org.DeleteOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + + // check password complexity settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + ), + ) + + // event org.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestOrgLockoutSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.LockoutSettingsRepository() + + IAMCTX, newInstance, orgId := createInstanceWithOrg(t) + + t.Run("test lockout policy added", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Mgmt.AddCustomLockoutPolicy(IAMCTX, &management.AddCustomLockoutPolicyRequest{ + MaxPasswordAttempts: 1, + MaxOtpAttempts: 1, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLockout, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.lockout.added + assert.Equal(t, uint64(1), *setting.MaxOTPAttempts) + assert.Equal(t, uint64(1), *setting.MaxPasswordAttempts) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test lockout policy changed", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Mgmt.UpdateCustomLockoutPolicy(IAMCTX, &management.UpdateCustomLockoutPolicyRequest{ + MaxPasswordAttempts: 5, + MaxOtpAttempts: 5, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLockout, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.lockout.changed + assert.Equal(t, uint64(5), *setting.MaxOTPAttempts) + assert.Equal(t, uint64(5), *setting.MaxPasswordAttempts) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test remove lockout policy reduces", func(t *testing.T) { + // remove lockout policy org + _, err := newInstance.Client.Mgmt.ResetLockoutPolicyToDefault(IAMCTX, &management.ResetLockoutPolicyToDefaultRequest{}) + require.NoError(t, err) + + // check password complexity settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLockout, domain.SettingStateActive), + ), + ) + + // event org.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) + + t.Run("test delete org reduces", func(t *testing.T) { + // add delete org + _, err := newInstance.Client.OrgV2beta.DeleteOrganization(IAMCTX, &v2beta_org.DeleteOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + + // check password complexity settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLockout, domain.SettingStateActive), + ), + ) + + // event org.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestOrgDomainSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.DomainSettingsRepository() + + IAMCTX, newInstance, orgId := createInstanceWithOrg(t) + + t.Run("test domain policy added", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Admin.AddCustomDomainPolicy(IAMCTX, &admin.AddCustomDomainPolicyRequest{ + OrgId: orgId, + UserLoginMustBeDomain: false, + ValidateOrgDomains: false, + SmtpSenderAddressMatchesInstanceDomain: false, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeDomain, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.domain.added + assert.Equal(t, false, *setting.SMTPSenderAddressMatchesInstanceDomain) + assert.Equal(t, false, *setting.LoginNameIncludesDomain) + assert.Equal(t, false, *setting.RequireOrgDomainVerification) + assert.WithinRange(t, setting.CreatedAt, before, after) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test domain policy changed", func(t *testing.T) { + // update domain policy + before := time.Now() + _, err := newInstance.Client.Admin.UpdateCustomDomainPolicy(IAMCTX, &admin.UpdateCustomDomainPolicyRequest{ + OrgId: orgId, + UserLoginMustBeDomain: true, + ValidateOrgDomains: true, + SmtpSenderAddressMatchesInstanceDomain: true, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeDomain, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event org.policy.domain.changed + assert.Equal(t, true, *setting.SMTPSenderAddressMatchesInstanceDomain) + assert.Equal(t, true, *setting.LoginNameIncludesDomain) + assert.Equal(t, true, *setting.RequireOrgDomainVerification) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test remove domain policy reduces", func(t *testing.T) { + // remove domain policy org + _, err := newInstance.Client.Admin.ResetCustomDomainPolicyToDefault(IAMCTX, &admin.ResetCustomDomainPolicyToDefaultRequest{ + OrgId: orgId, + }) + require.NoError(t, err) + + // check domain settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeDomain, domain.SettingStateActive), + ), + ) + + // event org.policy.domain.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) + + t.Run("test delete org reduces", func(t *testing.T) { + // add delete org + _, err := newInstance.Client.OrgV2beta.DeleteOrganization(IAMCTX, &v2beta_org.DeleteOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + + // check password complexity settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeDomain, domain.SettingStateActive), + ), + ) + + // event org.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestOrgSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.OrganizationSettingsRepository() + + IAMCTX, newInstance, orgId := createInstanceWithOrg(t) + + t.Run("test add org settings set", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.SettingsV2beta.SetOrganizationSettings(IAMCTX, &settings.SetOrganizationSettingsRequest{ + OrganizationId: orgId, + OrganizationScopedUsernames: gu.Ptr(true), + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeOrganization, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event settings.organization.set + assert.Equal(t, true, *setting.OrganizationScopedUsernames) + assert.WithinRange(t, setting.CreatedAt, before, after) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test add org settings re-set", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.SettingsV2beta.SetOrganizationSettings(IAMCTX, &settings.SetOrganizationSettingsRequest{ + OrganizationId: orgId, + OrganizationScopedUsernames: gu.Ptr(false), + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeOrganization, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + // event settings.organization.set + assert.Equal(t, false, *setting.OrganizationScopedUsernames) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test organization settings removed reduces", func(t *testing.T) { + // delete organization setting + _, err := newInstance.Client.SettingsV2beta.DeleteOrganizationSettings(IAMCTX, &settings.DeleteOrganizationSettingsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + + // check organization settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeOrganization, domain.SettingStateActive), + ), + ) + + // event settings.organization.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) + + t.Run("test organization removed reduces", func(t *testing.T) { + // add delete org + _, err := newInstance.Client.OrgV2beta.DeleteOrganization(IAMCTX, &v2beta_org.DeleteOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + + // check organization settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeOrganization, domain.SettingStateActive), + ), + ) + + // event org.removed + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestNotificationSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.NotificationSettingsRepository() + + IAMCTX, newInstance, orgId := createInstanceWithOrg(t) + + t.Run("test add notification settings set", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Mgmt.AddCustomNotificationPolicy(IAMCTX, &management.AddCustomNotificationPolicyRequest{ + PasswordChange: true, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeNotification, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + assert.Equal(t, true, *setting.PasswordChange) + assert.WithinRange(t, setting.CreatedAt, before, after) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test add notification settings re-set", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Mgmt.UpdateCustomNotificationPolicy(IAMCTX, &management.UpdateCustomNotificationPolicyRequest{ + PasswordChange: false, + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeNotification, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + assert.Equal(t, false, *setting.PasswordChange) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test notification settings removed reduces", func(t *testing.T) { + _, err := newInstance.Client.Mgmt.ResetNotificationPolicyToDefault(IAMCTX, &management.ResetNotificationPolicyToDefaultRequest{}) + require.NoError(t, err) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeNotification, domain.SettingStateActive), + ), + ) + + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) + + t.Run("test organization removed reduces", func(t *testing.T) { + // add delete org + _, err := newInstance.Client.OrgV2beta.DeleteOrganization(IAMCTX, &v2beta_org.DeleteOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + + // check organization settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeNotification, domain.SettingStateActive), + ), + ) + + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} + +func TestServer_TestLegalAndSupportSettingsReduces(t *testing.T) { + t.Parallel() + + settingsRepo := repository.LegalAndSupportSettingsRepository() + + IAMCTX, newInstance, orgId := createInstanceWithOrg(t) + + t.Run("test add legal and support settings set", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Mgmt.AddCustomPrivacyPolicy(IAMCTX, &management.AddCustomPrivacyPolicyRequest{ + TosLink: "https://tos.example.com", + PrivacyLink: "https://privacy.example.com", + HelpLink: "https://help.example.com", + SupportEmail: "support@example.com", + DocsLink: "https://docs.example.com", + CustomLink: "https://custom.example.com", + CustomLinkText: "Custom link text", + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + assert.Equal(t, "https://tos.example.com", *setting.TOSLink) + assert.Equal(t, "https://privacy.example.com", *setting.PrivacyPolicyLink) + assert.Equal(t, "https://help.example.com", *setting.HelpLink) + assert.Equal(t, "support@example.com", *setting.SupportEmail) + assert.Equal(t, "https://docs.example.com", *setting.DocsLink) + assert.Equal(t, "https://custom.example.com", *setting.CustomLink) + assert.Equal(t, "Custom link text", *setting.CustomLinkText) + assert.WithinRange(t, setting.CreatedAt, before, after) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test add legal and support settings re-set", func(t *testing.T) { + before := time.Now() + _, err := newInstance.Client.Mgmt.UpdateCustomPrivacyPolicy(IAMCTX, &management.UpdateCustomPrivacyPolicyRequest{ + TosLink: "https://tos.example2.com", + PrivacyLink: "https://privacy.example2.com", + HelpLink: "https://help.example2.com", + SupportEmail: "support@example2.com", + DocsLink: "https://docs.example2.com", + CustomLink: "https://custom.example2.com", + CustomLinkText: "Custom link text2", + }) + require.NoError(t, err) + after := time.Now() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + setting, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + ), + ) + require.NoError(t, err) + + assert.Equal(t, "https://tos.example2.com", *setting.TOSLink) + assert.Equal(t, "https://privacy.example2.com", *setting.PrivacyPolicyLink) + assert.Equal(t, "https://help.example2.com", *setting.HelpLink) + assert.Equal(t, "support@example2.com", *setting.SupportEmail) + assert.Equal(t, "https://docs.example2.com", *setting.DocsLink) + assert.Equal(t, "https://custom.example2.com", *setting.CustomLink) + assert.Equal(t, "Custom link text2", *setting.CustomLinkText) + assert.WithinRange(t, setting.UpdatedAt, before, after) + }, retryDuration, tick) + }) + + t.Run("test legal and support settings removed reduces", func(t *testing.T) { + _, err := newInstance.Client.Mgmt.ResetPrivacyPolicyToDefault(IAMCTX, &management.ResetPrivacyPolicyToDefaultRequest{}) + require.NoError(t, err) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + ), + ) + + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) + + t.Run("test organization removed reduces", func(t *testing.T) { + // add delete org + _, err := newInstance.Client.OrgV2beta.DeleteOrganization(IAMCTX, &v2beta_org.DeleteOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + + // check organization settings removed + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20) + assert.EventuallyWithT(t, func(t *assert.CollectT) { + _, err := settingsRepo.Get( + IAMCTX, pool, + database.WithCondition( + settingsRepo.UniqueCondition(newInstance.ID(), &orgId, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + ), + ) + + require.ErrorIs(t, err, new(database.NoRowFoundError)) + }, retryDuration, tick) + }) +} diff --git a/backend/v3/storage/database/json/json.go b/backend/v3/storage/database/json/json.go new file mode 100644 index 000000000..55942e6e4 --- /dev/null +++ b/backend/v3/storage/database/json/json.go @@ -0,0 +1,225 @@ +package json + +import ( + "encoding/json" + "reflect" + + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +var _ database.Change = (*jsonChanges)(nil) + +type JsonUpdate interface { + writeUpdate(builder *database.StatementBuilder, changes jsonChanges, i int) error + getPathValue() ([]string, string, error) +} + +type FieldChange struct { + path []string + value any +} + +func NewFieldChange(path []string, value any) JsonUpdate { + return &FieldChange{ + path: path, + value: value, + } +} + +func (c *FieldChange) getPathValue() ([]string, string, error) { + path, v := c.path, c.value + + value, err := json.Marshal(v) + if err != nil { + return nil, "", err + } + + return path, string(value), nil +} + +func (c *FieldChange) writeUpdate(builder *database.StatementBuilder, changes jsonChanges, i int) error { + path, value, err := c.getPathValue() + if err != nil { + return err + } + + builder.WriteString("jsonb_set_lax(") + if i < 0 { + changes.column.WriteQualified(builder) + } else { + err := changes.changes[i].writeUpdate(builder, changes, i-1) + if err != nil { + return err + } + } + builder.WriteString(", ") + builder.WriteArg(path) + builder.WriteString(", ") + builder.WriteArg(value) + builder.WriteString(", true, 'delete_key')") + + return nil +} + +type ArrayChange struct { + path []string + value any + remove bool +} + +func NewArrayChange(path []string, value any, remove bool) JsonUpdate { + return &ArrayChange{ + path: path, + value: value, + remove: remove, + } +} + +func (c *ArrayChange) getPathValue() ([]string, string, error) { + path, v := c.path, c.value + + value, err := json.Marshal(v) + if err != nil { + return nil, "", err + } + + return path, string(value), nil +} + +func (c *ArrayChange) writeUpdate(builder *database.StatementBuilder, changes jsonChanges, i int) error { + if c.remove { + return c.removeFromArray(builder, changes, i) + } + return c.addToArray(builder, changes, i) +} + +func (c *ArrayChange) addToArray(builder *database.StatementBuilder, changes jsonChanges, i int) error { + path, value, err := c.getPathValue() + if err != nil { + return err + } + + builder.WriteString("zitadel.jsonb_array_append(") + + // to mitigate the possibility of having duplicate values in the array + builder.WriteString("zitadel.jsonb_array_remove(") + if i < 0 { + changes.column.WriteQualified(builder) + } else { + err := changes.changes[i].writeUpdate(builder, changes, i-1) + if err != nil { + return err + } + } + + // zitadel.jsonb_array_remove + builder.WriteString(", ") + builder.WriteArg(path) + builder.WriteString(", ") + builder.WriteArg(value) + builder.WriteString("::TEXT)") + + // zitadel.jsonb_array_append + builder.WriteString(", ") + builder.WriteArg(path) + builder.WriteString(", ") + builder.WriteArg(value) + if len(value) > 2 && value[0] == '"' && value[len(value)-1] == '"' { + builder.WriteString("::TEXT") + } else { + builder.WriteString("::NUMERIC") + } + + builder.WriteString(")") + + return nil +} + +func (c *ArrayChange) removeFromArray(builder *database.StatementBuilder, changes jsonChanges, i int) error { + path, value, err := c.getPathValue() + if err != nil { + return err + } + + builder.WriteString("zitadel.jsonb_array_remove(") + if i < 0 { + changes.column.WriteQualified(builder) + } else { + err := changes.changes[i].writeUpdate(builder, changes, i-1) + if err != nil { + return err + } + } + builder.WriteString(", ") + builder.WriteArg(path) + builder.WriteString(", ") + builder.WriteArg(value) + builder.WriteString("::TEXT") + builder.WriteString(")") + + return nil +} + +var _ JsonUpdate = (*ArrayChange)(nil) + +type jsonChanges struct { + column database.Column + changes []JsonUpdate +} + +var _ database.Change = (*jsonChanges)(nil) + +func NewJsonChanges(col database.Column, changes ...JsonUpdate) database.Change { + return &jsonChanges{ + column: col, + changes: changes, + } +} + +func (c *jsonChanges) Matches(x any) bool { + toMatch, ok := x.(*jsonChanges) + if !ok { + return false + } + + if !c.column.Equals(toMatch.column) { + return false + } + + if len(c.changes) != len(toMatch.changes) { + return false + } + + for _, c1 := range c.changes { + found := false + for _, c2 := range toMatch.changes { + if reflect.DeepEqual(c1, c2) { + found = true + } + } + if !found { + return false + } + } + + return true +} + +func (c *jsonChanges) String() string { + return "database.json.jsonChange" +} + +func (c jsonChanges) Write(builder *database.StatementBuilder) error { + c.column.WriteUnqualified(builder) + builder.WriteString(" = ") + + if c.changes == nil { + return nil + } + + return c.changes[len(c.changes)-1].writeUpdate(builder, c, len(c.changes)-2) +} + +func (c jsonChanges) IsOnColumn(col database.Column) bool { + return c.column.Equals(col) +} diff --git a/backend/v3/storage/database/json/json_test.go b/backend/v3/storage/database/json/json_test.go new file mode 100644 index 000000000..6df635b1c --- /dev/null +++ b/backend/v3/storage/database/json/json_test.go @@ -0,0 +1,250 @@ +package json + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +func TestFieldChange(t *testing.T) { + for _, test := range []struct { + name string + change database.Change + output string + }{ + { + name: "single json update", + change: func() database.Change { + col := database.NewColumn("table", "column") + change := NewFieldChange([]string{"path"}, "value") + changes := NewJsonChanges(col, change) + + return changes + }(), + output: `column = jsonb_set_lax(table.column, $1, $2, true, 'delete_key')`, + }, + { + name: "two json update", + change: func() database.Change { + col := database.NewColumn("table", "column") + change1 := NewFieldChange([]string{"path"}, "value") + change2 := NewFieldChange([]string{"path"}, "value") + changes := NewJsonChanges(col, change1, change2) + + return changes + }(), + output: `column = jsonb_set_lax(jsonb_set_lax(table.column, $1, $2, true, 'delete_key'), $3, $4, true, 'delete_key')`, + }, + } { + t.Run(test.name, func(t *testing.T) { + builder := database.StatementBuilder{} + err := test.change.Write(&builder) + require.NoError(t, err) + + assert.Equal(t, test.output, builder.String()) + }) + } +} + +func TestArrayChange(t *testing.T) { + for _, test := range []struct { + name string + change database.Change + output string + }{ + { + name: "one json array add", + change: func() database.Change { + col := database.NewColumn("table", "column") + change := NewArrayChange([]string{"path"}, "value", false) + changes := NewJsonChanges(col, change) + + return changes + }(), + output: `column = zitadel.jsonb_array_append(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT)`, + }, + { + name: "two json array add", + change: func() database.Change { + col := database.NewColumn("table", "column") + change1 := NewArrayChange([]string{"path)"}, "value", false) + change2 := NewArrayChange([]string{"path"}, "value", false) + changes := NewJsonChanges(col, change1, change2) + + return changes + }(), + output: `column = zitadel.jsonb_array_append(zitadel.jsonb_array_remove(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT), $7, $8::TEXT)`, + }, + { + name: "one json array remove", + change: func() database.Change { + col := database.NewColumn("table", "column") + change := NewArrayChange([]string{"path"}, "value", true) + changes := NewJsonChanges(col, change) + + return changes + }(), + output: `column = zitadel.jsonb_array_remove(table.column, $1, $2::TEXT)`, + }, + { + name: "two json array remove", + change: func() database.Change { + col := database.NewColumn("table", "column") + change1 := NewArrayChange([]string{"path)"}, "value", true) + change2 := NewArrayChange([]string{"path"}, "value", true) + changes := NewJsonChanges(col, change1, change2) + + return changes + }(), + output: `column = zitadel.jsonb_array_remove(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT)`, + }, + } { + t.Run(test.name, func(t *testing.T) { + builder := database.StatementBuilder{} + err := test.change.Write(&builder) + require.NoError(t, err) + + assert.Equal(t, test.output, builder.String()) + }) + } +} + +func TestArrayMixedChange(t *testing.T) { + for _, test := range []struct { + name string + change database.Change + output string + }{ + { + name: "one json array add, one json remove", + change: func() database.Change { + col := database.NewColumn("table", "column") + change1 := NewArrayChange([]string{"path)"}, "value", false) + change2 := NewArrayChange([]string{"path"}, "value", true) + changes := NewJsonChanges(col, change1, change2) + + return changes + }(), + output: `column = zitadel.jsonb_array_remove(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT)`, + }, + { + name: "one json array remove, one json array add", + change: func() database.Change { + col := database.NewColumn("table", "column") + change1 := NewArrayChange([]string{"path)"}, "value", true) + change2 := NewArrayChange([]string{"path"}, "value", false) + changes := NewJsonChanges(col, change1, change2) + + return changes + }(), + output: `column = zitadel.jsonb_array_append(zitadel.jsonb_array_remove(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT)`, + }, + { + name: "one json array add, one json array remove, one array add", + change: func() database.Change { + col := database.NewColumn("table", "column") + change1 := NewArrayChange([]string{"path)"}, "value", false) + change2 := NewArrayChange([]string{"path"}, "value", true) + change3 := NewArrayChange([]string{"path)"}, "value", false) + changes := NewJsonChanges(col, change1, change2, change3) + + return changes + }(), + output: `column = zitadel.jsonb_array_append(zitadel.jsonb_array_remove(zitadel.jsonb_array_remove(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT), $7, $8::TEXT), $9, $10::TEXT)`, + }, + { + name: "one json array remove, one json array add, one array remove", + change: func() database.Change { + col := database.NewColumn("table", "column") + change1 := NewArrayChange([]string{"path)"}, "value", true) + change2 := NewArrayChange([]string{"path"}, "value", false) + change3 := NewArrayChange([]string{"path)"}, "value", true) + changes := NewJsonChanges(col, change1, change2, change3) + + return changes + }(), + output: `column = zitadel.jsonb_array_remove(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT), $7, $8::TEXT)`, + }, + } { + t.Run(test.name, func(t *testing.T) { + builder := database.StatementBuilder{} + err := test.change.Write(&builder) + require.NoError(t, err) + + assert.Equal(t, test.output, builder.String()) + }) + } +} + +// TODO +func TestFieldArrayMixedChange(t *testing.T) { + for _, test := range []struct { + name string + change database.Change + output string + }{ + { + name: "one field change, one json array add, one json remove", + change: func() database.Change { + col := database.NewColumn("table", "column") + change1 := NewFieldChange([]string{"path"}, "value") + change2 := NewArrayChange([]string{"path)"}, "value", false) + change3 := NewArrayChange([]string{"path"}, "value", true) + changes := NewJsonChanges(col, change1, change2, change3) + + return changes + }(), + output: `column = zitadel.jsonb_array_remove(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(jsonb_set_lax(table.column, $1, $2, true, 'delete_key'), $3, $4::TEXT), $5, $6::TEXT), $7, $8::TEXT)`, + }, + { + name: "one json array add, one field change, one json remove", + change: func() database.Change { + col := database.NewColumn("table", "column") + change1 := NewArrayChange([]string{"path)"}, "value", false) + change2 := NewFieldChange([]string{"path"}, "value") + change3 := NewArrayChange([]string{"path"}, "value", true) + changes := NewJsonChanges(col, change1, change2, change3) + + return changes + }(), + output: `column = zitadel.jsonb_array_remove(jsonb_set_lax(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6, true, 'delete_key'), $7, $8::TEXT)`, + }, + { + name: "one json remove, one json array, add one field change", + change: func() database.Change { + col := database.NewColumn("table", "column") + change1 := NewArrayChange([]string{"path"}, "value", true) + change2 := NewArrayChange([]string{"path)"}, "value", false) + change3 := NewFieldChange([]string{"path"}, "value") + changes := NewJsonChanges(col, change1, change2, change3) + + return changes + }(), + output: `column = jsonb_set_lax(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT), $7, $8, true, 'delete_key')`, + }, + { + name: "one json remove, one field change, one json array add", + change: func() database.Change { + col := database.NewColumn("table", "column") + change1 := NewArrayChange([]string{"path"}, "value", true) + change2 := NewFieldChange([]string{"path"}, "value") + change3 := NewArrayChange([]string{"path)"}, "value", false) + changes := NewJsonChanges(col, change1, change2, change3) + + return changes + }(), + output: `column = zitadel.jsonb_array_append(zitadel.jsonb_array_remove(jsonb_set_lax(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4, true, 'delete_key'), $5, $6::TEXT), $7, $8::TEXT)`, + }, + } { + t.Run(test.name, func(t *testing.T) { + builder := database.StatementBuilder{} + err := test.change.Write(&builder) + require.NoError(t, err) + + assert.Equal(t, test.output, builder.String()) + }) + } +} diff --git a/backend/v3/storage/database/repository/id_provider.go b/backend/v3/storage/database/repository/id_provider.go index 54decf427..930246b6f 100644 --- a/backend/v3/storage/database/repository/id_provider.go +++ b/backend/v3/storage/database/repository/id_provider.go @@ -92,7 +92,10 @@ func (i *idProvider) Update(ctx context.Context, client database.QueryExecutor, i.InstanceIDCondition(instanceID), i.OrgIDCondition(orgID), } - database.Changes(changes).Write(&builder) + err := database.Changes(changes).Write(&builder) + if err != nil { + return 0, err + } writeCondition(&builder, database.And(conditions...)) stmt := builder.String() diff --git a/backend/v3/storage/database/repository/instance.go b/backend/v3/storage/database/repository/instance.go index 63f5ba5b6..a088172cb 100644 --- a/backend/v3/storage/database/repository/instance.go +++ b/backend/v3/storage/database/repository/instance.go @@ -110,7 +110,10 @@ func (i instance) Update(ctx context.Context, client database.QueryExecutor, id var builder database.StatementBuilder builder.WriteString(`UPDATE zitadel.instances SET `) - database.Changes(changes).Write(&builder) + err := database.Changes(changes).Write(&builder) + if err != nil { + return 0, err + } idCondition := i.IDCondition(id) writeCondition(&builder, idCondition) @@ -149,15 +152,19 @@ func (i instance) SetUpdatedAt(time time.Time) database.Change { func (i instance) SetIAMProject(id string) database.Change { return database.NewChange(i.IAMProjectIDColumn(), id) } + func (i instance) SetDefaultOrg(id string) database.Change { return database.NewChange(i.DefaultOrgIDColumn(), id) } + func (i instance) SetDefaultLanguage(lang language.Tag) database.Change { return database.NewChange(i.DefaultLanguageColumn(), lang.String()) } + func (i instance) SetConsoleClientID(id string) database.Change { return database.NewChange(i.ConsoleClientIDColumn(), id) } + func (i instance) SetConsoleAppID(id string) database.Change { return database.NewChange(i.ConsoleAppIDColumn(), id) } @@ -182,7 +189,6 @@ func (i instance) NameCondition(op database.TextOperation, name string) database // ExistsDomain creates a correlated [database.Exists] condition on instance_domains. // Use this filter to make sure the Instance returned contains a specific domain. -// of the instance in the aggregated result. // Example usage: // // domainRepo := instanceRepo.Domains(true) // ensure domains are loaded/aggregated diff --git a/backend/v3/storage/database/repository/instance_domain.go b/backend/v3/storage/database/repository/instance_domain.go index 5edaf0af6..709be82b5 100644 --- a/backend/v3/storage/database/repository/instance_domain.go +++ b/backend/v3/storage/database/repository/instance_domain.go @@ -93,7 +93,10 @@ func (i instanceDomain) Update(ctx context.Context, client database.QueryExecuto var builder database.StatementBuilder builder.WriteString(`UPDATE zitadel.instance_domains SET `) - database.Changes(changes).Write(&builder) + err := database.Changes(changes).Write(&builder) + if err != nil { + return 0, err + } writeCondition(&builder, condition) diff --git a/backend/v3/storage/database/repository/instance_domain_test.go b/backend/v3/storage/database/repository/instance_domain_test.go index 1900e6484..2b57dd6ed 100644 --- a/backend/v3/storage/database/repository/instance_domain_test.go +++ b/backend/v3/storage/database/repository/instance_domain_test.go @@ -721,7 +721,8 @@ func TestInstanceDomainChanges(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { var builder database.StatementBuilder - test.change.Write(&builder) + err := test.change.Write(&builder) + require.NoError(t, err) assert.Equal(t, test.expected, builder.String()) }) } diff --git a/backend/v3/storage/database/repository/org.go b/backend/v3/storage/database/repository/org.go index 014ca25bc..8a3c63246 100644 --- a/backend/v3/storage/database/repository/org.go +++ b/backend/v3/storage/database/repository/org.go @@ -108,7 +108,10 @@ func (o org) Update(ctx context.Context, client database.QueryExecutor, conditio var builder database.StatementBuilder builder.WriteString(`UPDATE zitadel.organizations SET `) - database.Changes(changes).Write(&builder) + err := database.Changes(changes).Write(&builder) + if err != nil { + return 0, err + } writeCondition(&builder, condition) stmt := builder.String() diff --git a/backend/v3/storage/database/repository/org_domain.go b/backend/v3/storage/database/repository/org_domain.go index 2ecaf0961..ed21439ef 100644 --- a/backend/v3/storage/database/repository/org_domain.go +++ b/backend/v3/storage/database/repository/org_domain.go @@ -107,7 +107,10 @@ func (o orgDomain) Update(ctx context.Context, client database.QueryExecutor, co var builder database.StatementBuilder builder.WriteString(`UPDATE zitadel.org_domains SET `) - database.Changes(changes).Write(&builder) + err := database.Changes(changes).Write(&builder) + if err != nil { + return 0, err + } writeCondition(&builder, condition) return client.Exec(ctx, builder.String(), builder.Args()...) diff --git a/backend/v3/storage/database/repository/org_domain_test.go b/backend/v3/storage/database/repository/org_domain_test.go index c0a108a27..ae7e1a49a 100644 --- a/backend/v3/storage/database/repository/org_domain_test.go +++ b/backend/v3/storage/database/repository/org_domain_test.go @@ -928,7 +928,8 @@ func TestOrganizationDomainChanges(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { var builder database.StatementBuilder - test.change.Write(&builder) + err := test.change.Write(&builder) + require.NoError(t, err) assert.Equal(t, test.expected, builder.String()) }) } diff --git a/backend/v3/storage/database/repository/project_role.go b/backend/v3/storage/database/repository/project_role.go index 98ebd8734..f50759196 100644 --- a/backend/v3/storage/database/repository/project_role.go +++ b/backend/v3/storage/database/repository/project_role.go @@ -60,7 +60,10 @@ func (p projectRole) Update(ctx context.Context, client database.QueryExecutor, changes = append(changes, database.NewChange(p.UpdatedAtColumn(), database.NullInstruction)) } builder := database.NewStatementBuilder(`UPDATE zitadel.project_roles SET `) - database.Changes(changes).Write(builder) + err := database.Changes(changes).Write(builder) + if err != nil { + return 0, err + } writeCondition(builder, condition) return client.Exec(ctx, builder.String(), builder.Args()...) diff --git a/backend/v3/storage/database/repository/settings.go b/backend/v3/storage/database/repository/settings.go new file mode 100644 index 000000000..5496e1811 --- /dev/null +++ b/backend/v3/storage/database/repository/settings.go @@ -0,0 +1,1407 @@ +package repository + +import ( + "context" + "encoding/json" + "net/url" + "time" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" + db_json "github.com/zitadel/zitadel/backend/v3/storage/database/json" +) + +type settings struct{} + +func (s settings) qualifiedTableName() string { + return "zitadel." + s.unqualifiedTableName() +} + +func (settings) unqualifiedTableName() string { + return "settings" +} + +// ------------------------------------------------------------- +// columns +// ------------------------------------------------------------- + +// PrimaryKeyColumns implements [domain.Repository]. +func (s settings) PrimaryKeyColumns() []database.Column { + return []database.Column{ + s.InstanceIDColumn(), + s.IDColumn(), + } +} + +func (s settings) UniqueColumns() []database.Column { + return []database.Column{ + s.InstanceIDColumn(), + s.OrganizationIDColumn(), + s.TypeColumn(), + s.StateColumn(), + } +} + +func (s settings) IDColumn() database.Column { + return database.NewColumn(s.unqualifiedTableName(), "id") +} + +func (s settings) InstanceIDColumn() database.Column { + return database.NewColumn(s.unqualifiedTableName(), "instance_id") +} + +func (s settings) OrganizationIDColumn() database.Column { + return database.NewColumn(s.unqualifiedTableName(), "organization_id") +} + +func (s settings) TypeColumn() database.Column { + return database.NewColumn(s.unqualifiedTableName(), "type") +} + +func (s settings) StateColumn() database.Column { + return database.NewColumn(s.unqualifiedTableName(), "state") +} + +func (s settings) SettingsColumn() database.Column { + return database.NewColumn(s.unqualifiedTableName(), "settings") +} + +func (s settings) CreatedAtColumn() database.Column { + return database.NewColumn(s.unqualifiedTableName(), "created_at") +} + +func (s settings) UpdatedAtColumn() database.Column { + return database.NewColumn(s.unqualifiedTableName(), "updated_at") +} + +// ------------------------------------------------------------- +// conditions +// ------------------------------------------------------------- + +func (s settings) PrimaryKeyCondition(instanceID, id string) database.Condition { + return database.And( + s.InstanceIDCondition(instanceID), + s.IDCondition(id), + ) +} + +func (s settings) UniqueCondition(instanceID string, orgID *string, typ domain.SettingType, state domain.SettingState) database.Condition { + return database.And( + s.InstanceIDCondition(instanceID), + s.OrganizationIDCondition(orgID), + s.TypeCondition(typ), + s.StateCondition(state), + ) +} + +func (s settings) InstanceIDCondition(id string) database.Condition { + return database.NewTextCondition(s.InstanceIDColumn(), database.TextOperationEqual, id) +} + +func (s settings) OrganizationIDCondition(id *string) database.Condition { + if id == nil { + return database.IsNull(s.OrganizationIDColumn()) + } + return database.NewTextCondition(s.OrganizationIDColumn(), database.TextOperationEqual, *id) +} + +func (s settings) IDCondition(id string) database.Condition { + return database.NewTextCondition(s.IDColumn(), database.TextOperationEqual, id) +} + +func (s settings) TypeCondition(typ domain.SettingType) database.Condition { + return database.NewTextCondition(s.TypeColumn(), database.TextOperationEqual, typ.String()) +} + +func (s settings) StateCondition(typ domain.SettingState) database.Condition { + return database.NewTextCondition(s.StateColumn(), database.TextOperationEqual, typ.String()) +} + +// ------------------------------------------------------------- +// changes +// ------------------------------------------------------------- + +// func (s settings) SetSettings(settings string) database.Change { +// return database.NewChange(s.SettingsColumn(), settings) +// } + +func (s settings) SetUpdatedAt(updatedAt *time.Time) database.Change { + return database.NewChangePtr(s.UpdatedAtColumn(), updatedAt) +} + +func (s settings) get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Settings, error) { + builder, err := s.prepareQuery(opts) + if err != nil { + return nil, err + } + return getOne[domain.Settings](ctx, client, builder) +} + +func (s settings) list(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Settings, error) { + builder, err := s.prepareQuery(opts) + if err != nil { + return nil, err + } + return getMany[domain.Settings](ctx, client, builder) +} + +func (s settings) set(ctx context.Context, client database.QueryExecutor, settings *domain.Settings, changes ...database.Change) error { + var ( + id, createdAt, updatedAt any = database.DefaultInstruction, database.DefaultInstruction, database.DefaultInstruction + ) + + if settings.ID != "" { + id = settings.ID + } + if !settings.CreatedAt.IsZero() { + createdAt = settings.CreatedAt + } + if !settings.UpdatedAt.IsZero() { + updatedAt = settings.UpdatedAt + changes = append(changes, s.SetUpdatedAt(&settings.UpdatedAt)) + } + + builder := database.NewStatementBuilder(`INSERT INTO ` + s.qualifiedTableName()) + builder.WriteString(` (id, instance_id, organization_id, type, state, settings, created_at, updated_at) VALUES (`) + builder.WriteArgs( + id, + settings.InstanceID, + settings.OrganizationID, + settings.Type.String(), + settings.State.String(), + settings.Settings, + createdAt, + updatedAt, + ) + builder.WriteString(`) ON CONFLICT (instance_id, organization_id, type, state) DO UPDATE SET `) + err := database.Changes(changes).Write(builder) + if err != nil { + return err + } + builder.WriteString(` RETURNING id, created_at, updated_at`) + return client.QueryRow(ctx, builder.String(), builder.Args()...).Scan(&settings.ID, &settings.CreatedAt, &settings.UpdatedAt) +} + +func (s settings) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) { + if err := checkRestrictingColumns(condition, s.UniqueColumns()...); err != nil { + return 0, err + } + + var builder database.StatementBuilder + builder.WriteString(`DELETE FROM ` + s.qualifiedTableName()) + + writeCondition(&builder, condition) + return client.Exec(ctx, builder.String(), builder.Args()...) +} + +// ------------------------------------------------------------- +// login +// ------------------------------------------------------------- +type loginSettings struct { + settings +} + +func (l loginSettings) SetSettingFields(value domain.LoginSettingsAttributes) database.Change { + changes := make([]db_json.JsonUpdate, 0) + if value.AllowUserNamePassword != nil { + changes = append(changes, l.SetAllowUserNamePassword(*value.AllowUserNamePassword)) + } + if value.AllowRegister != nil { + changes = append(changes, l.SetAllowRegister(*value.AllowRegister)) + } + if value.AllowExternalIDP != nil { + changes = append(changes, l.SetAllowExternalIDP(*value.AllowExternalIDP)) + } + if value.ForceMFA != nil { + changes = append(changes, l.SetForceMFA(*value.ForceMFA)) + } + if value.ForceMFALocalOnly != nil { + changes = append(changes, l.SetForceMFALocalOnly(*value.ForceMFALocalOnly)) + } + if value.HidePasswordReset != nil { + changes = append(changes, l.SetHidePasswordReset(*value.HidePasswordReset)) + } + if value.IgnoreUnknownUsernames != nil { + changes = append(changes, l.SetIgnoreUnknownUsernames(*value.IgnoreUnknownUsernames)) + } + if value.AllowDomainDiscovery != nil { + changes = append(changes, l.SetAllowDomainDiscovery(*value.AllowDomainDiscovery)) + } + if value.DisableLoginWithEmail != nil { + changes = append(changes, l.SetDisableLoginWithEmail(*value.DisableLoginWithEmail)) + } + if value.DisableLoginWithPhone != nil { + changes = append(changes, l.SetDisableLoginWithPhone(*value.DisableLoginWithPhone)) + } + if value.PasswordlessType != nil { + changes = append(changes, l.SetPasswordlessType(*value.PasswordlessType)) + } + if value.DefaultRedirectURI != nil { + changes = append(changes, l.SetDefaultRedirectURI(*value.DefaultRedirectURI)) + } + if value.PasswordCheckLifetime != nil { + changes = append(changes, l.SetPasswordCheckLifetime(*value.PasswordCheckLifetime)) + } + if value.ExternalLoginCheckLifetime != nil { + changes = append(changes, l.SetExternalLoginCheckLifetime(*value.ExternalLoginCheckLifetime)) + } + if value.MFAInitSkipLifetime != nil { + changes = append(changes, l.SetMFAInitSkipLifetime(*value.MFAInitSkipLifetime)) + } + if value.SecondFactorCheckLifetime != nil { + changes = append(changes, l.SetSecondFactorCheckLifetime(*value.SecondFactorCheckLifetime)) + } + if value.MultiFactorCheckLifetime != nil { + changes = append(changes, l.SetMultiFactorCheckLifetime(*value.MultiFactorCheckLifetime)) + } + if value.DisableLoginWithEmail != nil { + changes = append(changes, l.SetDisableLoginWithEmail(*value.DisableLoginWithEmail)) + } + if value.MFAType != nil { + changes = append(changes, l.SetMFAType(value.MFAType)) + } + if value.SecondFactorTypes != nil { + changes = append(changes, l.SetSecondFactorTypes(value.SecondFactorTypes)) + } + return db_json.NewJsonChanges(l.SettingsColumn(), changes...) +} + +func (loginSettings) SetAllowUserNamePassword(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"allowUsernamePassword"}, value) +} + +func (loginSettings) SetAllowRegister(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"allowRegister"}, value) +} + +func (loginSettings) SetAllowExternalIDP(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"allowExternalIdp"}, value) +} + +func (loginSettings) SetForceMFA(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"forceMfa"}, value) +} + +func (loginSettings) SetForceMFALocalOnly(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"forceMfaLocalOnly"}, value) +} + +func (loginSettings) SetHidePasswordReset(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"hidePasswordReset"}, value) +} + +func (loginSettings) SetIgnoreUnknownUsernames(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"ignoreUnknownUsernames"}, value) +} + +func (loginSettings) SetAllowDomainDiscovery(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"allowDomainDiscovery"}, value) +} + +func (loginSettings) SetDisableLoginWithEmail(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"disableLoginWithEmail"}, value) +} + +func (loginSettings) SetDisableLoginWithPhone(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"disableLoginWithPhone"}, value) +} + +func (loginSettings) SetPasswordlessType(value domain.PasswordlessType) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"passwordlessType"}, value) +} + +func (loginSettings) SetDefaultRedirectURI(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"defaultRedirectUri"}, value) +} + +func (loginSettings) SetPasswordCheckLifetime(value time.Duration) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"passwordCheckLifetime"}, value) +} + +func (loginSettings) SetExternalLoginCheckLifetime(value time.Duration) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"externalLoginCheckLifetime"}, value) +} + +func (loginSettings) SetMFAInitSkipLifetime(value time.Duration) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"mfaInitSkipLifetime"}, value) +} + +func (loginSettings) SetSecondFactorCheckLifetime(value time.Duration) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"secondFactorCheckLifetime"}, value) +} + +func (loginSettings) SetMultiFactorCheckLifetime(value time.Duration) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"multiFactorCheckLifetime"}, value) +} + +func (loginSettings) SetMFAType(values []domain.MultiFactorType) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"mfaType"}, values) +} + +func (l loginSettings) AddMFAType(value domain.MultiFactorType) database.Change { + return db_json.NewJsonChanges(l.SettingsColumn(), + db_json.NewArrayChange([]string{"mfaType"}, value, false), + ) +} + +func (l loginSettings) RemoveMFAType(value domain.MultiFactorType) database.Change { + return db_json.NewJsonChanges(l.SettingsColumn(), + db_json.NewArrayChange([]string{"mfaType"}, value, true), + ) +} + +func (loginSettings) SetSecondFactorTypes(values []domain.SecondFactorType) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"secondFactors"}, values) +} + +func (l loginSettings) AddSecondFactorTypes(value domain.SecondFactorType) database.Change { + return db_json.NewJsonChanges(l.SettingsColumn(), + db_json.NewArrayChange([]string{"secondFactors"}, value, false), + ) +} + +func (l loginSettings) RemoveSecondFactorTypes(value domain.SecondFactorType) database.Change { + return db_json.NewJsonChanges(l.SettingsColumn(), + db_json.NewArrayChange([]string{"secondFactors"}, value, true), + ) +} + +func LoginSettingsRepository() domain.LoginSettingsRepository { + return &loginSettings{ + settings{}, + } +} + +var _ domain.LoginSettingsRepository = (*loginSettings)(nil) + +func (s loginSettings) Set(ctx context.Context, client database.QueryExecutor, settings *domain.LoginSettings) error { + settings.Type = domain.SettingTypeLogin + settings.State = domain.SettingStateActive + + settingsJSON, err := json.Marshal(settings.LoginSettingsAttributes) + if err != nil { + return err + } + settings.Settings.Settings = settingsJSON + if err := s.set(ctx, client, &settings.Settings, s.SetSettingFields(settings.LoginSettingsAttributes)); err != nil { + return err + } + settings.Settings.Settings = nil + return nil +} + +func (s loginSettings) SetColumns(ctx context.Context, client database.QueryExecutor, settings *domain.Settings, changes ...database.Change) error { + settings.Type = domain.SettingTypeLogin + settings.State = domain.SettingStateActive + + return s.set(ctx, client, settings, changes...) +} + +func (s loginSettings) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.LoginSettings, error) { + settings, err := s.list(ctx, client, opts...) + if err != nil { + return nil, err + } + + list := make([]*domain.LoginSettings, len(settings)) + for i := range settings { + list[i] = &domain.LoginSettings{Settings: *settings[i]} + if err := json.Unmarshal(list[i].Settings.Settings, &list[i].LoginSettingsAttributes); err != nil { + return nil, err + } + list[i].Settings.Settings = nil + } + return list, nil +} + +func (s loginSettings) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.LoginSettings, error) { + settings, err := s.get(ctx, client, opts...) + if err != nil { + return nil, err + } + + loginSettings := &domain.LoginSettings{Settings: *settings} + if err = json.Unmarshal(loginSettings.Settings.Settings, &loginSettings.LoginSettingsAttributes); err != nil { + return nil, err + } + loginSettings.Settings.Settings = nil + return loginSettings, nil +} + +// ------------------------------------------------------------- +// branding +// ------------------------------------------------------------- + +type brandingSettings struct { + settings +} + +func (b brandingSettings) SetSettingFields(value domain.BrandingSettingsAttributes) database.Change { + changes := make([]db_json.JsonUpdate, 0) + if value.PrimaryColorLight != nil { + changes = append(changes, b.SetPrimaryColorLight(*value.PrimaryColorLight)) + } + if value.BackgroundColorLight != nil { + changes = append(changes, b.SetBackgroundColorLight(*value.BackgroundColorLight)) + } + if value.WarnColorLight != nil { + changes = append(changes, b.SetWarnColorLight(*value.WarnColorLight)) + } + if value.FontColorLight != nil { + changes = append(changes, b.SetFontColorLight(*value.FontColorLight)) + } + if value.PrimaryColorDark != nil { + changes = append(changes, b.SetPrimaryColorDark(*value.PrimaryColorDark)) + } + if value.BackgroundColorDark != nil { + changes = append(changes, b.SetBackgroundColorDark(*value.BackgroundColorDark)) + } + if value.WarnColorDark != nil { + changes = append(changes, b.SetWarnColorDark(*value.WarnColorDark)) + } + if value.FontColorDark != nil { + changes = append(changes, b.SetFontColorDark(*value.FontColorDark)) + } + if value.HideLoginNameSuffix != nil { + changes = append(changes, b.SetHideLoginNameSuffix(*value.HideLoginNameSuffix)) + } + if value.ErrorMsgPopup != nil { + changes = append(changes, b.SetErrorMsgPopup(*value.ErrorMsgPopup)) + } + if value.DisableWatermark != nil { + changes = append(changes, b.SetDisableWatermark(*value.DisableWatermark)) + } + if value.ThemeMode != nil { + changes = append(changes, b.SetThemeMode(*value.ThemeMode)) + } + if value.LogoURLLight != nil { + changes = append(changes, b.SetLogoURLLight(*value.LogoURLLight)) + } + if value.LogoURLDark != nil { + changes = append(changes, b.SetLogoURLDark(*value.LogoURLDark)) + } + if value.IconURLLight != nil { + changes = append(changes, b.SetIconURLLight(*value.IconURLLight)) + } + if value.IconURLDark != nil { + changes = append(changes, b.SetIconURLDark(*value.IconURLDark)) + } + if value.FontURL != nil { + changes = append(changes, b.SetFontURL(*value.FontURL)) + } + return db_json.NewJsonChanges(b.SettingsColumn(), changes...) +} + +func (brandingSettings) SetPrimaryColorLight(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"primaryColorLight"}, value) +} + +func (brandingSettings) SetBackgroundColorLight(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"backgroundColorLight"}, value) +} + +func (brandingSettings) SetWarnColorLight(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"warnColorLight"}, value) +} + +func (brandingSettings) SetFontColorLight(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"fontColorLight"}, value) +} + +func (brandingSettings) SetPrimaryColorDark(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"primaryColorDark"}, value) +} + +func (brandingSettings) SetBackgroundColorDark(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"backgroundColorDark"}, value) +} + +func (brandingSettings) SetWarnColorDark(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"warnColorDark"}, value) +} + +func (brandingSettings) SetFontColorDark(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"fontColorDark"}, value) +} + +func (brandingSettings) SetHideLoginNameSuffix(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"hideLoginNameSuffix"}, value) +} + +func (brandingSettings) SetErrorMsgPopup(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"errorMsgPopup"}, value) +} + +func (brandingSettings) SetDisableWatermark(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"disableMsgPopup"}, value) +} + +func (brandingSettings) SetThemeMode(value domain.BrandingPolicyThemeMode) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"themeMode"}, value) +} + +func (brandingSettings) SetLogoURLLight(value url.URL) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"logoURLLight"}, value) +} + +func (brandingSettings) SetLogoURLDark(value url.URL) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"logoUrlDark"}, value) +} + +func (brandingSettings) SetIconURLLight(value url.URL) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"iconUrlLight"}, value) +} + +func (brandingSettings) SetIconURLDark(value url.URL) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"iconUrlDark"}, value) +} + +func (brandingSettings) SetFontURL(value url.URL) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"fontUrl"}, value) +} + +func BrandingSettingsRepository() domain.BrandingSettingsRepository { + return &brandingSettings{ + settings{}, + } +} + +var _ domain.BrandingSettingsRepository = (*brandingSettings)(nil) + +func (s brandingSettings) Set(ctx context.Context, client database.QueryExecutor, settings *domain.BrandingSettings) (err error) { + settings.Type = domain.SettingTypeBranding + settings.State = domain.SettingStatePreview + + settingsJSON, err := json.Marshal(settings.BrandingSettingsAttributes) + if err != nil { + return err + } + settings.Settings.Settings = settingsJSON + if err := s.set(ctx, client, &settings.Settings, s.SetSettingFields(settings.BrandingSettingsAttributes)); err != nil { + return err + } + settings.Settings.Settings = nil + return nil +} + +func (s brandingSettings) SetColumns(ctx context.Context, client database.QueryExecutor, settings *domain.Settings, changes ...database.Change) error { + settings.Type = domain.SettingTypeBranding + settings.State = domain.SettingStatePreview + + return s.set(ctx, client, settings, changes...) +} + +func (s brandingSettings) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.BrandingSettings, error) { + settings, err := s.list(ctx, client, opts...) + if err != nil { + return nil, err + } + + list := make([]*domain.BrandingSettings, len(settings)) + for i := range settings { + list[i] = &domain.BrandingSettings{Settings: *settings[i]} + if err := json.Unmarshal(list[i].Settings.Settings, &list[i].BrandingSettingsAttributes); err != nil { + return nil, err + } + list[i].Settings.Settings = nil + } + return list, nil +} + +func (s brandingSettings) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.BrandingSettings, error) { + settings, err := s.get(ctx, client, opts...) + if err != nil { + return nil, err + } + + brandingSettings := &domain.BrandingSettings{Settings: *settings} + if err = json.Unmarshal(brandingSettings.Settings.Settings, &brandingSettings.BrandingSettingsAttributes); err != nil { + return nil, err + } + brandingSettings.Settings.Settings = nil + return brandingSettings, nil +} + +const queryActivateBrandingSettingsStmt = `SELECT instance_id, organization_id, id, type, created_at, updated_at, settings FROM ` + +func (s brandingSettings) Activate(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) { + return s.activateAt(ctx, client, condition, time.Now()) +} + +func (s brandingSettings) ActivateAt(ctx context.Context, client database.QueryExecutor, condition database.Condition, updatedAt time.Time) (int64, error) { + return s.activateAt(ctx, client, condition, updatedAt) +} + +func (s brandingSettings) activateAt(ctx context.Context, client database.QueryExecutor, condition database.Condition, updatedAt time.Time) (int64, error) { + condition = database.And( + condition, + s.TypeCondition(domain.SettingTypeBranding), + s.StateCondition(domain.SettingStatePreview), + ) + // if you want to update the roles you have to have a unique condition, otherwise multiple branding settings get activated + if err := checkRestrictingColumns(condition, s.UniqueColumns()...); err != nil { + return 0, err + } + + // the statement begins with getting settings in preview state + builder := database.NewStatementBuilder("WITH preview AS (") + builder.WriteString(queryActivateBrandingSettingsStmt + s.qualifiedTableName()) + writeCondition(builder, condition) + builder.WriteString(" ) ") + + // insert or update settings as active + builder.WriteString(`INSERT INTO ` + s.qualifiedTableName()) + builder.WriteString(` (instance_id, organization_id, id, type, state, created_at, updated_at, settings)`) + + builder.WriteString(` SELECT preview.instance_id, preview.organization_id, preview.id, preview.type, `) + builder.WriteArg(domain.SettingStateActive) + builder.WriteString(`, preview.created_at, `) + builder.WriteArg(updatedAt) + builder.WriteString(`, preview.settings FROM preview`) + + builder.WriteString(` ON CONFLICT (instance_id, organization_id, type, state) DO UPDATE SET settings = EXCLUDED.settings, updated_at = `) + builder.WriteArg(updatedAt) + + return client.Exec(ctx, builder.String(), builder.Args()...) +} + +// ------------------------------------------------------------- +// password complexity +// ------------------------------------------------------------- +type passwordComplexitySettings struct { + settings +} + +func (s passwordComplexitySettings) SetSettingFields(value domain.PasswordComplexitySettingsAttributes) database.Change { + changes := make([]db_json.JsonUpdate, 0) + if value.MinLength != nil { + changes = append(changes, s.SetMinLength(*value.MinLength)) + } + if value.HasLowercase != nil { + changes = append(changes, s.SetHasLowercase(*value.HasLowercase)) + } + if value.HasUppercase != nil { + changes = append(changes, s.SetHasUppercase(*value.HasUppercase)) + } + if value.HasNumber != nil { + changes = append(changes, s.SetHasNumber(*value.HasNumber)) + } + if value.HasSymbol != nil { + changes = append(changes, s.SetHasSymbol(*value.HasSymbol)) + } + return db_json.NewJsonChanges(s.SettingsColumn(), changes...) +} + +func (passwordComplexitySettings) SetMinLength(value uint64) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"minLength"}, value) +} + +func (passwordComplexitySettings) SetHasLowercase(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"hasLowercase"}, value) +} + +func (passwordComplexitySettings) SetHasUppercase(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"hasUppercase"}, value) +} + +func (passwordComplexitySettings) SetHasNumber(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"hasNumber"}, value) +} + +func (passwordComplexitySettings) SetHasSymbol(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"hasSymbol"}, value) +} + +func PasswordComplexitySettingsRepository() domain.PasswordComplexitySettingsRepository { + return &passwordComplexitySettings{ + settings{}, + } +} + +var _ domain.PasswordComplexitySettingsRepository = (*passwordComplexitySettings)(nil) + +func (s passwordComplexitySettings) Set(ctx context.Context, client database.QueryExecutor, settings *domain.PasswordComplexitySettings) (err error) { + settings.Type = domain.SettingTypePasswordComplexity + settings.State = domain.SettingStateActive + + settingsJSON, err := json.Marshal(settings.PasswordComplexitySettingsAttributes) + if err != nil { + return err + } + settings.Settings.Settings = settingsJSON + if err := s.set(ctx, client, &settings.Settings, s.SetSettingFields(settings.PasswordComplexitySettingsAttributes)); err != nil { + return err + } + settings.Settings.Settings = nil + return nil +} + +func (s passwordComplexitySettings) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.PasswordComplexitySettings, error) { + settings, err := s.list(ctx, client, opts...) + if err != nil { + return nil, err + } + + list := make([]*domain.PasswordComplexitySettings, len(settings)) + for i := range settings { + list[i] = &domain.PasswordComplexitySettings{Settings: *settings[i]} + if err := json.Unmarshal(list[i].Settings.Settings, &list[i].PasswordComplexitySettingsAttributes); err != nil { + return nil, err + } + list[i].Settings.Settings = nil + } + return list, nil +} + +func (s passwordComplexitySettings) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.PasswordComplexitySettings, error) { + settings, err := s.get(ctx, client, opts...) + if err != nil { + return nil, err + } + + item := &domain.PasswordComplexitySettings{Settings: *settings} + if err := json.Unmarshal(item.Settings.Settings, &item.PasswordComplexitySettingsAttributes); err != nil { + return nil, err + } + item.Settings.Settings = nil + return item, nil +} + +// ------------------------------------------------------------- +// password expiry +// ------------------------------------------------------------- + +type passwordExpirySettings struct { + settings +} + +func (s passwordExpirySettings) SetSettingFields(value domain.PasswordExpirySettingsAttributes) database.Change { + changes := make([]db_json.JsonUpdate, 0) + if value.ExpireWarnDays != nil { + changes = append(changes, s.SetExpireWarnDays(*value.ExpireWarnDays)) + } + if value.MaxAgeDays != nil { + changes = append(changes, s.SetMaxAgeDays(*value.MaxAgeDays)) + } + return db_json.NewJsonChanges(s.SettingsColumn(), changes...) +} + +func (passwordExpirySettings) SetExpireWarnDays(value uint64) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"expireWarnDays"}, value) +} + +func (passwordExpirySettings) SetMaxAgeDays(value uint64) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"maxAgeDays"}, value) +} + +var _ domain.PasswordExpirySettingsRepository = (*passwordExpirySettings)(nil) + +func PasswordExpirySettingsRepository() domain.PasswordExpirySettingsRepository { + return &passwordExpirySettings{ + settings{}, + } +} + +func (s passwordExpirySettings) Set(ctx context.Context, client database.QueryExecutor, settings *domain.PasswordExpirySettings) (err error) { + settings.Type = domain.SettingTypePasswordExpiry + settings.State = domain.SettingStateActive + + settingsJSON, err := json.Marshal(settings.PasswordExpirySettingsAttributes) + if err != nil { + return err + } + settings.Settings.Settings = settingsJSON + if err := s.set(ctx, client, &settings.Settings, s.SetSettingFields(settings.PasswordExpirySettingsAttributes)); err != nil { + return err + } + settings.Settings.Settings = nil + return nil +} + +func (s passwordExpirySettings) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.PasswordExpirySettings, error) { + settings, err := s.list(ctx, client, opts...) + if err != nil { + return nil, err + } + + list := make([]*domain.PasswordExpirySettings, len(settings)) + for i := range settings { + list[i] = &domain.PasswordExpirySettings{Settings: *settings[i]} + if err := json.Unmarshal(list[i].Settings.Settings, &list[i].PasswordExpirySettingsAttributes); err != nil { + return nil, err + } + list[i].Settings.Settings = nil + } + return list, nil +} + +func (s passwordExpirySettings) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.PasswordExpirySettings, error) { + settings, err := s.get(ctx, client, opts...) + if err != nil { + return nil, err + } + + item := &domain.PasswordExpirySettings{Settings: *settings} + if err = json.Unmarshal(item.Settings.Settings, &item.PasswordExpirySettingsAttributes); err != nil { + return nil, err + } + item.Settings.Settings = nil + return item, nil +} + +// ------------------------------------------------------------- +// lockout +// ------------------------------------------------------------- +type lockoutSettings struct { + settings +} + +func (s lockoutSettings) SetSettingFields(value domain.LockoutSettingsAttributes) database.Change { + changes := make([]db_json.JsonUpdate, 0) + if value.MaxPasswordAttempts != nil { + changes = append(changes, s.SetMaxPasswordAttempts(*value.MaxPasswordAttempts)) + } + if value.MaxOTPAttempts != nil { + changes = append(changes, s.SetMaxOTPAttempts(*value.MaxOTPAttempts)) + } + if value.ShowLockOutFailures != nil { + changes = append(changes, s.SetShowLockOutFailures(*value.ShowLockOutFailures)) + } + return db_json.NewJsonChanges(s.SettingsColumn(), changes...) +} + +func (lockoutSettings) SetMaxPasswordAttempts(value uint64) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"maxPasswordAttempts"}, value) +} + +func (lockoutSettings) SetMaxOTPAttempts(value uint64) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"maxOtpAttempts"}, value) +} + +func (lockoutSettings) SetShowLockOutFailures(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"showLockOutFailures"}, value) +} + +func LockoutSettingsRepository() domain.LockoutSettingsRepository { + return &lockoutSettings{ + settings{}, + } +} + +var _ domain.LockoutSettingsRepository = (*lockoutSettings)(nil) + +func (s lockoutSettings) Set(ctx context.Context, client database.QueryExecutor, settings *domain.LockoutSettings) error { + settings.Type = domain.SettingTypeLockout + settings.State = domain.SettingStateActive + + settingsJSON, err := json.Marshal(settings.LockoutSettingsAttributes) + if err != nil { + return err + } + settings.Settings.Settings = settingsJSON + if err := s.set(ctx, client, &settings.Settings, s.SetSettingFields(settings.LockoutSettingsAttributes)); err != nil { + return err + } + settings.Settings.Settings = nil + return nil +} + +func (s lockoutSettings) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.LockoutSettings, error) { + settings, err := s.list(ctx, client, opts...) + if err != nil { + return nil, err + } + + list := make([]*domain.LockoutSettings, len(settings)) + for i := range settings { + list[i] = &domain.LockoutSettings{Settings: *settings[i]} + if err := json.Unmarshal(list[i].Settings.Settings, &list[i].LockoutSettingsAttributes); err != nil { + return nil, err + } + list[i].Settings.Settings = nil + } + return list, nil +} + +func (s lockoutSettings) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.LockoutSettings, error) { + settings, err := s.get(ctx, client, opts...) + if err != nil { + return nil, err + } + + item := &domain.LockoutSettings{Settings: *settings} + if err = json.Unmarshal(item.Settings.Settings, &item.LockoutSettingsAttributes); err != nil { + return nil, err + } + item.Settings.Settings = nil + return item, nil +} + +// ------------------------------------------------------------- +// security +// ------------------------------------------------------------- +type securitySettings struct { + settings +} + +func (s securitySettings) SetSettingFields(value domain.SecuritySettingsAttributes) database.Change { + changes := make([]db_json.JsonUpdate, 0) + if value.EnableIframeEmbedding != nil { + changes = append(changes, s.SetEnableIframeEmbedding(*value.EnableIframeEmbedding)) + } + if value.AllowedOrigins != nil { + changes = append(changes, s.SetAllowedOrigins(value.AllowedOrigins)) + } + if value.EnableImpersonation != nil { + changes = append(changes, s.SetEnableImpersonation(*value.EnableImpersonation)) + } + return db_json.NewJsonChanges(s.SettingsColumn(), changes...) +} + +func (securitySettings) SetEnableIframeEmbedding(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"enableIframeEmbedding"}, value) +} + +func (securitySettings) SetAllowedOrigins(value []string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"allowedOrigins"}, value) +} + +func (securitySettings) SetEnableImpersonation(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"enableImpersonation"}, value) +} + +func SecuritySettingsRepository() domain.SecuritySettingsRepository { + return &securitySettings{ + settings{}, + } +} + +var _ domain.SecuritySettingsRepository = (*securitySettings)(nil) + +func (s securitySettings) Set(ctx context.Context, client database.QueryExecutor, settings *domain.SecuritySettings) (err error) { + settings.Type = domain.SettingTypeSecurity + settings.State = domain.SettingStateActive + + settingsJSON, err := json.Marshal(settings.SecuritySettingsAttributes) + if err != nil { + return err + } + settings.Settings.Settings = settingsJSON + if err := s.set(ctx, client, &settings.Settings, s.SetSettingFields(settings.SecuritySettingsAttributes)); err != nil { + return err + } + settings.Settings.Settings = nil + return nil +} + +func (s securitySettings) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.SecuritySettings, error) { + settings, err := s.list(ctx, client, opts...) + if err != nil { + return nil, err + } + + list := make([]*domain.SecuritySettings, len(settings)) + for i := range settings { + list[i] = &domain.SecuritySettings{Settings: *settings[i]} + if err := json.Unmarshal(list[i].Settings.Settings, &list[i].SecuritySettingsAttributes); err != nil { + return nil, err + } + list[i].Settings.Settings = nil + } + return list, nil +} + +func (s securitySettings) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.SecuritySettings, error) { + settings, err := s.get(ctx, client, opts...) + if err != nil { + return nil, err + } + + item := &domain.SecuritySettings{Settings: *settings} + if err = json.Unmarshal(item.Settings.Settings, &item.SecuritySettingsAttributes); err != nil { + return nil, err + } + item.Settings.Settings = nil + return item, nil +} + +// ------------------------------------------------------------- +// domain +// ------------------------------------------------------------- +type domainSettings struct { + settings +} + +func (s domainSettings) SetSettingFields(value domain.DomainSettingsAttributes) database.Change { + changes := make([]db_json.JsonUpdate, 0) + if value.LoginNameIncludesDomain != nil { + changes = append(changes, s.SetLoginNameIncludesDomain(*value.LoginNameIncludesDomain)) + } + if value.RequireOrgDomainVerification != nil { + changes = append(changes, s.SetRequireOrgDomainVerification(*value.RequireOrgDomainVerification)) + } + if value.SMTPSenderAddressMatchesInstanceDomain != nil { + changes = append(changes, s.SetSMTPSenderAddressMatchesInstanceDomain(*value.SMTPSenderAddressMatchesInstanceDomain)) + } + return db_json.NewJsonChanges(s.SettingsColumn(), changes...) +} + +func (domainSettings) SetLoginNameIncludesDomain(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"loginNameIncludesDomain"}, value) +} + +func (domainSettings) SetRequireOrgDomainVerification(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"requireOrgDomainVerification"}, value) +} + +func (domainSettings) SetSMTPSenderAddressMatchesInstanceDomain(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"smtpSenderAddressMatchesInstanceDomain"}, value) +} + +func DomainSettingsRepository() domain.DomainSettingsRepository { + return &domainSettings{ + settings{}, + } +} + +var _ domain.DomainSettingsRepository = (*domainSettings)(nil) + +func (s domainSettings) Set(ctx context.Context, client database.QueryExecutor, settings *domain.DomainSettings) (err error) { + settings.Type = domain.SettingTypeDomain + settings.State = domain.SettingStateActive + + settingsJSON, err := json.Marshal(settings.DomainSettingsAttributes) + if err != nil { + return err + } + settings.Settings.Settings = settingsJSON + if err := s.set(ctx, client, &settings.Settings, s.SetSettingFields(settings.DomainSettingsAttributes)); err != nil { + return err + } + settings.Settings.Settings = nil + return nil +} + +func (s domainSettings) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.DomainSettings, error) { + settings, err := s.list(ctx, client, opts...) + if err != nil { + return nil, err + } + + list := make([]*domain.DomainSettings, len(settings)) + for i := range settings { + list[i] = &domain.DomainSettings{Settings: *settings[i]} + if err := json.Unmarshal(list[i].Settings.Settings, &list[i].DomainSettingsAttributes); err != nil { + return nil, err + } + list[i].Settings.Settings = nil + } + return list, nil +} + +func (s domainSettings) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.DomainSettings, error) { + settings, err := s.get(ctx, client, opts...) + if err != nil { + return nil, err + } + + item := &domain.DomainSettings{Settings: *settings} + if err = json.Unmarshal(item.Settings.Settings, &item.DomainSettingsAttributes); err != nil { + return nil, err + } + item.Settings.Settings = nil + return item, nil +} + +// ------------------------------------------------------------- +// organization +// ------------------------------------------------------------- +type organizationSettings struct { + settings +} + +func (s organizationSettings) SetSettingFields(value domain.OrganizationSettingsAttributes) database.Change { + changes := make([]db_json.JsonUpdate, 0) + if value.OrganizationScopedUsernames != nil { + changes = append(changes, s.SetOrganizationScopedUsernames(*value.OrganizationScopedUsernames)) + } + return db_json.NewJsonChanges(s.SettingsColumn(), changes...) +} + +func (l organizationSettings) SetOrganizationScopedUsernames(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"organizationScopedUsernames"}, value) +} + +func OrganizationSettingsRepository() domain.OrganizationSettingsRepository { + return &organizationSettings{ + settings{}, + } +} + +var _ domain.OrganizationSettingsRepository = (*organizationSettings)(nil) + +func (s organizationSettings) Set(ctx context.Context, client database.QueryExecutor, settings *domain.OrganizationSettings) (err error) { + settings.Type = domain.SettingTypeOrganization + settings.State = domain.SettingStateActive + + settingsJSON, err := json.Marshal(settings.OrganizationSettingsAttributes) + if err != nil { + return err + } + settings.Settings.Settings = settingsJSON + if err := s.set(ctx, client, &settings.Settings, s.SetSettingFields(settings.OrganizationSettingsAttributes)); err != nil { + return err + } + settings.Settings.Settings = nil + return nil +} + +func (s organizationSettings) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.OrganizationSettings, error) { + settings, err := s.list(ctx, client, opts...) + if err != nil { + return nil, err + } + + list := make([]*domain.OrganizationSettings, len(settings)) + for i := range settings { + list[i] = &domain.OrganizationSettings{Settings: *settings[i]} + if err := json.Unmarshal(list[i].Settings.Settings, &list[i].OrganizationSettingsAttributes); err != nil { + return nil, err + } + list[i].Settings.Settings = nil + } + return list, nil +} + +func (s organizationSettings) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.OrganizationSettings, error) { + settings, err := s.get(ctx, client, opts...) + if err != nil { + return nil, err + } + + item := &domain.OrganizationSettings{Settings: *settings} + if err = json.Unmarshal(item.Settings.Settings, &item.OrganizationSettingsAttributes); err != nil { + return nil, err + } + item.Settings.Settings = nil + return item, nil +} + +// ------------------------------------------------------------- +// notification +// ------------------------------------------------------------- +type notificationSettings struct { + settings +} + +func (s notificationSettings) SetSettingFields(value domain.NotificationSettingsAttributes) database.Change { + changes := make([]db_json.JsonUpdate, 0) + if value.PasswordChange != nil { + changes = append(changes, s.SetPasswordChange(*value.PasswordChange)) + } + return db_json.NewJsonChanges(s.SettingsColumn(), changes...) +} + +func (s notificationSettings) SetPasswordChange(value bool) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"passwordChange"}, value) +} + +func NotificationSettingsRepository() domain.NotificationSettingsRepository { + return ¬ificationSettings{ + settings{}, + } +} + +var _ domain.NotificationSettingsRepository = (*notificationSettings)(nil) + +func (s notificationSettings) Set(ctx context.Context, client database.QueryExecutor, settings *domain.NotificationSettings) (err error) { + settings.Type = domain.SettingTypeNotification + settings.State = domain.SettingStateActive + + settingsJSON, err := json.Marshal(settings.NotificationSettingsAttributes) + if err != nil { + return err + } + settings.Settings.Settings = settingsJSON + if err := s.set(ctx, client, &settings.Settings, s.SetSettingFields(settings.NotificationSettingsAttributes)); err != nil { + return err + } + settings.Settings.Settings = nil + return nil +} + +func (s notificationSettings) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.NotificationSettings, error) { + settings, err := s.list(ctx, client, opts...) + if err != nil { + return nil, err + } + + list := make([]*domain.NotificationSettings, len(settings)) + for i := range settings { + list[i] = &domain.NotificationSettings{Settings: *settings[i]} + if err := json.Unmarshal(list[i].Settings.Settings, &list[i].NotificationSettingsAttributes); err != nil { + return nil, err + } + list[i].Settings.Settings = nil + } + return list, nil +} + +func (s notificationSettings) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.NotificationSettings, error) { + settings, err := s.get(ctx, client, opts...) + if err != nil { + return nil, err + } + + item := &domain.NotificationSettings{Settings: *settings} + if err = json.Unmarshal(item.Settings.Settings, &item.NotificationSettingsAttributes); err != nil { + return nil, err + } + item.Settings.Settings = nil + return item, nil +} + +// ------------------------------------------------------------- +// legal and support +// ------------------------------------------------------------- +type legalAndSupportSettings struct { + settings +} + +func (s legalAndSupportSettings) SetSettingFields(value domain.LegalAndSupportSettingsAttributes) database.Change { + changes := make([]db_json.JsonUpdate, 0) + if value.TOSLink != nil { + changes = append(changes, s.SetTOSLink(*value.TOSLink)) + } + if value.PrivacyPolicyLink != nil { + changes = append(changes, s.SetPrivacyPolicyLink(*value.PrivacyPolicyLink)) + } + if value.HelpLink != nil { + changes = append(changes, s.SetHelpLink(*value.HelpLink)) + } + if value.SupportEmail != nil { + changes = append(changes, s.SetSupportEmail(*value.SupportEmail)) + } + if value.DocsLink != nil { + changes = append(changes, s.SetDocsLink(*value.DocsLink)) + } + if value.CustomLink != nil { + changes = append(changes, s.SetCustomLink(*value.CustomLink)) + } + if value.CustomLinkText != nil { + changes = append(changes, s.SetCustomLinkText(*value.CustomLinkText)) + } + return db_json.NewJsonChanges(s.SettingsColumn(), changes...) +} + +func (s legalAndSupportSettings) SetTOSLink(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"tosLink"}, value) +} + +func (s legalAndSupportSettings) SetPrivacyPolicyLink(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"privacyPolicyLink"}, value) +} + +func (s legalAndSupportSettings) SetHelpLink(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"helpLink"}, value) +} + +func (s legalAndSupportSettings) SetSupportEmail(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"supportEmail"}, value) +} + +func (s legalAndSupportSettings) SetDocsLink(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"docsLink"}, value) +} + +func (s legalAndSupportSettings) SetCustomLink(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"customLink"}, value) +} + +func (s legalAndSupportSettings) SetCustomLinkText(value string) db_json.JsonUpdate { + return db_json.NewFieldChange([]string{"customLinkText"}, value) +} + +func LegalAndSupportSettingsRepository() domain.LegalAndSupportSettingsRepository { + return &legalAndSupportSettings{ + settings{}, + } +} + +var _ domain.LegalAndSupportSettingsRepository = (*legalAndSupportSettings)(nil) + +func (s legalAndSupportSettings) Set(ctx context.Context, client database.QueryExecutor, settings *domain.LegalAndSupportSettings) (err error) { + settings.Type = domain.SettingTypeLegalAndSupport + settings.State = domain.SettingStateActive + + settingsJSON, err := json.Marshal(settings.LegalAndSupportSettingsAttributes) + if err != nil { + return err + } + settings.Settings.Settings = settingsJSON + if err := s.set(ctx, client, &settings.Settings, s.SetSettingFields(settings.LegalAndSupportSettingsAttributes)); err != nil { + return err + } + settings.Settings.Settings = nil + return nil +} + +func (s legalAndSupportSettings) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.LegalAndSupportSettings, error) { + settings, err := s.list(ctx, client, opts...) + if err != nil { + return nil, err + } + + list := make([]*domain.LegalAndSupportSettings, len(settings)) + for i := range settings { + list[i] = &domain.LegalAndSupportSettings{Settings: *settings[i]} + if err := json.Unmarshal(list[i].Settings.Settings, &list[i].LegalAndSupportSettingsAttributes); err != nil { + return nil, err + } + list[i].Settings.Settings = nil + } + return list, nil +} + +func (s legalAndSupportSettings) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.LegalAndSupportSettings, error) { + settings, err := s.get(ctx, client, opts...) + if err != nil { + return nil, err + } + + item := &domain.LegalAndSupportSettings{Settings: *settings} + if err = json.Unmarshal(item.Settings.Settings, &item.LegalAndSupportSettingsAttributes); err != nil { + return nil, err + } + item.Settings.Settings = nil + return item, nil +} + +// ------------------------------------------------------------- +// helpers +// ------------------------------------------------------------- + +const querySettingsStmt = `SELECT + settings.instance_id, + settings.organization_id, + settings.id, + settings.type, + settings.state, + settings.settings, + settings.created_at, + settings.updated_at + FROM ` + +func (s settings) prepareQuery(opts []database.QueryOption) (*database.StatementBuilder, error) { + options := new(database.QueryOpts) + for _, opt := range opts { + opt(options) + } + if err := checkRestrictingColumns(options.Condition, s.InstanceIDColumn()); err != nil { + return nil, err + } + builder := database.NewStatementBuilder(querySettingsStmt + s.qualifiedTableName()) + options.Write(builder) + + return builder, nil +} diff --git a/backend/v3/storage/database/repository/settings_test.go b/backend/v3/storage/database/repository/settings_test.go new file mode 100644 index 000000000..c8af17090 --- /dev/null +++ b/backend/v3/storage/database/repository/settings_test.go @@ -0,0 +1,4756 @@ +package repository_test + +import ( + "net/url" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" +) + +func TestGetLoginSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.LoginSettingsRepository() + + settings := []*domain.LoginSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(true), + AllowRegister: gu.Ptr(true), + AllowExternalIDP: gu.Ptr(true), + ForceMFA: gu.Ptr(true), + ForceMFALocalOnly: gu.Ptr(true), + HidePasswordReset: gu.Ptr(true), + IgnoreUnknownUsernames: gu.Ptr(true), + AllowDomainDiscovery: gu.Ptr(true), + DisableLoginWithEmail: gu.Ptr(true), + DisableLoginWithPhone: gu.Ptr(true), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeAllowed), + DefaultRedirectURI: gu.Ptr("uri"), + PasswordCheckLifetime: gu.Ptr(time.Hour), + ExternalLoginCheckLifetime: gu.Ptr(time.Hour), + MFAInitSkipLifetime: gu.Ptr(time.Hour), + SecondFactorCheckLifetime: gu.Ptr(time.Hour), + MultiFactorCheckLifetime: gu.Ptr(time.Hour), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(false), + AllowRegister: gu.Ptr(false), + AllowExternalIDP: gu.Ptr(false), + ForceMFA: gu.Ptr(false), + ForceMFALocalOnly: gu.Ptr(false), + HidePasswordReset: gu.Ptr(false), + IgnoreUnknownUsernames: gu.Ptr(false), + AllowDomainDiscovery: gu.Ptr(false), + DisableLoginWithEmail: gu.Ptr(false), + DisableLoginWithPhone: gu.Ptr(false), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeNotAllowed), + DefaultRedirectURI: gu.Ptr("uri2"), + PasswordCheckLifetime: gu.Ptr(time.Minute), + ExternalLoginCheckLifetime: gu.Ptr(time.Minute), + MFAInitSkipLifetime: gu.Ptr(time.Minute), + SecondFactorCheckLifetime: gu.Ptr(time.Minute), + MultiFactorCheckLifetime: gu.Ptr(time.Minute), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(true), + AllowRegister: gu.Ptr(true), + AllowExternalIDP: gu.Ptr(true), + ForceMFA: gu.Ptr(true), + ForceMFALocalOnly: gu.Ptr(true), + HidePasswordReset: gu.Ptr(true), + IgnoreUnknownUsernames: gu.Ptr(true), + AllowDomainDiscovery: gu.Ptr(true), + DisableLoginWithEmail: gu.Ptr(true), + DisableLoginWithPhone: gu.Ptr(true), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeAllowed), + DefaultRedirectURI: gu.Ptr("uri"), + PasswordCheckLifetime: gu.Ptr(time.Hour), + ExternalLoginCheckLifetime: gu.Ptr(time.Hour), + MFAInitSkipLifetime: gu.Ptr(time.Hour), + SecondFactorCheckLifetime: gu.Ptr(time.Hour), + MultiFactorCheckLifetime: gu.Ptr(time.Hour), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(false), + AllowRegister: gu.Ptr(false), + AllowExternalIDP: gu.Ptr(false), + ForceMFA: gu.Ptr(false), + ForceMFALocalOnly: gu.Ptr(false), + HidePasswordReset: gu.Ptr(false), + IgnoreUnknownUsernames: gu.Ptr(false), + AllowDomainDiscovery: gu.Ptr(false), + DisableLoginWithEmail: gu.Ptr(false), + DisableLoginWithPhone: gu.Ptr(false), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeNotAllowed), + DefaultRedirectURI: gu.Ptr("uri2"), + PasswordCheckLifetime: gu.Ptr(time.Minute), + ExternalLoginCheckLifetime: gu.Ptr(time.Minute), + MFAInitSkipLifetime: gu.Ptr(time.Minute), + SecondFactorCheckLifetime: gu.Ptr(time.Minute), + MultiFactorCheckLifetime: gu.Ptr(time.Minute), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want *domain.LoginSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.IDCondition(settings[0].ID), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + wantErr: database.NewNoRowFoundError(nil), + }, + { + name: "too many", + condition: repo.InstanceIDCondition(firstInstanceID), + wantErr: database.NewMultipleRowsFoundError(nil), + }, + { + name: "ok, instance", + condition: repo.PrimaryKeyCondition(firstInstanceID, settings[0].ID), + want: settings[0], + }, + { + name: "ok, organization", + condition: repo.PrimaryKeyCondition(secondInstanceID, settings[3].ID), + want: settings[3], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.Get(t.Context(), tx, database.WithCondition(tt.condition)) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestListLoginSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.LoginSettingsRepository() + + settings := []*domain.LoginSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(true), + AllowRegister: gu.Ptr(true), + AllowExternalIDP: gu.Ptr(true), + ForceMFA: gu.Ptr(true), + ForceMFALocalOnly: gu.Ptr(true), + HidePasswordReset: gu.Ptr(true), + IgnoreUnknownUsernames: gu.Ptr(true), + AllowDomainDiscovery: gu.Ptr(true), + DisableLoginWithEmail: gu.Ptr(true), + DisableLoginWithPhone: gu.Ptr(true), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeAllowed), + DefaultRedirectURI: gu.Ptr("uri"), + PasswordCheckLifetime: gu.Ptr(time.Hour), + ExternalLoginCheckLifetime: gu.Ptr(time.Hour), + MFAInitSkipLifetime: gu.Ptr(time.Hour), + SecondFactorCheckLifetime: gu.Ptr(time.Hour), + MultiFactorCheckLifetime: gu.Ptr(time.Hour), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(false), + AllowRegister: gu.Ptr(false), + AllowExternalIDP: gu.Ptr(false), + ForceMFA: gu.Ptr(false), + ForceMFALocalOnly: gu.Ptr(false), + HidePasswordReset: gu.Ptr(false), + IgnoreUnknownUsernames: gu.Ptr(false), + AllowDomainDiscovery: gu.Ptr(false), + DisableLoginWithEmail: gu.Ptr(false), + DisableLoginWithPhone: gu.Ptr(false), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeNotAllowed), + DefaultRedirectURI: gu.Ptr("uri2"), + PasswordCheckLifetime: gu.Ptr(time.Minute), + ExternalLoginCheckLifetime: gu.Ptr(time.Minute), + MFAInitSkipLifetime: gu.Ptr(time.Minute), + SecondFactorCheckLifetime: gu.Ptr(time.Minute), + MultiFactorCheckLifetime: gu.Ptr(time.Minute), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(true), + AllowRegister: gu.Ptr(true), + AllowExternalIDP: gu.Ptr(true), + ForceMFA: gu.Ptr(true), + ForceMFALocalOnly: gu.Ptr(true), + HidePasswordReset: gu.Ptr(true), + IgnoreUnknownUsernames: gu.Ptr(true), + AllowDomainDiscovery: gu.Ptr(true), + DisableLoginWithEmail: gu.Ptr(true), + DisableLoginWithPhone: gu.Ptr(true), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeAllowed), + DefaultRedirectURI: gu.Ptr("uri"), + PasswordCheckLifetime: gu.Ptr(time.Hour), + ExternalLoginCheckLifetime: gu.Ptr(time.Hour), + MFAInitSkipLifetime: gu.Ptr(time.Hour), + SecondFactorCheckLifetime: gu.Ptr(time.Hour), + MultiFactorCheckLifetime: gu.Ptr(time.Hour), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(false), + AllowRegister: gu.Ptr(false), + AllowExternalIDP: gu.Ptr(false), + ForceMFA: gu.Ptr(false), + ForceMFALocalOnly: gu.Ptr(false), + HidePasswordReset: gu.Ptr(false), + IgnoreUnknownUsernames: gu.Ptr(false), + AllowDomainDiscovery: gu.Ptr(false), + DisableLoginWithEmail: gu.Ptr(false), + DisableLoginWithPhone: gu.Ptr(false), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeNotAllowed), + DefaultRedirectURI: gu.Ptr("uri2"), + PasswordCheckLifetime: gu.Ptr(time.Minute), + ExternalLoginCheckLifetime: gu.Ptr(time.Minute), + MFAInitSkipLifetime: gu.Ptr(time.Minute), + SecondFactorCheckLifetime: gu.Ptr(time.Minute), + MultiFactorCheckLifetime: gu.Ptr(time.Minute), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want []*domain.LoginSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "no results, ok", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + want: []*domain.LoginSettings{}, + }, + { + name: "all from instance", + condition: repo.InstanceIDCondition(firstInstanceID), + want: []*domain.LoginSettings{settings[2], settings[0]}, + }, + { + name: "only from instance", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(nil), + ), + want: []*domain.LoginSettings{settings[0]}, + }, + { + name: "all from first org", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + ), + want: []*domain.LoginSettings{settings[2]}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.List(t.Context(), tx, + database.WithCondition(tt.condition), + database.WithOrderByAscending(repo.InstanceIDColumn(), repo.OrganizationIDColumn()), + ) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestSetLoginSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.LoginSettingsRepository() + + existingSettings := &domain.LoginSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(false), + AllowRegister: gu.Ptr(false), + AllowExternalIDP: gu.Ptr(false), + ForceMFA: gu.Ptr(false), + ForceMFALocalOnly: gu.Ptr(false), + HidePasswordReset: gu.Ptr(false), + IgnoreUnknownUsernames: gu.Ptr(false), + AllowDomainDiscovery: gu.Ptr(false), + DisableLoginWithEmail: gu.Ptr(false), + DisableLoginWithPhone: gu.Ptr(false), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeNotAllowed), + DefaultRedirectURI: gu.Ptr("uri2"), + PasswordCheckLifetime: gu.Ptr(time.Minute), + ExternalLoginCheckLifetime: gu.Ptr(time.Minute), + MFAInitSkipLifetime: gu.Ptr(time.Minute), + SecondFactorCheckLifetime: gu.Ptr(time.Minute), + MultiFactorCheckLifetime: gu.Ptr(time.Minute), + }, + } + + err := repo.Set(t.Context(), tx, existingSettings) + require.NoError(t, err) + + tests := []struct { + name string + settings *domain.LoginSettings + wantErr error + }{ + { + name: "create instance", + settings: &domain.LoginSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(true), + AllowRegister: gu.Ptr(true), + AllowExternalIDP: gu.Ptr(true), + ForceMFA: gu.Ptr(true), + ForceMFALocalOnly: gu.Ptr(true), + HidePasswordReset: gu.Ptr(true), + IgnoreUnknownUsernames: gu.Ptr(true), + AllowDomainDiscovery: gu.Ptr(true), + DisableLoginWithEmail: gu.Ptr(true), + DisableLoginWithPhone: gu.Ptr(true), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeAllowed), + DefaultRedirectURI: gu.Ptr("uri"), + PasswordCheckLifetime: gu.Ptr(time.Hour), + ExternalLoginCheckLifetime: gu.Ptr(time.Hour), + MFAInitSkipLifetime: gu.Ptr(time.Hour), + SecondFactorCheckLifetime: gu.Ptr(time.Hour), + MultiFactorCheckLifetime: gu.Ptr(time.Hour), + }, + }, + }, + { + name: "update organization", + settings: &domain.LoginSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(true), + AllowRegister: gu.Ptr(true), + AllowExternalIDP: gu.Ptr(true), + ForceMFA: gu.Ptr(true), + ForceMFALocalOnly: gu.Ptr(true), + HidePasswordReset: gu.Ptr(true), + IgnoreUnknownUsernames: gu.Ptr(true), + AllowDomainDiscovery: gu.Ptr(true), + DisableLoginWithEmail: gu.Ptr(true), + DisableLoginWithPhone: gu.Ptr(true), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeAllowed), + DefaultRedirectURI: gu.Ptr("uri"), + PasswordCheckLifetime: gu.Ptr(time.Hour), + ExternalLoginCheckLifetime: gu.Ptr(time.Hour), + MFAInitSkipLifetime: gu.Ptr(time.Hour), + SecondFactorCheckLifetime: gu.Ptr(time.Hour), + MultiFactorCheckLifetime: gu.Ptr(time.Hour), + }, + }, + }, + { + name: "non-existing instance", + settings: &domain.LoginSettings{ + Settings: domain.Settings{ + InstanceID: "foo", + OrganizationID: nil, + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(true), + AllowRegister: gu.Ptr(true), + AllowExternalIDP: gu.Ptr(true), + ForceMFA: gu.Ptr(true), + ForceMFALocalOnly: gu.Ptr(true), + HidePasswordReset: gu.Ptr(true), + IgnoreUnknownUsernames: gu.Ptr(true), + AllowDomainDiscovery: gu.Ptr(true), + DisableLoginWithEmail: gu.Ptr(true), + DisableLoginWithPhone: gu.Ptr(true), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeAllowed), + DefaultRedirectURI: gu.Ptr("uri"), + PasswordCheckLifetime: gu.Ptr(time.Hour), + ExternalLoginCheckLifetime: gu.Ptr(time.Hour), + MFAInitSkipLifetime: gu.Ptr(time.Hour), + SecondFactorCheckLifetime: gu.Ptr(time.Hour), + MultiFactorCheckLifetime: gu.Ptr(time.Hour), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existing org", + settings: &domain.LoginSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr("foo"), + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(true), + AllowRegister: gu.Ptr(true), + AllowExternalIDP: gu.Ptr(true), + ForceMFA: gu.Ptr(true), + ForceMFALocalOnly: gu.Ptr(true), + HidePasswordReset: gu.Ptr(true), + IgnoreUnknownUsernames: gu.Ptr(true), + AllowDomainDiscovery: gu.Ptr(true), + DisableLoginWithEmail: gu.Ptr(true), + DisableLoginWithPhone: gu.Ptr(true), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeAllowed), + DefaultRedirectURI: gu.Ptr("uri"), + PasswordCheckLifetime: gu.Ptr(time.Hour), + ExternalLoginCheckLifetime: gu.Ptr(time.Hour), + MFAInitSkipLifetime: gu.Ptr(time.Hour), + SecondFactorCheckLifetime: gu.Ptr(time.Hour), + MultiFactorCheckLifetime: gu.Ptr(time.Hour), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + err := repo.Set(t.Context(), savepoint, tt.settings) + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestDeleteLoginSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.LoginSettingsRepository() + + existingInstanceSettings := &domain.LoginSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(false), + AllowRegister: gu.Ptr(false), + AllowExternalIDP: gu.Ptr(false), + ForceMFA: gu.Ptr(false), + ForceMFALocalOnly: gu.Ptr(false), + HidePasswordReset: gu.Ptr(false), + IgnoreUnknownUsernames: gu.Ptr(false), + AllowDomainDiscovery: gu.Ptr(false), + DisableLoginWithEmail: gu.Ptr(false), + DisableLoginWithPhone: gu.Ptr(false), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeNotAllowed), + DefaultRedirectURI: gu.Ptr("uri"), + PasswordCheckLifetime: gu.Ptr(time.Minute), + ExternalLoginCheckLifetime: gu.Ptr(time.Minute), + MFAInitSkipLifetime: gu.Ptr(time.Minute), + SecondFactorCheckLifetime: gu.Ptr(time.Minute), + MultiFactorCheckLifetime: gu.Ptr(time.Minute), + }, + } + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + existingOrganizationSettings := &domain.LoginSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: gu.Ptr(false), + AllowRegister: gu.Ptr(false), + AllowExternalIDP: gu.Ptr(false), + ForceMFA: gu.Ptr(false), + ForceMFALocalOnly: gu.Ptr(false), + HidePasswordReset: gu.Ptr(false), + IgnoreUnknownUsernames: gu.Ptr(false), + AllowDomainDiscovery: gu.Ptr(false), + DisableLoginWithEmail: gu.Ptr(false), + DisableLoginWithPhone: gu.Ptr(false), + PasswordlessType: gu.Ptr(domain.PasswordlessTypeNotAllowed), + DefaultRedirectURI: gu.Ptr("uri"), + PasswordCheckLifetime: gu.Ptr(time.Minute), + ExternalLoginCheckLifetime: gu.Ptr(time.Minute), + MFAInitSkipLifetime: gu.Ptr(time.Minute), + SecondFactorCheckLifetime: gu.Ptr(time.Minute), + MultiFactorCheckLifetime: gu.Ptr(time.Minute), + }, + } + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.InstanceIDCondition(instanceID), + wantRowsAffected: 0, + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.UniqueCondition(instanceID, gu.Ptr("foo"), domain.SettingTypeLogin, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete instance", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeLogin, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete instance twice", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeLogin, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete organization", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeLogin, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete organization twice", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeLogin, domain.SettingStateActive), + wantRowsAffected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rowsAffected, err := repo.Delete(t.Context(), tx, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} + +func TestGetBrandingSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.BrandingSettingsRepository() + + settings := []*domain.BrandingSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + _, err := repo.Activate(t.Context(), tx, repo.UniqueCondition(settings[0].InstanceID, settings[0].OrganizationID, domain.SettingTypeBranding, domain.SettingStatePreview)) + require.NoError(t, err) + activeInstanceSetting := *settings[0] + activeInstanceSetting.State = domain.SettingStateActive + + _, err = repo.Activate(t.Context(), tx, repo.UniqueCondition(settings[3].InstanceID, settings[3].OrganizationID, domain.SettingTypeBranding, domain.SettingStatePreview)) + require.NoError(t, err) + activeOrganizationSetting := *settings[3] + activeOrganizationSetting.State = domain.SettingStateActive + + tests := []struct { + name string + condition database.Condition + want *domain.BrandingSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.IDCondition(settings[0].ID), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + wantErr: database.NewNoRowFoundError(nil), + }, + { + name: "too many", + condition: repo.InstanceIDCondition(firstInstanceID), + wantErr: database.NewMultipleRowsFoundError(nil), + }, + { + name: "ok, instance", + condition: database.And( + repo.PrimaryKeyCondition(settings[0].InstanceID, settings[0].ID), + repo.StateCondition(domain.SettingStatePreview), + ), + want: settings[0], + }, + { + name: "ok, instance, active", + condition: database.And( + repo.PrimaryKeyCondition(activeInstanceSetting.InstanceID, activeInstanceSetting.ID), + repo.StateCondition(domain.SettingStateActive), + ), + want: &activeInstanceSetting, + }, + { + name: "ok, organization", + condition: database.And( + repo.PrimaryKeyCondition(settings[3].InstanceID, settings[3].ID), + repo.StateCondition(domain.SettingStatePreview), + ), + want: settings[3], + }, + { + name: "ok, organization", + condition: database.And( + repo.PrimaryKeyCondition(activeOrganizationSetting.InstanceID, activeOrganizationSetting.ID), + repo.StateCondition(domain.SettingStateActive), + ), + want: &activeOrganizationSetting, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.Get(t.Context(), tx, database.WithCondition(tt.condition)) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestListBrandingSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.BrandingSettingsRepository() + + settings := []*domain.BrandingSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + _, err := repo.Activate(t.Context(), tx, repo.UniqueCondition(settings[0].InstanceID, settings[0].OrganizationID, domain.SettingTypeBranding, domain.SettingStatePreview)) + require.NoError(t, err) + activeInstanceSetting := *settings[0] + activeInstanceSetting.State = domain.SettingStateActive + + _, err = repo.Activate(t.Context(), tx, repo.UniqueCondition(settings[2].InstanceID, settings[2].OrganizationID, domain.SettingTypeBranding, domain.SettingStatePreview)) + require.NoError(t, err) + activeOrganizationSetting := *settings[2] + activeOrganizationSetting.State = domain.SettingStateActive + + tests := []struct { + name string + condition database.Condition + want []*domain.BrandingSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "no results, ok", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + want: []*domain.BrandingSettings{}, + }, + { + name: "all from instance", + condition: repo.InstanceIDCondition(firstInstanceID), + want: []*domain.BrandingSettings{&activeOrganizationSetting, settings[2], &activeInstanceSetting, settings[0]}, + }, + { + name: "all from instance, preview", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.StateCondition(domain.SettingStatePreview), + ), + want: []*domain.BrandingSettings{settings[2], settings[0]}, + }, + { + name: "all from instance, active", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.StateCondition(domain.SettingStateActive), + ), + want: []*domain.BrandingSettings{&activeOrganizationSetting, &activeInstanceSetting}, + }, + { + name: "only from instance", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(nil), + ), + want: []*domain.BrandingSettings{&activeInstanceSetting, settings[0]}, + }, + { + name: "all from first org", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + ), + want: []*domain.BrandingSettings{&activeOrganizationSetting, settings[2]}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.List(t.Context(), tx, + database.WithCondition(tt.condition), + database.WithOrderByAscending(repo.InstanceIDColumn(), repo.OrganizationIDColumn(), repo.StateColumn()), + ) + require.ErrorIs(t, err, tt.wantErr) + + if !assert.Len(t, got, len(tt.want)) { + return + } + for i := range got { + assert.EqualExportedValues(t, tt.want[i], got[i]) + } + }) + } +} + +func TestSetBrandingSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.BrandingSettingsRepository() + + existingSettings := &domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + } + + err := repo.Set(t.Context(), tx, existingSettings) + require.NoError(t, err) + + tests := []struct { + name string + settings *domain.BrandingSettings + wantErr error + }{ + { + name: "create instance", + settings: &domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + }, + { + name: "update organization", + settings: &domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + }, + { + name: "non-existing instance", + settings: &domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: "foo", + OrganizationID: nil, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existing org", + settings: &domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr("foo"), + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + }, + wantErr: new(database.ForeignKeyError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + err := repo.Set(t.Context(), savepoint, tt.settings) + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestActivateBrandingSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.BrandingSettingsRepository() + + existingInstanceSettings := &domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + } + + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + existingOrganizationSettings := &domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + } + + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + tests := []struct { + name string + wantRowsAffected int64 + condition database.Condition + wantErr error + }{ + { + name: "activate instance", + condition: database.And( + repo.InstanceIDCondition(existingInstanceSettings.InstanceID), + repo.OrganizationIDCondition(nil), + ), + wantRowsAffected: 1, + }, + { + name: "activate organization", + condition: database.And( + repo.InstanceIDCondition(existingOrganizationSettings.InstanceID), + repo.OrganizationIDCondition(existingOrganizationSettings.OrganizationID), + ), + wantRowsAffected: 1, + }, + { + name: "non-existing instance", + condition: database.And( + repo.InstanceIDCondition("foo"), + repo.OrganizationIDCondition(nil), + ), + wantRowsAffected: 0, + }, + + { + name: "non-existing organization", + condition: database.And( + repo.InstanceIDCondition(existingOrganizationSettings.InstanceID), + repo.OrganizationIDCondition(gu.Ptr("foo")), + ), + wantRowsAffected: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + rowsAffected, err := repo.Activate(t.Context(), savepoint, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} + +func TestActivateAtBrandingSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.BrandingSettingsRepository() + + existingInstanceSettings := &domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + } + + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + existingOrganizationSettings := &domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + } + + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + tests := []struct { + name string + wantRowsAffected int64 + condition database.Condition + updatedAt time.Time + wantErr error + }{ + { + name: "activate instance", + condition: database.And( + repo.InstanceIDCondition(existingInstanceSettings.InstanceID), + repo.OrganizationIDCondition(nil), + ), + updatedAt: time.Now(), + wantRowsAffected: 1, + }, + { + name: "activate organization", + condition: database.And( + repo.InstanceIDCondition(existingOrganizationSettings.InstanceID), + repo.OrganizationIDCondition(existingOrganizationSettings.OrganizationID), + ), + updatedAt: time.Now(), + wantRowsAffected: 1, + }, + { + name: "non-existing instance", + condition: database.And( + repo.InstanceIDCondition("foo"), + repo.OrganizationIDCondition(nil), + ), + updatedAt: time.Now(), + wantRowsAffected: 0, + }, + + { + name: "non-existing organization", + condition: database.And( + repo.InstanceIDCondition(existingOrganizationSettings.InstanceID), + repo.OrganizationIDCondition(gu.Ptr("foo")), + ), + updatedAt: time.Now(), + wantRowsAffected: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + rowsAffected, err := repo.ActivateAt(t.Context(), savepoint, tt.condition, tt.updatedAt) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} + +func TestDeleteBrandingSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.BrandingSettingsRepository() + + existingInstanceSettings := &domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + } + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + _, err = repo.Activate(t.Context(), tx, + database.And( + repo.InstanceIDCondition(existingInstanceSettings.InstanceID), + repo.OrganizationIDCondition(existingInstanceSettings.OrganizationID), + ), + ) + require.NoError(t, err) + activeInstanceSetting := *existingInstanceSettings + activeInstanceSetting.State = domain.SettingStateActive + + existingOrganizationSettings := &domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: gu.Ptr("color"), + BackgroundColorLight: gu.Ptr("color"), + WarnColorLight: gu.Ptr("color"), + FontColorLight: gu.Ptr("color"), + LogoURLLight: &url.URL{Scheme: "https", Host: "host"}, + IconURLLight: &url.URL{Scheme: "https", Host: "host"}, + PrimaryColorDark: gu.Ptr("color"), + BackgroundColorDark: gu.Ptr("color"), + WarnColorDark: gu.Ptr("color"), + FontColorDark: gu.Ptr("color"), + LogoURLDark: &url.URL{Scheme: "https", Host: "host"}, + IconURLDark: &url.URL{Scheme: "https", Host: "host"}, + HideLoginNameSuffix: gu.Ptr(true), + ErrorMsgPopup: gu.Ptr(true), + DisableWatermark: gu.Ptr(true), + ThemeMode: gu.Ptr(domain.BrandingPolicyThemeAuto), + FontURL: &url.URL{Scheme: "https", Host: "host"}, + }, + } + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + _, err = repo.Activate(t.Context(), tx, + database.And( + repo.InstanceIDCondition(existingOrganizationSettings.InstanceID), + repo.OrganizationIDCondition(existingOrganizationSettings.OrganizationID), + ), + ) + require.NoError(t, err) + activeOrganizationSetting := *existingOrganizationSettings + activeOrganizationSetting.State = domain.SettingStateActive + + tests := []struct { + name string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.InstanceIDCondition(instanceID), + wantRowsAffected: 0, + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, gu.Ptr("foo"), domain.SettingTypeBranding, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete instance, active", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeBranding, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete instance", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeBranding, domain.SettingStatePreview), + wantRowsAffected: 1, + }, + { + name: "delete instance twice", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeBranding, domain.SettingStatePreview), + wantRowsAffected: 0, + }, + { + name: "delete organization, active", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeBranding, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete organization twice, active", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeBranding, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete organization", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeBranding, domain.SettingStatePreview), + wantRowsAffected: 1, + }, + { + name: "delete organization twice", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeBranding, domain.SettingStatePreview), + wantRowsAffected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rowsAffected, err := repo.Delete(t.Context(), tx, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} + +func TestGetPasswordComplexitySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.PasswordComplexitySettingsRepository() + + settings := []*domain.PasswordComplexitySettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(8)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(10)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(12)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(14)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want *domain.PasswordComplexitySettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.IDCondition(settings[0].ID), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + wantErr: database.NewNoRowFoundError(nil), + }, + { + name: "too many", + condition: repo.InstanceIDCondition(firstInstanceID), + wantErr: database.NewMultipleRowsFoundError(nil), + }, + { + name: "ok, instance", + condition: repo.PrimaryKeyCondition(firstInstanceID, settings[0].ID), + want: settings[0], + }, + { + name: "ok, organization", + condition: repo.PrimaryKeyCondition(secondInstanceID, settings[3].ID), + want: settings[3], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.Get(t.Context(), tx, database.WithCondition(tt.condition)) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestListPasswordComplexitySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.PasswordComplexitySettingsRepository() + + settings := []*domain.PasswordComplexitySettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(8)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(10)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(10)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(12)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want []*domain.PasswordComplexitySettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "no results, ok", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + want: []*domain.PasswordComplexitySettings{}, + }, + { + name: "all from instance", + condition: repo.InstanceIDCondition(firstInstanceID), + want: []*domain.PasswordComplexitySettings{settings[2], settings[0]}, + }, + { + name: "only from instance", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(nil), + ), + want: []*domain.PasswordComplexitySettings{settings[0]}, + }, + { + name: "all from first org", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + ), + want: []*domain.PasswordComplexitySettings{settings[2]}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.List(t.Context(), tx, + database.WithCondition(tt.condition), + database.WithOrderByAscending(repo.InstanceIDColumn(), repo.OrganizationIDColumn()), + ) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestSetPasswordComplexitySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.PasswordComplexitySettingsRepository() + + existingSettings := &domain.PasswordComplexitySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(8)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + } + + err := repo.Set(t.Context(), tx, existingSettings) + require.NoError(t, err) + + tests := []struct { + name string + settings *domain.PasswordComplexitySettings + wantErr error + }{ + { + name: "create instance", + settings: &domain.PasswordComplexitySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(8)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + }, + { + name: "update organization", + settings: &domain.PasswordComplexitySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(10)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + }, + { + name: "non-existing instance", + settings: &domain.PasswordComplexitySettings{ + Settings: domain.Settings{ + InstanceID: "foo", + OrganizationID: nil, + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(8)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existing org", + settings: &domain.PasswordComplexitySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr("foo"), + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(8)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + err := repo.Set(t.Context(), savepoint, tt.settings) + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestDeletePasswordComplexitySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.PasswordComplexitySettingsRepository() + + existingInstanceSettings := &domain.PasswordComplexitySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(8)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + } + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + existingOrganizationSettings := &domain.PasswordComplexitySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: gu.Ptr(uint64(8)), + HasLowercase: gu.Ptr(true), + HasUppercase: gu.Ptr(true), + HasNumber: gu.Ptr(true), + HasSymbol: gu.Ptr(true), + }, + } + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.InstanceIDCondition(instanceID), + wantRowsAffected: 0, + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, gu.Ptr("foo"), domain.SettingTypePasswordComplexity, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete instance", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete instance twice", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete organization", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete organization twice", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + wantRowsAffected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rowsAffected, err := repo.Delete(t.Context(), tx, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} + +func TestGetPasswordExpirySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.PasswordExpirySettingsRepository() + + settings := []*domain.PasswordExpirySettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(10)), + MaxAgeDays: gu.Ptr(uint64(30)), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(11)), + MaxAgeDays: gu.Ptr(uint64(31)), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(12)), + MaxAgeDays: gu.Ptr(uint64(32)), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(13)), + MaxAgeDays: gu.Ptr(uint64(33)), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want *domain.PasswordExpirySettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.IDCondition(settings[0].ID), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + wantErr: database.NewNoRowFoundError(nil), + }, + { + name: "too many", + condition: repo.InstanceIDCondition(firstInstanceID), + wantErr: database.NewMultipleRowsFoundError(nil), + }, + { + name: "ok, instance", + condition: repo.PrimaryKeyCondition(firstInstanceID, settings[0].ID), + want: settings[0], + }, + { + name: "ok, organization", + condition: repo.PrimaryKeyCondition(secondInstanceID, settings[3].ID), + want: settings[3], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.Get(t.Context(), tx, database.WithCondition(tt.condition)) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestListPasswordExpirySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.PasswordExpirySettingsRepository() + + settings := []*domain.PasswordExpirySettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(10)), + MaxAgeDays: gu.Ptr(uint64(30)), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(11)), + MaxAgeDays: gu.Ptr(uint64(31)), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(12)), + MaxAgeDays: gu.Ptr(uint64(32)), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(13)), + MaxAgeDays: gu.Ptr(uint64(33)), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want []*domain.PasswordExpirySettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "no results, ok", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + want: []*domain.PasswordExpirySettings{}, + }, + { + name: "all from instance", + condition: repo.InstanceIDCondition(firstInstanceID), + want: []*domain.PasswordExpirySettings{settings[2], settings[0]}, + }, + { + name: "only from instance", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(nil), + ), + want: []*domain.PasswordExpirySettings{settings[0]}, + }, + { + name: "all from first org", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + ), + want: []*domain.PasswordExpirySettings{settings[2]}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.List(t.Context(), tx, + database.WithCondition(tt.condition), + database.WithOrderByAscending(repo.InstanceIDColumn(), repo.OrganizationIDColumn()), + ) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestSetPasswordExpirySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.PasswordExpirySettingsRepository() + + existingSettings := &domain.PasswordExpirySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(10)), + MaxAgeDays: gu.Ptr(uint64(30)), + }, + } + + err := repo.Set(t.Context(), tx, existingSettings) + require.NoError(t, err) + + tests := []struct { + name string + settings *domain.PasswordExpirySettings + wantErr error + }{ + { + name: "create instance", + settings: &domain.PasswordExpirySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(10)), + MaxAgeDays: gu.Ptr(uint64(30)), + }, + }, + }, + { + name: "update organization", + settings: &domain.PasswordExpirySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(10)), + MaxAgeDays: gu.Ptr(uint64(30)), + }, + }, + }, + { + name: "non-existing instance", + settings: &domain.PasswordExpirySettings{ + Settings: domain.Settings{ + InstanceID: "foo", + OrganizationID: nil, + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(10)), + MaxAgeDays: gu.Ptr(uint64(30)), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existing org", + settings: &domain.PasswordExpirySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr("foo"), + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(10)), + MaxAgeDays: gu.Ptr(uint64(30)), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + err := repo.Set(t.Context(), savepoint, tt.settings) + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestDeletePasswordExpirySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.PasswordExpirySettingsRepository() + + existingInstanceSettings := &domain.PasswordExpirySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(10)), + MaxAgeDays: gu.Ptr(uint64(30)), + }, + } + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + existingOrganizationSettings := &domain.PasswordExpirySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: gu.Ptr(uint64(10)), + MaxAgeDays: gu.Ptr(uint64(30)), + }, + } + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.InstanceIDCondition(instanceID), + wantRowsAffected: 0, + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, gu.Ptr("foo"), domain.SettingTypePasswordExpiry, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete instance", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete instance twice", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete organization", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete organization twice", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + wantRowsAffected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rowsAffected, err := repo.Delete(t.Context(), tx, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} + +func TestGetLockoutSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.LockoutSettingsRepository() + + settings := []*domain.LockoutSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(3)), + MaxOTPAttempts: gu.Ptr(uint64(3)), + ShowLockOutFailures: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(4)), + MaxOTPAttempts: gu.Ptr(uint64(4)), + ShowLockOutFailures: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(5)), + MaxOTPAttempts: gu.Ptr(uint64(5)), + ShowLockOutFailures: gu.Ptr(false), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(6)), + MaxOTPAttempts: gu.Ptr(uint64(6)), + ShowLockOutFailures: gu.Ptr(true), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want *domain.LockoutSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.IDCondition(settings[0].ID), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + wantErr: database.NewNoRowFoundError(nil), + }, + { + name: "too many", + condition: repo.InstanceIDCondition(firstInstanceID), + wantErr: database.NewMultipleRowsFoundError(nil), + }, + { + name: "ok, instance", + condition: repo.PrimaryKeyCondition(firstInstanceID, settings[0].ID), + want: settings[0], + }, + { + name: "ok, organization", + condition: repo.PrimaryKeyCondition(secondInstanceID, settings[3].ID), + want: settings[3], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.Get(t.Context(), tx, database.WithCondition(tt.condition)) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestListLockoutSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.LockoutSettingsRepository() + + settings := []*domain.LockoutSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(3)), + MaxOTPAttempts: gu.Ptr(uint64(3)), + ShowLockOutFailures: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(4)), + MaxOTPAttempts: gu.Ptr(uint64(4)), + ShowLockOutFailures: gu.Ptr(false), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(5)), + MaxOTPAttempts: gu.Ptr(uint64(5)), + ShowLockOutFailures: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(6)), + MaxOTPAttempts: gu.Ptr(uint64(6)), + ShowLockOutFailures: gu.Ptr(true), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want []*domain.LockoutSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "no results, ok", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + want: []*domain.LockoutSettings{}, + }, + { + name: "all from instance", + condition: repo.InstanceIDCondition(firstInstanceID), + want: []*domain.LockoutSettings{settings[2], settings[0]}, + }, + { + name: "only from instance", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(nil), + ), + want: []*domain.LockoutSettings{settings[0]}, + }, + { + name: "all from first org", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + ), + want: []*domain.LockoutSettings{settings[2]}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.List(t.Context(), tx, + database.WithCondition(tt.condition), + database.WithOrderByAscending(repo.InstanceIDColumn(), repo.OrganizationIDColumn()), + ) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestSetLockoutSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.LockoutSettingsRepository() + + existingSettings := &domain.LockoutSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(3)), + MaxOTPAttempts: gu.Ptr(uint64(3)), + ShowLockOutFailures: gu.Ptr(true), + }, + } + + err := repo.Set(t.Context(), tx, existingSettings) + require.NoError(t, err) + + tests := []struct { + name string + settings *domain.LockoutSettings + wantErr error + }{ + { + name: "create instance", + settings: &domain.LockoutSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(4)), + MaxOTPAttempts: gu.Ptr(uint64(4)), + ShowLockOutFailures: gu.Ptr(true), + }, + }, + }, + { + name: "update organization", + settings: &domain.LockoutSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(5)), + MaxOTPAttempts: gu.Ptr(uint64(5)), + ShowLockOutFailures: gu.Ptr(true), + }, + }, + }, + { + name: "non-existing instance", + settings: &domain.LockoutSettings{ + Settings: domain.Settings{ + InstanceID: "foo", + OrganizationID: nil, + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(3)), + MaxOTPAttempts: gu.Ptr(uint64(3)), + ShowLockOutFailures: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existing org", + settings: &domain.LockoutSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr("foo"), + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(3)), + MaxOTPAttempts: gu.Ptr(uint64(3)), + ShowLockOutFailures: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + err := repo.Set(t.Context(), savepoint, tt.settings) + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestDeleteLockoutSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.LockoutSettingsRepository() + + existingInstanceSettings := &domain.LockoutSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(3)), + MaxOTPAttempts: gu.Ptr(uint64(3)), + ShowLockOutFailures: gu.Ptr(true), + }, + } + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + existingOrganizationSettings := &domain.LockoutSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: gu.Ptr(uint64(4)), + MaxOTPAttempts: gu.Ptr(uint64(4)), + ShowLockOutFailures: gu.Ptr(true), + }, + } + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.InstanceIDCondition(instanceID), + wantRowsAffected: 0, + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, gu.Ptr("foo"), domain.SettingTypeLockout, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete instance", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeLockout, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete instance twice", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeLockout, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete organization", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeLockout, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete organization twice", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeLockout, domain.SettingStateActive), + wantRowsAffected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rowsAffected, err := repo.Delete(t.Context(), tx, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} + +func TestGetSecuritySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.SecuritySettingsRepository() + + settings := []*domain.SecuritySettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin1", "origin2"}, + EnableImpersonation: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin3", "origin4"}, + EnableImpersonation: gu.Ptr(false), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin5", "origin6"}, + EnableImpersonation: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(false), + AllowedOrigins: []string{"origin7", "origin8"}, + EnableImpersonation: gu.Ptr(true), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want *domain.SecuritySettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.IDCondition(settings[0].ID), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + wantErr: database.NewNoRowFoundError(nil), + }, + { + name: "too many", + condition: repo.InstanceIDCondition(firstInstanceID), + wantErr: database.NewMultipleRowsFoundError(nil), + }, + { + name: "ok, instance", + condition: repo.PrimaryKeyCondition(firstInstanceID, settings[0].ID), + want: settings[0], + }, + { + name: "ok, organization", + condition: repo.PrimaryKeyCondition(secondInstanceID, settings[3].ID), + want: settings[3], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.Get(t.Context(), tx, database.WithCondition(tt.condition)) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestListSecuritySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.SecuritySettingsRepository() + + settings := []*domain.SecuritySettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin1", "origin2"}, + EnableImpersonation: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin3", "origin4"}, + EnableImpersonation: gu.Ptr(false), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin5", "origin6"}, + EnableImpersonation: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(false), + AllowedOrigins: []string{"origin7", "origin8"}, + EnableImpersonation: gu.Ptr(true), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want []*domain.SecuritySettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "no results, ok", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + want: []*domain.SecuritySettings{}, + }, + { + name: "all from instance", + condition: repo.InstanceIDCondition(firstInstanceID), + want: []*domain.SecuritySettings{settings[2], settings[0]}, + }, + { + name: "only from instance", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(nil), + ), + want: []*domain.SecuritySettings{settings[0]}, + }, + { + name: "all from first org", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + ), + want: []*domain.SecuritySettings{settings[2]}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.List(t.Context(), tx, + database.WithCondition(tt.condition), + database.WithOrderByAscending(repo.InstanceIDColumn(), repo.OrganizationIDColumn()), + ) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestSetSecuritySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.SecuritySettingsRepository() + + existingSettings := &domain.SecuritySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin1", "origin2"}, + EnableImpersonation: gu.Ptr(true), + }, + } + + err := repo.Set(t.Context(), tx, existingSettings) + require.NoError(t, err) + + tests := []struct { + name string + settings *domain.SecuritySettings + wantErr error + }{ + { + name: "create instance", + settings: &domain.SecuritySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin1", "origin2"}, + EnableImpersonation: gu.Ptr(true), + }, + }, + }, + { + name: "update organization", + settings: &domain.SecuritySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin1", "origin2"}, + EnableImpersonation: gu.Ptr(true), + }, + }, + }, + { + name: "non-existing instance", + settings: &domain.SecuritySettings{ + Settings: domain.Settings{ + InstanceID: "foo", + OrganizationID: nil, + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin1", "origin2"}, + EnableImpersonation: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existing org", + settings: &domain.SecuritySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr("foo"), + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin1", "origin2"}, + EnableImpersonation: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + err := repo.Set(t.Context(), savepoint, tt.settings) + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestDeleteSecuritySettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.SecuritySettingsRepository() + + existingInstanceSettings := &domain.SecuritySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin1", "origin2"}, + EnableImpersonation: gu.Ptr(true), + }, + } + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + existingOrganizationSettings := &domain.SecuritySettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: gu.Ptr(true), + AllowedOrigins: []string{"origin3", "origin4"}, + EnableImpersonation: gu.Ptr(true), + }, + } + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.InstanceIDCondition(instanceID), + wantRowsAffected: 0, + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, gu.Ptr("foo"), domain.SettingTypeSecurity, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete instance", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeSecurity, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete instance twice", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeSecurity, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete organization", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeSecurity, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete organization twice", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeSecurity, domain.SettingStateActive), + wantRowsAffected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rowsAffected, err := repo.Delete(t.Context(), tx, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} + +func TestGetDomainSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.DomainSettingsRepository() + + settings := []*domain.DomainSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(false), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(false), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(false), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want *domain.DomainSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.IDCondition(settings[0].ID), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + wantErr: database.NewNoRowFoundError(nil), + }, + { + name: "too many", + condition: repo.InstanceIDCondition(firstInstanceID), + wantErr: database.NewMultipleRowsFoundError(nil), + }, + { + name: "ok, instance", + condition: repo.PrimaryKeyCondition(firstInstanceID, settings[0].ID), + want: settings[0], + }, + { + name: "ok, organization", + condition: repo.PrimaryKeyCondition(secondInstanceID, settings[3].ID), + want: settings[3], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.Get(t.Context(), tx, database.WithCondition(tt.condition)) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestListDomainSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.DomainSettingsRepository() + + settings := []*domain.DomainSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(false), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(false), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(false), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want []*domain.DomainSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "no results, ok", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + want: []*domain.DomainSettings{}, + }, + { + name: "all from instance", + condition: repo.InstanceIDCondition(firstInstanceID), + want: []*domain.DomainSettings{settings[2], settings[0]}, + }, + { + name: "only from instance", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(nil), + ), + want: []*domain.DomainSettings{settings[0]}, + }, + { + name: "all from first org", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + ), + want: []*domain.DomainSettings{settings[2]}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.List(t.Context(), tx, + database.WithCondition(tt.condition), + database.WithOrderByAscending(repo.InstanceIDColumn(), repo.OrganizationIDColumn()), + ) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestSetDomainSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.DomainSettingsRepository() + + existingSettings := &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + } + + err := repo.Set(t.Context(), tx, existingSettings) + require.NoError(t, err) + + tests := []struct { + name string + settings *domain.DomainSettings + wantErr error + }{ + { + name: "create instance", + settings: &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + }, + }, + { + name: "update organization", + settings: &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + }, + }, + { + name: "non-existing instance", + settings: &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: "foo", + OrganizationID: nil, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existing org", + settings: &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr("foo"), + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + err := repo.Set(t.Context(), savepoint, tt.settings) + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestDeleteDomainSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.DomainSettingsRepository() + + existingInstanceSettings := &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(true), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + } + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + existingOrganizationSettings := &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(false), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(true), + }, + } + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.InstanceIDCondition(instanceID), + wantRowsAffected: 0, + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, gu.Ptr("foo"), domain.SettingTypeDomain, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete instance", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeDomain, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete instance twice", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeDomain, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete organization", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeDomain, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete organization twice", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeDomain, domain.SettingStateActive), + wantRowsAffected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rowsAffected, err := repo.Delete(t.Context(), tx, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} + +func TestGetOrganizationSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.OrganizationSettingsRepository() + + settings := []*domain.OrganizationSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want *domain.OrganizationSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.IDCondition(settings[0].ID), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + wantErr: database.NewNoRowFoundError(nil), + }, + { + name: "too many", + condition: repo.InstanceIDCondition(firstInstanceID), + wantErr: database.NewMultipleRowsFoundError(nil), + }, + { + name: "ok, instance", + condition: repo.PrimaryKeyCondition(firstInstanceID, settings[0].ID), + want: settings[0], + }, + { + name: "ok, organization", + condition: repo.PrimaryKeyCondition(secondInstanceID, settings[3].ID), + want: settings[3], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.Get(t.Context(), tx, database.WithCondition(tt.condition)) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestListOrganizationSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.OrganizationSettingsRepository() + + settings := []*domain.OrganizationSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want []*domain.OrganizationSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "no results, ok", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + want: []*domain.OrganizationSettings{}, + }, + { + name: "all from instance", + condition: repo.InstanceIDCondition(firstInstanceID), + want: []*domain.OrganizationSettings{settings[2], settings[0]}, + }, + { + name: "only from instance", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(nil), + ), + want: []*domain.OrganizationSettings{settings[0]}, + }, + { + name: "all from first org", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + ), + want: []*domain.OrganizationSettings{settings[2]}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.List(t.Context(), tx, + database.WithCondition(tt.condition), + database.WithOrderByAscending(repo.InstanceIDColumn(), repo.OrganizationIDColumn()), + ) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestSetOrganizationSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.OrganizationSettingsRepository() + + existingSettings := &domain.OrganizationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + } + + err := repo.Set(t.Context(), tx, existingSettings) + require.NoError(t, err) + + tests := []struct { + name string + settings *domain.OrganizationSettings + wantErr error + }{ + { + name: "create instance", + settings: &domain.OrganizationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + }, + { + name: "update organization", + settings: &domain.OrganizationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + }, + { + name: "non-existing instance", + settings: &domain.OrganizationSettings{ + Settings: domain.Settings{ + InstanceID: "foo", + OrganizationID: nil, + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existing org", + settings: &domain.OrganizationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr("foo"), + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + err := repo.Set(t.Context(), savepoint, tt.settings) + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestDeleteOrganizationSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.OrganizationSettingsRepository() + + existingInstanceSettings := &domain.OrganizationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + } + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + existingOrganizationSettings := &domain.OrganizationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + } + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.InstanceIDCondition(instanceID), + wantRowsAffected: 0, + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, gu.Ptr("foo"), domain.SettingTypeOrganization, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete instance", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeOrganization, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete instance twice", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeOrganization, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete organization", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeOrganization, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete organization twice", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeOrganization, domain.SettingStateActive), + wantRowsAffected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rowsAffected, err := repo.Delete(t.Context(), tx, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} + +func TestGetNotificationSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.NotificationSettingsRepository() + + settings := []*domain.NotificationSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want *domain.NotificationSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.IDCondition(settings[0].ID), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + wantErr: database.NewNoRowFoundError(nil), + }, + { + name: "too many", + condition: repo.InstanceIDCondition(firstInstanceID), + wantErr: database.NewMultipleRowsFoundError(nil), + }, + { + name: "ok, instance", + condition: repo.PrimaryKeyCondition(firstInstanceID, settings[0].ID), + want: settings[0], + }, + { + name: "ok, organization", + condition: repo.PrimaryKeyCondition(secondInstanceID, settings[3].ID), + want: settings[3], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.Get(t.Context(), tx, database.WithCondition(tt.condition)) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestListNotificationSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.NotificationSettingsRepository() + + settings := []*domain.NotificationSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want []*domain.NotificationSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "no results, ok", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + want: []*domain.NotificationSettings{}, + }, + { + name: "all from instance", + condition: repo.InstanceIDCondition(firstInstanceID), + want: []*domain.NotificationSettings{settings[2], settings[0]}, + }, + { + name: "only from instance", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(nil), + ), + want: []*domain.NotificationSettings{settings[0]}, + }, + { + name: "all from first org", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + ), + want: []*domain.NotificationSettings{settings[2]}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.List(t.Context(), tx, + database.WithCondition(tt.condition), + database.WithOrderByAscending(repo.InstanceIDColumn(), repo.OrganizationIDColumn()), + ) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestSetNotificationSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.NotificationSettingsRepository() + + existingSettings := &domain.NotificationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + } + + err := repo.Set(t.Context(), tx, existingSettings) + require.NoError(t, err) + + tests := []struct { + name string + settings *domain.NotificationSettings + wantErr error + }{ + { + name: "create instance", + settings: &domain.NotificationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + }, + { + name: "update organization", + settings: &domain.NotificationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + }, + { + name: "non-existing instance", + settings: &domain.NotificationSettings{ + Settings: domain.Settings{ + InstanceID: "foo", + OrganizationID: nil, + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existing org", + settings: &domain.NotificationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr("foo"), + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + err := repo.Set(t.Context(), savepoint, tt.settings) + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestDeleteNotificationSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.NotificationSettingsRepository() + + existingInstanceSettings := &domain.NotificationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + } + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + existingOrganizationSettings := &domain.NotificationSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: gu.Ptr(true), + }, + } + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.InstanceIDCondition(instanceID), + wantRowsAffected: 0, + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, gu.Ptr("foo"), domain.SettingTypeNotification, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete instance", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeNotification, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete instance twice", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeNotification, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete organization", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeNotification, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete organization twice", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeNotification, domain.SettingStateActive), + wantRowsAffected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rowsAffected, err := repo.Delete(t.Context(), tx, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} + +func TestGetLegalAndSupportSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.LegalAndSupportSettingsRepository() + + settings := []*domain.LegalAndSupportSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want *domain.LegalAndSupportSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.IDCondition(settings[0].ID), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + wantErr: database.NewNoRowFoundError(nil), + }, + { + name: "too many", + condition: repo.InstanceIDCondition(firstInstanceID), + wantErr: database.NewMultipleRowsFoundError(nil), + }, + { + name: "ok, instance", + condition: repo.PrimaryKeyCondition(firstInstanceID, settings[0].ID), + want: settings[0], + }, + { + name: "ok, organization", + condition: repo.PrimaryKeyCondition(secondInstanceID, settings[3].ID), + want: settings[3], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.Get(t.Context(), tx, database.WithCondition(tt.condition)) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestListLegalAndSupportSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + firstInstanceID := createInstance(t, tx) + secondInstanceID := createInstance(t, tx) + firstOrgID := createOrganization(t, tx, firstInstanceID) + secondOrgID := createOrganization(t, tx, secondInstanceID) + repo := repository.LegalAndSupportSettingsRepository() + + settings := []*domain.LegalAndSupportSettings{ + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: nil, + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: nil, + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + { + Settings: domain.Settings{ + InstanceID: firstInstanceID, + OrganizationID: gu.Ptr(firstOrgID), + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + { + Settings: domain.Settings{ + InstanceID: secondInstanceID, + OrganizationID: gu.Ptr(secondOrgID), + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + } + + for _, setting := range settings { + err := repo.Set(t.Context(), tx, setting) + require.NoError(t, err) + } + + tests := []struct { + name string + condition database.Condition + want []*domain.LegalAndSupportSettings + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "no results, ok", + condition: repo.PrimaryKeyCondition(firstInstanceID, "nix"), + want: []*domain.LegalAndSupportSettings{}, + }, + { + name: "all from instance", + condition: repo.InstanceIDCondition(firstInstanceID), + want: []*domain.LegalAndSupportSettings{settings[2], settings[0]}, + }, + { + name: "only from instance", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(nil), + ), + want: []*domain.LegalAndSupportSettings{settings[0]}, + }, + { + name: "all from first org", + condition: database.And( + repo.InstanceIDCondition(firstInstanceID), + repo.OrganizationIDCondition(gu.Ptr(firstOrgID)), + ), + want: []*domain.LegalAndSupportSettings{settings[2]}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.List(t.Context(), tx, + database.WithCondition(tt.condition), + database.WithOrderByAscending(repo.InstanceIDColumn(), repo.OrganizationIDColumn()), + ) + require.ErrorIs(t, err, tt.wantErr) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestSetLegalAndSupportSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.LegalAndSupportSettingsRepository() + + existingSettings := &domain.LegalAndSupportSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + } + + err := repo.Set(t.Context(), tx, existingSettings) + require.NoError(t, err) + + tests := []struct { + name string + settings *domain.LegalAndSupportSettings + wantErr error + }{ + { + name: "create instance", + settings: &domain.LegalAndSupportSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + }, + { + name: "update organization", + settings: &domain.LegalAndSupportSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + }, + { + name: "non-existing instance", + settings: &domain.LegalAndSupportSettings{ + Settings: domain.Settings{ + InstanceID: "foo", + OrganizationID: nil, + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existing org", + settings: &domain.LegalAndSupportSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr("foo"), + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + }, + wantErr: new(database.ForeignKeyError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + err := repo.Set(t.Context(), savepoint, tt.settings) + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestDeleteLegalAndSupportSettings(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + repo := repository.LegalAndSupportSettingsRepository() + + existingInstanceSettings := &domain.LegalAndSupportSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: nil, + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + } + err := repo.Set(t.Context(), tx, existingInstanceSettings) + require.NoError(t, err) + + existingOrganizationSettings := &domain.LegalAndSupportSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: gu.Ptr(orgID), + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: gu.Ptr("https://host"), + PrivacyPolicyLink: gu.Ptr("https://host"), + HelpLink: gu.Ptr("https://host"), + SupportEmail: gu.Ptr("email"), + DocsLink: gu.Ptr("https://host"), + CustomLink: gu.Ptr("https://host"), + CustomLinkText: gu.Ptr("linktext"), + }, + } + err = repo.Set(t.Context(), tx, existingOrganizationSettings) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete condition", + condition: repo.InstanceIDCondition(instanceID), + wantRowsAffected: 0, + wantErr: database.NewMissingConditionError(repo.IDColumn()), + }, + { + name: "not found", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, gu.Ptr("foo"), domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete instance", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete instance twice", + condition: repo.UniqueCondition(existingInstanceSettings.InstanceID, existingInstanceSettings.OrganizationID, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + wantRowsAffected: 0, + }, + { + name: "delete organization", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + wantRowsAffected: 1, + }, + { + name: "delete organization twice", + condition: repo.UniqueCondition(existingOrganizationSettings.InstanceID, existingOrganizationSettings.OrganizationID, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + wantRowsAffected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rowsAffected, err := repo.Delete(t.Context(), tx, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + }) + } +} diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index 64eb10129..7b905c780 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -3,7 +3,7 @@ package systemdefaults import ( "fmt" "time" - + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" ) diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index b0ea3f48b..45b9b436d 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -98,6 +98,7 @@ var ( ProjectRoleRelationalProjection *handler.Handler OrganizationMetadataRelationalProjection *handler.Handler ProjectGrantRelationalProjection *handler.Handler + SettingsRelationalProjection *handler.Handler ProjectGrantFields *handler.FieldHandler OrgDomainVerifiedFields *handler.FieldHandler @@ -219,6 +220,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, OrganizationRelationalProjection = newOrgRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["organizations_relational"])) InstanceDomainRelationalProjection = newInstanceDomainRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_domains_relational"])) OrganizationDomainRelationalProjection = newOrgDomainRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["organization_domains_relational"])) + SettingsRelationalProjection = newSettingsRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["settings_relational"])) IDPTemplateRelationalProjection = newIDPTemplateRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idp_templates_relational"])) ProjectRelationalProjection = newProjectRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["projects_relational"])) ProjectRoleRelationalProjection = newProjectRoleRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_roles_relational"])) @@ -413,6 +415,7 @@ func newProjectionsList() { OrganizationRelationalProjection, InstanceDomainRelationalProjection, OrganizationDomainRelationalProjection, + SettingsRelationalProjection, IDPTemplateRelationalProjection, ProjectRelationalProjection, ProjectRoleRelationalProjection, diff --git a/internal/query/projection/settings_relational.go b/internal/query/projection/settings_relational.go new file mode 100644 index 000000000..8409c3eae --- /dev/null +++ b/internal/query/projection/settings_relational.go @@ -0,0 +1,1787 @@ +package projection + +import ( + "context" + "database/sql" + "net/url" + + "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" + v3_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + settings "github.com/zitadel/zitadel/internal/repository/organization_settings" + "github.com/zitadel/zitadel/internal/repository/policy" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + SettingsRelationalProjectionTable = "zitadel.settings" +) + +type settingsRelationalProjection struct{} + +func newSettingsRelationalProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(settingsRelationalProjection)) +} + +func (*settingsRelationalProjection) Name() string { + return SettingsRelationalProjectionTable +} + +func (s *settingsRelationalProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: org.AggregateType, + EventReducers: []handler.EventReducer{ + // // Login + { + Event: org.LoginPolicyAddedEventType, + Reduce: s.reduceLoginPolicyAdded, + }, + { + Event: org.LoginPolicyChangedEventType, + Reduce: s.reduceLoginPolicyChanged, + }, + { + Event: org.LoginPolicyRemovedEventType, + Reduce: s.reduceLoginPolicyRemoved, + }, + { + Event: org.LoginPolicyMultiFactorAddedEventType, + Reduce: s.reduceMFAAdded, + }, + { + Event: org.LoginPolicyMultiFactorRemovedEventType, + Reduce: s.reduceMFARemoved, + }, + { + Event: org.LoginPolicySecondFactorAddedEventType, + Reduce: s.reduceSecondFactorAdded, + }, + { + Event: org.LoginPolicySecondFactorRemovedEventType, + Reduce: s.reduceSecondFactorRemoved, + }, + // label + { + Event: org.LabelPolicyAddedEventType, + Reduce: s.reduceLabelAdded, + }, + { + Event: org.LabelPolicyChangedEventType, + Reduce: s.reduceLabelChanged, + }, + { + Event: org.LabelPolicyRemovedEventType, + Reduce: s.reduceLabelPolicyRemoved, + }, + { + Event: org.LabelPolicyActivatedEventType, + Reduce: s.reduceLabelActivated, + }, + { + Event: org.LabelPolicyLogoAddedEventType, + Reduce: s.reduceLogoAdded, + }, + { + Event: org.LabelPolicyLogoRemovedEventType, + Reduce: s.reduceLogoRemoved, + }, + { + Event: org.LabelPolicyLogoDarkAddedEventType, + Reduce: s.reduceLogoDarkAdded, + }, + { + Event: org.LabelPolicyLogoDarkRemovedEventType, + Reduce: s.reduceLogoDarkRemoved, + }, + { + Event: org.LabelPolicyIconAddedEventType, + Reduce: s.reduceIconAdded, + }, + { + Event: org.LabelPolicyIconRemovedEventType, + Reduce: s.reduceIconRemoved, + }, + { + Event: org.LabelPolicyIconDarkAddedEventType, + Reduce: s.reduceIconDarkAdded, + }, + { + Event: org.LabelPolicyIconDarkRemovedEventType, + Reduce: s.reduceIconDarkRemoved, + }, + { + Event: org.LabelPolicyFontAddedEventType, + Reduce: s.reduceFontAdded, + }, + { + Event: org.LabelPolicyFontRemovedEventType, + Reduce: s.reduceFontRemoved, + }, + // // Password Complexity + { + Event: org.PasswordComplexityPolicyAddedEventType, + Reduce: s.reducePasswordComplexityAdded, + }, + { + Event: org.PasswordComplexityPolicyChangedEventType, + Reduce: s.reducePasswordComplexityChanged, + }, + { + Event: org.PasswordComplexityPolicyRemovedEventType, + Reduce: s.reducePasswordComplexityRemoved, + }, + // // Password Policy + { + Event: org.PasswordAgePolicyAddedEventType, + Reduce: s.reducePasswordPolicyAdded, + }, + { + Event: org.PasswordAgePolicyChangedEventType, + Reduce: s.reducePasswordPolicyChanged, + }, + { + Event: org.PasswordAgePolicyRemovedEventType, + Reduce: s.reducePasswordPolicyRemoved, + }, + // // Lockout Policy + { + Event: org.LockoutPolicyAddedEventType, + Reduce: s.reduceLockoutPolicyAdded, + }, + { + Event: org.LockoutPolicyChangedEventType, + Reduce: s.reduceLockoutPolicyChanged, + }, + { + Event: org.LockoutPolicyRemovedEventType, + Reduce: s.reduceOrgLockoutPolicyRemoved, + }, + // // Domain Policy + { + Event: org.DomainPolicyAddedEventType, + Reduce: s.reduceDomainPolicyAdded, + }, + { + Event: org.DomainPolicyChangedEventType, + Reduce: s.reduceDomainPolicyChanged, + }, + { + Event: org.DomainPolicyRemovedEventType, + Reduce: s.reduceOrgDomainPolicyRemoved, + }, + // Notification + { + Event: org.NotificationPolicyAddedEventType, + Reduce: s.reduceNotificationPolicyAdded, + }, + { + Event: org.NotificationPolicyChangedEventType, + Reduce: s.reduceNotificationPolicyChanged, + }, + { + Event: org.NotificationPolicyRemovedEventType, + Reduce: s.reduceOrgNotificationPolicyRemoved, + }, + // Privacy + { + Event: org.PrivacyPolicyAddedEventType, + Reduce: s.reducePrivacyPolicyAdded, + }, + { + Event: org.PrivacyPolicyChangedEventType, + Reduce: s.reducePrivacyPolicyChanged, + }, + { + Event: org.PrivacyPolicyRemovedEventType, + Reduce: s.reduceOrgPrivacyPolicyRemoved, + }, + }, + }, + // // settings + { + Aggregate: settings.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: settings.OrganizationSettingsSetEventType, + Reduce: s.reduceOrganizationSettingsSet, + }, + { + Event: settings.OrganizationSettingsRemovedEventType, + Reduce: s.reduceOrganizationSettingsRemoved, + }, + }, + }, + { + Aggregate: instance.AggregateType, + EventReducers: []handler.EventReducer{ + // // Login + { + Event: instance.LoginPolicyAddedEventType, + Reduce: s.reduceLoginPolicyAdded, + }, + { + Event: instance.LoginPolicyChangedEventType, + Reduce: s.reduceLoginPolicyChanged, + }, + { + Event: instance.LoginPolicyMultiFactorAddedEventType, + Reduce: s.reduceMFAAdded, + }, + { + Event: instance.LoginPolicyMultiFactorRemovedEventType, + Reduce: s.reduceMFARemoved, + }, + { + Event: instance.LoginPolicySecondFactorAddedEventType, + Reduce: s.reduceSecondFactorAdded, + }, + { + Event: instance.LoginPolicySecondFactorRemovedEventType, + Reduce: s.reduceSecondFactorRemoved, + }, + // // Label + { + Event: instance.LabelPolicyAddedEventType, + Reduce: s.reduceLabelAdded, + }, + { + Event: instance.LabelPolicyChangedEventType, + Reduce: s.reduceLabelChanged, + }, + { + Event: instance.LabelPolicyActivatedEventType, + Reduce: s.reduceLabelActivated, + }, + { + Event: instance.LabelPolicyLogoAddedEventType, + Reduce: s.reduceLogoAdded, + }, + { + Event: instance.LabelPolicyLogoRemovedEventType, + Reduce: s.reduceLogoRemoved, + }, + { + Event: instance.LabelPolicyLogoDarkAddedEventType, + Reduce: s.reduceLogoDarkAdded, + }, + { + Event: instance.LabelPolicyLogoDarkRemovedEventType, + Reduce: s.reduceLogoDarkRemoved, + }, + { + Event: instance.LabelPolicyIconAddedEventType, + Reduce: s.reduceIconAdded, + }, + { + Event: instance.LabelPolicyIconRemovedEventType, + Reduce: s.reduceIconRemoved, + }, + { + Event: instance.LabelPolicyIconDarkAddedEventType, + Reduce: s.reduceIconDarkAdded, + }, + { + Event: instance.LabelPolicyIconDarkRemovedEventType, + Reduce: s.reduceIconDarkRemoved, + }, + { + Event: instance.LabelPolicyFontAddedEventType, + Reduce: s.reduceFontAdded, + }, + { + Event: instance.LabelPolicyFontRemovedEventType, + Reduce: s.reduceFontRemoved, + }, + // // Password Complexity + { + Event: instance.PasswordComplexityPolicyAddedEventType, + Reduce: s.reducePasswordComplexityAdded, + }, + { + Event: instance.PasswordComplexityPolicyChangedEventType, + Reduce: s.reducePasswordComplexityChanged, + }, + // // Password Policy + { + Event: instance.PasswordAgePolicyAddedEventType, + Reduce: s.reducePasswordPolicyAdded, + }, + { + Event: instance.PasswordAgePolicyChangedEventType, + Reduce: s.reducePasswordPolicyChanged, + }, + // // Lockout Policy + { + Event: instance.LockoutPolicyAddedEventType, + Reduce: s.reduceLockoutPolicyAdded, + }, + { + Event: instance.LockoutPolicyChangedEventType, + Reduce: s.reduceLockoutPolicyChanged, + }, + // // Domain Policy + { + Event: instance.DomainPolicyAddedEventType, + Reduce: s.reduceDomainPolicyAdded, + }, + { + Event: instance.DomainPolicyChangedEventType, + Reduce: s.reduceDomainPolicyChanged, + }, + // // Security Policy + { + Event: instance.SecurityPolicySetEventType, + Reduce: s.reduceSecurityPolicySet, + }, + // Notification + { + Event: instance.NotificationPolicyAddedEventType, + Reduce: s.reduceNotificationPolicyAdded, + }, + { + Event: instance.NotificationPolicyChangedEventType, + Reduce: s.reduceNotificationPolicyChanged, + }, + // Privacy policy + { + Event: instance.PrivacyPolicyAddedEventType, + Reduce: s.reducePrivacyPolicyAdded, + }, + { + Event: instance.PrivacyPolicyChangedEventType, + Reduce: s.reducePrivacyPolicyChanged, + }, + }, + }, + } +} + +func (s *settingsRelationalProjection) reduceLoginPolicyAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LoginPolicyAddedEvent + switch e := event.(type) { + case *instance.LoginPolicyAddedEvent: + policyEvent = e.LoginPolicyAddedEvent + case *org.LoginPolicyAddedEvent: + policyEvent = e.LoginPolicyAddedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-YYPxS", "reduce.wrong.event.type %v", []eventstore.EventType{org.LoginPolicyAddedEventType, instance.LoginPolicyAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + settingsRepo := repository.LoginSettingsRepository() + + passwordlessType := domain.PasswordlessType(policyEvent.PasswordlessType) + setting := domain.LoginSettings{ + Settings: domain.Settings{ + InstanceID: policyEvent.Aggregate().InstanceID, + OrganizationID: orgId, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: &policyEvent.AllowUserNamePassword, + AllowRegister: &policyEvent.AllowRegister, + AllowExternalIDP: &policyEvent.AllowExternalIDP, + ForceMFA: &policyEvent.ForceMFA, + ForceMFALocalOnly: &policyEvent.ForceMFALocalOnly, + HidePasswordReset: &policyEvent.HidePasswordReset, + IgnoreUnknownUsernames: &policyEvent.IgnoreUnknownUsernames, + AllowDomainDiscovery: &policyEvent.AllowDomainDiscovery, + DisableLoginWithEmail: &policyEvent.DisableLoginWithEmail, + DisableLoginWithPhone: &policyEvent.DisableLoginWithPhone, + PasswordlessType: &passwordlessType, + DefaultRedirectURI: &policyEvent.DefaultRedirectURI, + PasswordCheckLifetime: &policyEvent.PasswordCheckLifetime, + ExternalLoginCheckLifetime: &policyEvent.ExternalLoginCheckLifetime, + MFAInitSkipLifetime: &policyEvent.MFAInitSkipLifetime, + SecondFactorCheckLifetime: &policyEvent.SecondFactorCheckLifetime, + MultiFactorCheckLifetime: &policyEvent.MultiFactorCheckLifetime, + MFAType: nil, + SecondFactorTypes: nil, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &setting) + }), nil +} + +// //nolint:gocognit +func (s *settingsRelationalProjection) reduceLoginPolicyChanged(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LoginPolicyChangedEvent + switch e := event.(type) { + case *instance.LoginPolicyChangedEvent: + policyEvent = e.LoginPolicyChangedEvent + case *org.LoginPolicyChangedEvent: + policyEvent = e.LoginPolicyChangedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-BHd86", "reduce.wrong.event.type %v", []eventstore.EventType{org.LoginPolicyChangedEventType, instance.LoginPolicyChangedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rLk9y", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.LoginSettingsRepository() + + var passwordlessType *domain.PasswordlessType + if policyEvent.PasswordlessType != nil { + passwordlessType = gu.Ptr(domain.PasswordlessType(*policyEvent.PasswordlessType)) + } + + settings := domain.LoginSettings{ + Settings: domain.Settings{ + InstanceID: policyEvent.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + LoginSettingsAttributes: domain.LoginSettingsAttributes{ + AllowUserNamePassword: policyEvent.AllowUserNamePassword, + AllowRegister: policyEvent.AllowRegister, + AllowExternalIDP: policyEvent.AllowExternalIDP, + ForceMFA: policyEvent.ForceMFA, + ForceMFALocalOnly: policyEvent.ForceMFALocalOnly, + HidePasswordReset: policyEvent.HidePasswordReset, + IgnoreUnknownUsernames: policyEvent.IgnoreUnknownUsernames, + AllowDomainDiscovery: policyEvent.AllowDomainDiscovery, + DisableLoginWithEmail: policyEvent.DisableLoginWithEmail, + DisableLoginWithPhone: policyEvent.DisableLoginWithPhone, + PasswordlessType: passwordlessType, + DefaultRedirectURI: policyEvent.DefaultRedirectURI, + PasswordCheckLifetime: policyEvent.PasswordCheckLifetime, + ExternalLoginCheckLifetime: policyEvent.ExternalLoginCheckLifetime, + MFAInitSkipLifetime: policyEvent.MFAInitSkipLifetime, + SecondFactorCheckLifetime: policyEvent.SecondFactorCheckLifetime, + MultiFactorCheckLifetime: policyEvent.MultiFactorCheckLifetime, + MFAType: nil, + SecondFactorTypes: nil, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (s *settingsRelationalProjection) reduceMFAAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.MultiFactorAddedEvent + switch e := event.(type) { + case *instance.LoginPolicyMultiFactorAddedEvent: + policyEvent = e.MultiFactorAddedEvent + case *org.LoginPolicyMultiFactorAddedEvent: + policyEvent = e.MultiFactorAddedEvent + orgId = &policyEvent.Aggregate().ID + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-WghuV", "reduce.wrong.event.type %v", []eventstore.EventType{org.LoginPolicyMultiFactorAddedEventType, instance.LoginPolicyMultiFactorAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rLw7y", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.LoginSettingsRepository() + return settingsRepo.SetColumns(ctx, v3_sql.SQLTx(tx), + &domain.Settings{ + InstanceID: policyEvent.Agg.InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + settingsRepo.AddMFAType(domain.MultiFactorType(policyEvent.MFAType)), + ) + }), nil +} + +func (s *settingsRelationalProjection) reduceMFARemoved(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.MultiFactorRemovedEvent + switch e := event.(type) { + case *instance.LoginPolicyMultiFactorRemovedEvent: + policyEvent = e.MultiFactorRemovedEvent + case *org.LoginPolicyMultiFactorRemovedEvent: + policyEvent = e.MultiFactorRemovedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-cHU7u", "reduce.wrong.event.type %v", []eventstore.EventType{org.LoginPolicyMultiFactorRemovedEventType, instance.LoginPolicyMultiFactorRemovedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rLi9y", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.LoginSettingsRepository() + return settingsRepo.SetColumns(ctx, v3_sql.SQLTx(tx), + &domain.Settings{ + InstanceID: policyEvent.Agg.InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + settingsRepo.RemoveMFAType(domain.MultiFactorType(policyEvent.MFAType)), + ) + }), nil +} + +func (*settingsRelationalProjection) reduceLoginPolicyRemoved(event eventstore.Event) (*handler.Statement, error) { + loginPolicyRemovedEvent, ok := event.(*org.LoginPolicyRemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-oRSvD", "reduce.wrong.event.type %s", org.LoginPolicyRemovedEventType) + } + return handler.NewStatement(loginPolicyRemovedEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-arg9y", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.LoginSettingsRepository() + _, err := settingsRepo.Delete( + ctx, v3_sql.SQLTx(tx), + settingsRepo.UniqueCondition(loginPolicyRemovedEvent.Aggregate().InstanceID, &loginPolicyRemovedEvent.Aggregate().ID, domain.SettingTypeLogin, domain.SettingStateActive), + ) + return err + }), nil +} + +func (s *settingsRelationalProjection) reduceSecondFactorAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.SecondFactorAddedEvent + switch e := event.(type) { + case *instance.LoginPolicySecondFactorAddedEvent: + policyEvent = e.SecondFactorAddedEvent + case *org.LoginPolicySecondFactorAddedEvent: + policyEvent = e.SecondFactorAddedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-apB2E", "reduce.wrong.event.type %v", []eventstore.EventType{org.LoginPolicySecondFactorAddedEventType, instance.LoginPolicySecondFactorAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-iLk4m", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.LoginSettingsRepository() + return settingsRepo.SetColumns(ctx, v3_sql.SQLTx(tx), + &domain.Settings{ + InstanceID: policyEvent.Agg.InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + settingsRepo.AddSecondFactorTypes(domain.SecondFactorType(policyEvent.MFAType)), + ) + }), nil +} + +func (s *settingsRelationalProjection) reduceSecondFactorRemoved(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.SecondFactorRemovedEvent + switch e := event.(type) { + case *instance.LoginPolicySecondFactorRemovedEvent: + policyEvent = e.SecondFactorRemovedEvent + case *org.LoginPolicySecondFactorRemovedEvent: + policyEvent = e.SecondFactorRemovedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-bYpmA", "reduce.wrong.event.type %v", []eventstore.EventType{org.LoginPolicySecondFactorRemovedEventType, instance.LoginPolicySecondFactorRemovedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rnd0y", "reduce.wrong.db.pool %T", ex) + } + settingsRepo := repository.LoginSettingsRepository() + return settingsRepo.SetColumns(ctx, v3_sql.SQLTx(tx), + &domain.Settings{ + InstanceID: policyEvent.Agg.InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + settingsRepo.RemoveSecondFactorTypes(domain.SecondFactorType(policyEvent.MFAType)), + ) + }), nil +} + +// label +func (s *settingsRelationalProjection) reduceLabelAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyAddedEvent + switch e := event.(type) { + case *org.LabelPolicyAddedEvent: + policyEvent = e.LabelPolicyAddedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.LabelPolicyAddedEvent: + policyEvent = e.LabelPolicyAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-CSE7A", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyAddedEventType, instance.LabelPolicyAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + settingsRepo := repository.BrandingSettingsRepository() + themeMode := domain.BrandingPolicyThemeMode(policyEvent.ThemeMode) + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: policyEvent.Aggregate().InstanceID, + OrganizationID: orgId, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: &policyEvent.PrimaryColor, + BackgroundColorLight: &policyEvent.BackgroundColor, + WarnColorLight: &policyEvent.WarnColor, + FontColorLight: &policyEvent.FontColor, + PrimaryColorDark: &policyEvent.PrimaryColorDark, + BackgroundColorDark: &policyEvent.BackgroundColorDark, + WarnColorDark: &policyEvent.WarnColorDark, + FontColorDark: &policyEvent.FontColorDark, + HideLoginNameSuffix: &policyEvent.HideLoginNameSuffix, + ErrorMsgPopup: &policyEvent.ErrorMsgPopup, + DisableWatermark: &policyEvent.DisableWatermark, + ThemeMode: &themeMode, + LogoURLLight: nil, + IconURLLight: nil, + LogoURLDark: nil, + IconURLDark: nil, + FontURL: nil, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (s *settingsRelationalProjection) reduceLabelChanged(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyChangedEvent + switch e := event.(type) { + case *org.LabelPolicyChangedEvent: + policyEvent = e.LabelPolicyChangedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.LabelPolicyChangedEvent: + policyEvent = e.LabelPolicyChangedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-qgVug", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyChangedEventType, instance.LabelPolicyChangedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-lhb9y", "reduce.wrong.db.pool %T", ex) + } + settingsRepo := repository.BrandingSettingsRepository() + var themeMode *domain.BrandingPolicyThemeMode + if policyEvent.ThemeMode != nil { + themeMode = gu.Ptr(domain.BrandingPolicyThemeMode(*policyEvent.ThemeMode)) + } + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: policyEvent.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + PrimaryColorLight: policyEvent.PrimaryColor, + BackgroundColorLight: policyEvent.BackgroundColor, + WarnColorLight: policyEvent.WarnColor, + FontColorLight: policyEvent.FontColor, + PrimaryColorDark: policyEvent.PrimaryColorDark, + BackgroundColorDark: policyEvent.BackgroundColorDark, + WarnColorDark: policyEvent.WarnColorDark, + FontColorDark: policyEvent.FontColorDark, + HideLoginNameSuffix: policyEvent.HideLoginNameSuffix, + ErrorMsgPopup: policyEvent.ErrorMsgPopup, + DisableWatermark: policyEvent.DisableWatermark, + ThemeMode: themeMode, + LogoURLLight: nil, + IconURLLight: nil, + LogoURLDark: nil, + IconURLDark: nil, + FontURL: nil, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (s *settingsRelationalProjection) reduceLabelPolicyRemoved(event eventstore.Event) (*handler.Statement, error) { + policyEvent, ok := event.(*org.LabelPolicyRemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-ATMBz", "reduce.wrong.event.type %s", org.LabelPolicyRemovedEventType) + } + + return handler.NewStatement(policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-r7k0y", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.BrandingSettingsRepository() + _, err := settingsRepo.Delete(ctx, v3_sql.SQLTx(tx), + settingsRepo.UniqueCondition(policyEvent.Aggregate().InstanceID, &policyEvent.Aggregate().ID, domain.SettingTypeBranding, domain.SettingStateActive), + ) + return err + }), nil +} + +func (s *settingsRelationalProjection) reduceLabelActivated(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyActivatedEvent + switch e := event.(type) { + case *org.LabelPolicyActivatedEvent: + orgId = &event.Aggregate().ID + policyEvent = e.LabelPolicyActivatedEvent + case *instance.LabelPolicyActivatedEvent: + policyEvent = e.LabelPolicyActivatedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-7Kd8U", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyActivatedEventType, instance.LabelPolicyActivatedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-r7k0y", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.BrandingSettingsRepository() + _, err := settingsRepo.ActivateAt(ctx, v3_sql.SQLTx(tx), + database.And( + settingsRepo.InstanceIDCondition(event.Aggregate().InstanceID), + settingsRepo.OrganizationIDCondition(orgId), + ), + policyEvent.Creation, + ) + return err + }), nil +} + +func (p *settingsRelationalProjection) reduceLogoAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyLogoAddedEvent + switch e := event.(type) { + case *org.LabelPolicyLogoAddedEvent: + orgId = &e.Aggregate().ResourceOwner + policyEvent = e.LabelPolicyLogoAddedEvent + case *instance.LabelPolicyLogoAddedEvent: + policyEvent = e.LabelPolicyLogoAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-kg8H4", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyLogoAddedEventType, instance.LabelPolicyLogoAddedEventType}) + } + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + + url, err := url.Parse(policyEvent.StoreKey) + if err != nil { + return err + } + + settingsRepo := repository.BrandingSettingsRepository() + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + LogoURLLight: url, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceLogoDarkAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyLogoDarkAddedEvent + switch e := event.(type) { + case *org.LabelPolicyLogoDarkAddedEvent: + orgId = &e.Aggregate().ResourceOwner + policyEvent = e.LabelPolicyLogoDarkAddedEvent + case *instance.LabelPolicyLogoDarkAddedEvent: + policyEvent = e.LabelPolicyLogoDarkAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-kg8H4", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyLogoDarkAddedEventType, instance.LabelPolicyLogoDarkAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + + url, err := url.Parse(policyEvent.StoreKey) + if err != nil { + return err + } + + settingsRepo := repository.BrandingSettingsRepository() + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + LogoURLDark: url, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceLogoRemoved(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyLogoRemovedEvent + switch e := event.(type) { + case *org.LabelPolicyLogoRemovedEvent: + orgId = &event.Aggregate().ID + policyEvent = e.LabelPolicyLogoRemovedEvent + case *instance.LabelPolicyLogoRemovedEvent: + policyEvent = e.LabelPolicyLogoRemovedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-kg8H4", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyLogoRemovedEventType, instance.LabelPolicyLogoRemovedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.BrandingSettingsRepository() + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + LogoURLLight: &url.URL{}, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceLogoDarkRemoved(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyLogoDarkRemovedEvent + switch e := event.(type) { + case *org.LabelPolicyLogoDarkRemovedEvent: + orgId = &event.Aggregate().ID + policyEvent = e.LabelPolicyLogoDarkRemovedEvent + case *instance.LabelPolicyLogoDarkRemovedEvent: + policyEvent = e.LabelPolicyLogoDarkRemovedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-kg8H4", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyLogoDarkRemovedEventType, instance.LabelPolicyLogoDarkRemovedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + settingsRepo := repository.BrandingSettingsRepository() + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + LogoURLDark: &url.URL{}, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceIconAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyIconAddedEvent + switch e := event.(type) { + case *org.LabelPolicyIconAddedEvent: + orgId = &event.Aggregate().ID + policyEvent = e.LabelPolicyIconAddedEvent + case *instance.LabelPolicyIconAddedEvent: + policyEvent = e.LabelPolicyIconAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-e2JFz", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyIconDarkAddedEventType, instance.LabelPolicyIconDarkAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + + url, err := url.Parse(policyEvent.StoreKey) + if err != nil { + return err + } + + settingsRepo := repository.BrandingSettingsRepository() + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + IconURLLight: url, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceIconDarkAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyIconDarkAddedEvent + switch e := event.(type) { + case *org.LabelPolicyIconDarkAddedEvent: + orgId = &event.Aggregate().ID + policyEvent = e.LabelPolicyIconDarkAddedEvent + case *instance.LabelPolicyIconDarkAddedEvent: + policyEvent = e.LabelPolicyIconDarkAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-e2JFz", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyIconDarkAddedEventType, instance.LabelPolicyIconDarkAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + + url, err := url.Parse(policyEvent.StoreKey) + if err != nil { + return err + } + + settingsRepo := repository.BrandingSettingsRepository() + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + IconURLDark: url, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceIconRemoved(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyIconRemovedEvent + switch e := event.(type) { + case *org.LabelPolicyIconRemovedEvent: + orgId = &event.Aggregate().ID + policyEvent = e.LabelPolicyIconRemovedEvent + case *instance.LabelPolicyIconRemovedEvent: + policyEvent = e.LabelPolicyIconRemovedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-gfgbY", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyIconRemovedEventType, instance.LabelPolicyIconRemovedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.BrandingSettingsRepository() + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + IconURLLight: &url.URL{}, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceIconDarkRemoved(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyIconDarkRemovedEvent + switch e := event.(type) { + case *org.LabelPolicyIconDarkRemovedEvent: + orgId = &event.Aggregate().ID + policyEvent = e.LabelPolicyIconDarkRemovedEvent + case *instance.LabelPolicyIconDarkRemovedEvent: + policyEvent = e.LabelPolicyIconDarkRemovedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-gfgbY", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyIconDarkRemovedEventType, instance.LabelPolicyIconDarkRemovedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.BrandingSettingsRepository() + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + IconURLDark: &url.URL{}, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceFontAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyFontAddedEvent + switch e := event.(type) { + case *org.LabelPolicyFontAddedEvent: + orgId = &event.Aggregate().ID + policyEvent = e.LabelPolicyFontAddedEvent + case *instance.LabelPolicyFontAddedEvent: + policyEvent = e.LabelPolicyFontAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-65i9W", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyFontAddedEventType, instance.LabelPolicyFontAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + + url, err := url.Parse(policyEvent.StoreKey) + if err != nil { + return err + } + + settingsRepo := repository.BrandingSettingsRepository() + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + FontURL: url, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceFontRemoved(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LabelPolicyFontRemovedEvent + switch e := event.(type) { + case *org.LabelPolicyFontRemovedEvent: + orgId = &event.Aggregate().ID + policyEvent = e.LabelPolicyFontRemovedEvent + case *instance.LabelPolicyFontRemovedEvent: + policyEvent = e.LabelPolicyFontRemovedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-xf32J", "reduce.wrong.event.type %v", []eventstore.EventType{org.LabelPolicyFontRemovedEventType, instance.LabelPolicyFontRemovedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.BrandingSettingsRepository() + settings := domain.BrandingSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + BrandingSettingsAttributes: domain.BrandingSettingsAttributes{ + FontURL: &url.URL{}, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reducePasswordComplexityAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.PasswordComplexityPolicyAddedEvent + switch e := event.(type) { + case *org.PasswordComplexityPolicyAddedEvent: + policyEvent = e.PasswordComplexityPolicyAddedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.PasswordComplexityPolicyAddedEvent: + policyEvent = e.PasswordComplexityPolicyAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-KTHmJ", "reduce.wrong.event.type %v", []eventstore.EventType{org.PasswordComplexityPolicyAddedEventType, instance.PasswordComplexityPolicyAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.PasswordComplexitySettingsRepository() + settings := domain.PasswordComplexitySettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: &policyEvent.MinLength, + HasLowercase: &policyEvent.HasLowercase, + HasUppercase: &policyEvent.HasUppercase, + HasNumber: &policyEvent.HasNumber, + HasSymbol: &policyEvent.HasSymbol, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (s *settingsRelationalProjection) reducePasswordComplexityChanged(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.PasswordComplexityPolicyChangedEvent + switch e := event.(type) { + case *org.PasswordComplexityPolicyChangedEvent: + policyEvent = e.PasswordComplexityPolicyChangedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.PasswordComplexityPolicyChangedEvent: + policyEvent = e.PasswordComplexityPolicyChangedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-cf3Xb", "reduce.wrong.event.type %v", []eventstore.EventType{org.PasswordComplexityPolicyChangedEventType, instance.PasswordComplexityPolicyChangedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rLrfy", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.PasswordComplexitySettingsRepository() + settings := domain.PasswordComplexitySettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + PasswordComplexitySettingsAttributes: domain.PasswordComplexitySettingsAttributes{ + MinLength: policyEvent.MinLength, + HasLowercase: policyEvent.HasLowercase, + HasUppercase: policyEvent.HasUppercase, + HasNumber: policyEvent.HasNumber, + HasSymbol: policyEvent.HasSymbol, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (s *settingsRelationalProjection) reducePasswordComplexityRemoved(event eventstore.Event) (*handler.Statement, error) { + policyEvent, ok := event.(*org.PasswordComplexityPolicyRemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-wttCd", "reduce.wrong.event.type %s", org.PasswordComplexityPolicyRemovedEventType) + } + + return handler.NewStatement(policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-UrdHy", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.PasswordComplexitySettingsRepository() + _, err := settingsRepo.Delete(ctx, v3_sql.SQLTx(tx), + settingsRepo.UniqueCondition(policyEvent.Aggregate().InstanceID, &policyEvent.Aggregate().ID, domain.SettingTypePasswordComplexity, domain.SettingStateActive), + ) + return err + }), nil +} + +func (p *settingsRelationalProjection) reducePasswordPolicyAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.PasswordAgePolicyAddedEvent + switch e := event.(type) { + case *org.PasswordAgePolicyAddedEvent: + policyEvent = e.PasswordAgePolicyAddedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.PasswordAgePolicyAddedEvent: + policyEvent = e.PasswordAgePolicyAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-CJqF0", "reduce.wrong.event.type %v", []eventstore.EventType{org.PasswordAgePolicyAddedEventType, instance.PasswordAgePolicyAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hONE", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.PasswordExpirySettingsRepository() + settings := domain.PasswordExpirySettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: &policyEvent.ExpireWarnDays, + MaxAgeDays: &policyEvent.MaxAgeDays, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reducePasswordPolicyChanged(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.PasswordAgePolicyChangedEvent + switch e := event.(type) { + case *org.PasswordAgePolicyChangedEvent: + policyEvent = e.PasswordAgePolicyChangedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.PasswordAgePolicyChangedEvent: + policyEvent = e.PasswordAgePolicyChangedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-i7FZt", "reduce.wrong.event.type %v", []eventstore.EventType{org.PasswordAgePolicyChangedEventType, instance.PasswordAgePolicyChangedEventType}) + } + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-Mlk6y", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.PasswordExpirySettingsRepository() + settings := domain.PasswordExpirySettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + PasswordExpirySettingsAttributes: domain.PasswordExpirySettingsAttributes{ + ExpireWarnDays: policyEvent.ExpireWarnDays, + MaxAgeDays: policyEvent.MaxAgeDays, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reducePasswordPolicyRemoved(event eventstore.Event) (*handler.Statement, error) { + policyEvent, ok := event.(*org.PasswordAgePolicyRemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-EtHWB", "reduce.wrong.event.type %s", org.PasswordAgePolicyRemovedEventType) + } + return handler.NewStatement(policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-UrdHy", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.PasswordExpirySettingsRepository() + _, err := settingsRepo.Delete(ctx, v3_sql.SQLTx(tx), + settingsRepo.UniqueCondition(policyEvent.Aggregate().InstanceID, &policyEvent.Aggregate().ID, domain.SettingTypePasswordExpiry, domain.SettingStateActive), + ) + return err + }), nil +} + +func (p *settingsRelationalProjection) reduceOrgLockoutPolicyRemoved(event eventstore.Event) (*handler.Statement, error) { + policyEvent, ok := event.(*org.LockoutPolicyRemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-Bqut9", "reduce.wrong.event.type %s", org.LockoutPolicyRemovedEventType) + } + return handler.NewStatement(policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-UrdHy", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.LockoutSettingsRepository() + _, err := settingsRepo.Delete(ctx, v3_sql.SQLTx(tx), + settingsRepo.UniqueCondition(policyEvent.Aggregate().InstanceID, &policyEvent.Aggregate().ID, domain.SettingTypeLockout, domain.SettingStateActive), + ) + return err + }), nil +} + +func (p *settingsRelationalProjection) reduceLockoutPolicyAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LockoutPolicyAddedEvent + switch e := event.(type) { + case *org.LockoutPolicyAddedEvent: + policyEvent = e.LockoutPolicyAddedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.LockoutPolicyAddedEvent: + policyEvent = e.LockoutPolicyAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-d8mZO", "reduce.wrong.event.type, %v", []eventstore.EventType{org.LockoutPolicyAddedEventType, instance.LockoutPolicyAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5hnNE", "reduce.wrong.db.pool %T", ex) + } + settingsRepo := repository.LockoutSettingsRepository() + settings := domain.LockoutSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: &policyEvent.MaxPasswordAttempts, + MaxOTPAttempts: &policyEvent.MaxOTPAttempts, + ShowLockOutFailures: &policyEvent.ShowLockOutFailures, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceLockoutPolicyChanged(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.LockoutPolicyChangedEvent + switch e := event.(type) { + case *org.LockoutPolicyChangedEvent: + policyEvent = e.LockoutPolicyChangedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.LockoutPolicyChangedEvent: + policyEvent = e.LockoutPolicyChangedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-gT3BQ", "reduce.wrong.event.type, %v", []eventstore.EventType{org.LockoutPolicyChangedEventType, instance.LockoutPolicyChangedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rbsxy", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.LockoutSettingsRepository() + settings := domain.LockoutSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + LockoutSettingsAttributes: domain.LockoutSettingsAttributes{ + MaxPasswordAttempts: policyEvent.MaxPasswordAttempts, + MaxOTPAttempts: policyEvent.MaxOTPAttempts, + ShowLockOutFailures: policyEvent.ShowLockOutFailures, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceDomainPolicyAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.DomainPolicyAddedEvent + switch e := event.(type) { + case *org.DomainPolicyAddedEvent: + policyEvent = e.DomainPolicyAddedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.DomainPolicyAddedEvent: + policyEvent = e.DomainPolicyAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-8se7M", "reduce.wrong.event.type %v", []eventstore.EventType{org.DomainPolicyAddedEventType, instance.DomainPolicyAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-chduE", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.DomainSettingsRepository() + settings := domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: &policyEvent.UserLoginMustBeDomain, + RequireOrgDomainVerification: &policyEvent.ValidateOrgDomains, + SMTPSenderAddressMatchesInstanceDomain: &policyEvent.SMTPSenderAddressMatchesInstanceDomain, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (s *settingsRelationalProjection) reduceDomainPolicyChanged(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.DomainPolicyChangedEvent + switch e := event.(type) { + case *org.DomainPolicyChangedEvent: + policyEvent = e.DomainPolicyChangedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.DomainPolicyChangedEvent: + policyEvent = e.DomainPolicyChangedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-qgVug", "reduce.wrong.event.type %v", []eventstore.EventType{org.DomainPolicyChangedEventType, instance.DomainPolicyChangedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rbsxy", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.DomainSettingsRepository() + settings := domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: policyEvent.UserLoginMustBeDomain, + RequireOrgDomainVerification: policyEvent.ValidateOrgDomains, + SMTPSenderAddressMatchesInstanceDomain: policyEvent.SMTPSenderAddressMatchesInstanceDomain, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceOrgDomainPolicyRemoved(event eventstore.Event) (*handler.Statement, error) { + policyEvent, ok := event.(*org.DomainPolicyRemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-Bqut9", "reduce.wrong.event.type %s", org.LockoutPolicyRemovedEventType) + } + return handler.NewStatement(policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-UrdHy", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.DomainSettingsRepository() + _, err := settingsRepo.Delete(ctx, v3_sql.SQLTx(tx), + settingsRepo.UniqueCondition(policyEvent.Aggregate().InstanceID, &policyEvent.Aggregate().ID, domain.SettingTypeDomain, domain.SettingStateActive), + ) + return err + }), nil +} + +func (p *settingsRelationalProjection) reduceNotificationPolicyAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.NotificationPolicyAddedEvent + switch e := event.(type) { + case *org.NotificationPolicyAddedEvent: + policyEvent = e.NotificationPolicyAddedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.NotificationPolicyAddedEvent: + policyEvent = e.NotificationPolicyAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-8se7M", "reduce.wrong.event.type %v", []eventstore.EventType{org.NotificationPolicyAddedEventType, instance.NotificationPolicyAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-chduE", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.NotificationSettingsRepository() + settings := domain.NotificationSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: &policyEvent.PasswordChange, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (s *settingsRelationalProjection) reduceNotificationPolicyChanged(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.NotificationPolicyChangedEvent + switch e := event.(type) { + case *org.NotificationPolicyChangedEvent: + policyEvent = e.NotificationPolicyChangedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.NotificationPolicyChangedEvent: + policyEvent = e.NotificationPolicyChangedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-qgVug", "reduce.wrong.event.type %v", []eventstore.EventType{org.NotificationPolicyChangedEventType, instance.NotificationPolicyChangedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rbsxy", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.NotificationSettingsRepository() + settings := domain.NotificationSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + NotificationSettingsAttributes: domain.NotificationSettingsAttributes{ + PasswordChange: policyEvent.PasswordChange, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceOrgNotificationPolicyRemoved(event eventstore.Event) (*handler.Statement, error) { + policyEvent, ok := event.(*org.NotificationPolicyRemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-Bqut9", "reduce.wrong.event.type %s", org.NotificationPolicyRemovedEventType) + } + return handler.NewStatement(policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-UrdHy", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.NotificationSettingsRepository() + _, err := settingsRepo.Delete(ctx, v3_sql.SQLTx(tx), + settingsRepo.UniqueCondition(policyEvent.Aggregate().InstanceID, &policyEvent.Aggregate().ID, domain.SettingTypeNotification, domain.SettingStateActive), + ) + return err + }), nil +} + +func (p *settingsRelationalProjection) reducePrivacyPolicyAdded(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.PrivacyPolicyAddedEvent + switch e := event.(type) { + case *org.PrivacyPolicyAddedEvent: + policyEvent = e.PrivacyPolicyAddedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.PrivacyPolicyAddedEvent: + policyEvent = e.PrivacyPolicyAddedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-8se7M", "reduce.wrong.event.type %v", []eventstore.EventType{org.PrivacyPolicyAddedEventType, instance.PrivacyPolicyAddedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-chduE", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.LegalAndSupportSettingsRepository() + email := string(policyEvent.SupportEmail) + settings := domain.LegalAndSupportSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: &policyEvent.TOSLink, + PrivacyPolicyLink: &policyEvent.PrivacyLink, + HelpLink: &policyEvent.HelpLink, + SupportEmail: &email, + DocsLink: &policyEvent.DocsLink, + CustomLink: &policyEvent.CustomLink, + CustomLinkText: &policyEvent.CustomLinkText, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (s *settingsRelationalProjection) reducePrivacyPolicyChanged(event eventstore.Event) (*handler.Statement, error) { + var orgId *string + var policyEvent policy.PrivacyPolicyChangedEvent + switch e := event.(type) { + case *org.PrivacyPolicyChangedEvent: + policyEvent = e.PrivacyPolicyChangedEvent + orgId = &policyEvent.Aggregate().ResourceOwner + case *instance.PrivacyPolicyChangedEvent: + policyEvent = e.PrivacyPolicyChangedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-qgVug", "reduce.wrong.event.type %v", []eventstore.EventType{org.PrivacyPolicyChangedEventType, instance.PrivacyPolicyChangedEventType}) + } + + return handler.NewStatement(&policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rbsxy", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.LegalAndSupportSettingsRepository() + var email string + if policyEvent.SupportEmail != nil { + email = string(*policyEvent.SupportEmail) + } + settings := domain.LegalAndSupportSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: orgId, + UpdatedAt: policyEvent.Creation, + }, + LegalAndSupportSettingsAttributes: domain.LegalAndSupportSettingsAttributes{ + TOSLink: policyEvent.TOSLink, + PrivacyPolicyLink: policyEvent.PrivacyLink, + HelpLink: policyEvent.HelpLink, + SupportEmail: &email, + DocsLink: policyEvent.DocsLink, + CustomLink: policyEvent.CustomLink, + CustomLinkText: policyEvent.CustomLinkText, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (p *settingsRelationalProjection) reduceOrgPrivacyPolicyRemoved(event eventstore.Event) (*handler.Statement, error) { + policyEvent, ok := event.(*org.PrivacyPolicyRemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-Bqut9", "reduce.wrong.event.type %s", org.PrivacyPolicyRemovedEventType) + } + return handler.NewStatement(policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-UrdHy", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.LegalAndSupportSettingsRepository() + _, err := settingsRepo.Delete(ctx, v3_sql.SQLTx(tx), + settingsRepo.UniqueCondition(policyEvent.Aggregate().InstanceID, &policyEvent.Aggregate().ID, domain.SettingTypeLegalAndSupport, domain.SettingStateActive), + ) + return err + }), nil +} + +func (s *settingsRelationalProjection) reduceSecurityPolicySet(event eventstore.Event) (*handler.Statement, error) { + policyEvent, ok := event.(*instance.SecurityPolicySetEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-83U8p", "reduce.wrong.event.type %s", instance.SecurityPolicySetEventType) + } + + return handler.NewStatement(policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-lhPul", "reduce.wrong.db.pool %T", ex) + } + + var allowedOrigins []string + if policyEvent.AllowedOrigins != nil { + allowedOrigins = *policyEvent.AllowedOrigins + } + + settingsRepo := repository.SecuritySettingsRepository() + settings := domain.SecuritySettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: nil, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + SecuritySettingsAttributes: domain.SecuritySettingsAttributes{ + EnableIframeEmbedding: policyEvent.EnableIframeEmbedding, + AllowedOrigins: allowedOrigins, + EnableImpersonation: policyEvent.EnableImpersonation, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (s *settingsRelationalProjection) reduceOrganizationSettingsSet(event eventstore.Event) (*handler.Statement, error) { + policyEvent, err := assertEvent[*settings.OrganizationSettingsSetEvent](event) + if err != nil { + return nil, err + } + + return handler.NewStatement(policyEvent, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-lhPul", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.OrganizationSettingsRepository() + settings := domain.OrganizationSettings{ + Settings: domain.Settings{ + InstanceID: event.Aggregate().InstanceID, + OrganizationID: &event.Aggregate().ID, + CreatedAt: policyEvent.Creation, + UpdatedAt: policyEvent.Creation, + }, + OrganizationSettingsAttributes: domain.OrganizationSettingsAttributes{ + OrganizationScopedUsernames: &policyEvent.OrganizationScopedUsernames, + }, + } + return settingsRepo.Set(ctx, v3_sql.SQLTx(tx), &settings) + }), nil +} + +func (s *settingsRelationalProjection) reduceOrganizationSettingsRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*settings.OrganizationSettingsRemovedEvent](event) + if err != nil { + return nil, err + } + + return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rHiHb", "reduce.wrong.db.pool %T", ex) + } + + settingsRepo := repository.OrganizationSettingsRepository() + _, err := settingsRepo.Delete(ctx, v3_sql.SQLTx(tx), + settingsRepo.UniqueCondition(e.Aggregate().InstanceID, &e.Aggregate().ID, domain.SettingTypeOrganization, domain.SettingStateActive), + ) + return err + }), nil +}