feat: add notification policy and password change message (#5065)

Implementation of new notification policy with functionality to send email when a password is changed
This commit is contained in:
Stefan Benz
2023-01-25 09:49:41 +01:00
committed by GitHub
parent 8b5894c0bb
commit 19621acfd3
73 changed files with 4196 additions and 83 deletions

View File

@@ -82,6 +82,9 @@ type InstanceSetup struct {
SecondFactorCheckLifetime time.Duration
MultiFactorCheckLifetime time.Duration
}
NotificationPolicy struct {
PasswordChange bool
}
PrivacyPolicy struct {
TOSLink string
PrivacyLink string
@@ -236,6 +239,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
prepareAddMultiFactorToDefaultLoginPolicy(instanceAgg, domain.MultiFactorTypeU2FWithPIN),
prepareAddDefaultPrivacyPolicy(instanceAgg, setup.PrivacyPolicy.TOSLink, setup.PrivacyPolicy.PrivacyLink, setup.PrivacyPolicy.HelpLink),
prepareAddDefaultNotificationPolicy(instanceAgg, setup.NotificationPolicy.PasswordChange),
prepareAddDefaultLockoutPolicy(instanceAgg, setup.LockoutPolicy.MaxAttempts, setup.LockoutPolicy.ShouldShowLockoutFailure),
prepareAddDefaultLabelPolicy(

View File

@@ -0,0 +1,92 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
)
func (c *Commands) AddDefaultNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddDefaultNotificationPolicy(instanceAgg, passwordChange))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func (c *Commands) ChangeDefaultNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareChangeDefaultNotificationPolicy(instanceAgg, passwordChange))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func prepareAddDefaultNotificationPolicy(
a *instance.Aggregate,
passwordChange bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewInstanceNotificationPolicyWriteModel(ctx)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if writeModel.State == domain.PolicyStateActive {
return nil, caos_errs.ThrowAlreadyExists(nil, "INSTANCE-xpo1bj", "Errors.Instance.NotificationPolicy.AlreadyExists")
}
return []eventstore.Command{
instance.NewNotificationPolicyAddedEvent(ctx, &a.Aggregate, passwordChange),
}, nil
}, nil
}
}
func prepareChangeDefaultNotificationPolicy(
a *instance.Aggregate,
passwordChange bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewInstanceNotificationPolicyWriteModel(ctx)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if writeModel.State == domain.PolicyStateUnspecified || writeModel.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-x891na", "Errors.IAM.NotificationPolicy.NotFound")
}
change, hasChanged := writeModel.NewChangedEvent(ctx, &a.Aggregate, passwordChange)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-29x02n", "Errors.IAM.NotificationPolicy.NotChanged")
}
return []eventstore.Command{
change,
}, nil
}, nil
}
}

View File

@@ -0,0 +1,72 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/policy"
)
type InstanceNotificationPolicyWriteModel struct {
NotificationPolicyWriteModel
}
func NewInstanceNotificationPolicyWriteModel(ctx context.Context) *InstanceNotificationPolicyWriteModel {
return &InstanceNotificationPolicyWriteModel{
NotificationPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: authz.GetInstance(ctx).InstanceID(),
ResourceOwner: authz.GetInstance(ctx).InstanceID(),
},
},
}
}
func (wm *InstanceNotificationPolicyWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *instance.NotificationPolicyAddedEvent:
wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyAddedEvent)
case *instance.NotificationPolicyChangedEvent:
wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyChangedEvent)
}
}
}
func (wm *InstanceNotificationPolicyWriteModel) Reduce() error {
return wm.NotificationPolicyWriteModel.Reduce()
}
func (wm *InstanceNotificationPolicyWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(instance.AggregateType).
AggregateIDs(wm.NotificationPolicyWriteModel.AggregateID).
EventTypes(
instance.NotificationPolicyAddedEventType,
instance.NotificationPolicyChangedEventType).
Builder()
}
func (wm *InstanceNotificationPolicyWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
passwordChange bool,
) (*instance.NotificationPolicyChangedEvent, bool) {
changes := make([]policy.NotificationPolicyChanges, 0)
if wm.PasswordChange != passwordChange {
changes = append(changes, policy.ChangePasswordChange(passwordChange))
}
if len(changes) == 0 {
return nil, false
}
changedEvent, err := instance.NewNotificationPolicyChangedEvent(ctx, aggregate, changes)
if err != nil {
return nil, false
}
return changedEvent, true
}

View File

@@ -0,0 +1,260 @@
package command
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/policy"
)
func TestCommandSide_AddDefaultNotificationPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
resourceOwner string
passwordChange bool
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "notification policy already existing, already exists error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewNotificationPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
true,
),
),
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
err: caos_errs.IsErrorAlreadyExists,
},
},
{
name: "add policy,ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
instance.NewNotificationPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
true,
),
),
},
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "add empty policy,ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
instance.NewNotificationPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
true,
),
),
},
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.AddDefaultNotificationPolicy(tt.args.ctx, tt.args.resourceOwner, tt.args.passwordChange)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_ChangeDefaultNotificationPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
resourceOwner string
passwordChange bool
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "privacy policy not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "no changes, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewNotificationPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
true,
),
),
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewNotificationPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
false,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newDefaultNotificationPolicyChangedEvent(context.Background(),
true,
)),
},
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.ChangeDefaultNotificationPolicy(tt.args.ctx, tt.args.resourceOwner, tt.args.passwordChange)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func newDefaultNotificationPolicyChangedEvent(ctx context.Context, passwordChange bool) *instance.NotificationPolicyChangedEvent {
event, _ := instance.NewNotificationPolicyChangedEvent(ctx,
&instance.NewAggregate("INSTANCE").Aggregate,
[]policy.NotificationPolicyChanges{
policy.ChangePasswordChange(passwordChange),
},
)
return event
}

View File

@@ -49,5 +49,6 @@ func orgWriteModelToPrivacyPolicy(wm *OrgPrivacyPolicyWriteModel) *domain.Privac
ObjectRoot: writeModelToObjectRoot(wm.PrivacyPolicyWriteModel.WriteModel),
TOSLink: wm.TOSLink,
PrivacyLink: wm.PrivacyLink,
HelpLink: wm.HelpLink,
}
}

View File

@@ -0,0 +1,139 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
)
func (c *Commands) AddNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-x801sk2i", "Errors.ResourceOwnerMissing")
}
orgAgg := org.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddNotificationPolicy(orgAgg, passwordChange))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func prepareAddNotificationPolicy(
a *org.Aggregate,
passwordChange bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewOrgNotificationPolicyWriteModel(a.Aggregate.ID)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if writeModel.State == domain.PolicyStateActive {
return nil, caos_errs.ThrowAlreadyExists(nil, "Org-xa08n2", "Errors.Org.NotificationPolicy.AlreadyExists")
}
return []eventstore.Command{
org.NewNotificationPolicyAddedEvent(ctx, &a.Aggregate, passwordChange),
}, nil
}, nil
}
}
func (c *Commands) ChangeNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-x091n1g", "Errors.ResourceOwnerMissing")
}
orgAgg := org.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareChangeNotificationPolicy(orgAgg, passwordChange))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func prepareChangeNotificationPolicy(
a *org.Aggregate,
passwordChange bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewOrgNotificationPolicyWriteModel(a.Aggregate.ID)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if writeModel.State == domain.PolicyStateUnspecified || writeModel.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-x029n3", "Errors.Org.NotificationPolicy.NotFound")
}
change, hasChanged := writeModel.NewChangedEvent(ctx, &a.Aggregate, passwordChange)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-ioqnxz", "Errors.Org.NotificationPolicy.NotChanged")
}
return []eventstore.Command{
change,
}, nil
}, nil
}
}
func (c *Commands) RemoveNotificationPolicy(ctx context.Context, resourceOwner string) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-x89ns2", "Errors.ResourceOwnerMissing")
}
orgAgg := org.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveNotificationPolicy(orgAgg))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func prepareRemoveNotificationPolicy(
a *org.Aggregate,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewOrgNotificationPolicyWriteModel(a.Aggregate.ID)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if writeModel.State == domain.PolicyStateUnspecified || writeModel.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-x029n1s", "Errors.Org.NotificationPolicy.NotFound")
}
return []eventstore.Command{
org.NewNotificationPolicyRemovedEvent(ctx, &a.Aggregate),
}, nil
}, nil
}
}

View File

@@ -0,0 +1,73 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/policy"
)
type OrgNotificationPolicyWriteModel struct {
NotificationPolicyWriteModel
}
func NewOrgNotificationPolicyWriteModel(orgID string) *OrgNotificationPolicyWriteModel {
return &OrgNotificationPolicyWriteModel{
NotificationPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: orgID,
ResourceOwner: orgID,
},
},
}
}
func (wm *OrgNotificationPolicyWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *org.NotificationPolicyAddedEvent:
wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyAddedEvent)
case *org.NotificationPolicyChangedEvent:
wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyChangedEvent)
case *org.NotificationPolicyRemovedEvent:
wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyRemovedEvent)
}
}
}
func (wm *OrgNotificationPolicyWriteModel) Reduce() error {
return wm.NotificationPolicyWriteModel.Reduce()
}
func (wm *OrgNotificationPolicyWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateIDs(wm.NotificationPolicyWriteModel.AggregateID).
AggregateTypes(org.AggregateType).
EventTypes(org.NotificationPolicyAddedEventType,
org.NotificationPolicyChangedEventType,
org.NotificationPolicyRemovedEventType).
Builder()
}
func (wm *OrgNotificationPolicyWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
passwordChange bool,
) (*org.NotificationPolicyChangedEvent, bool) {
changes := make([]policy.NotificationPolicyChanges, 0)
if wm.PasswordChange != passwordChange {
changes = append(changes, policy.ChangePasswordChange(passwordChange))
}
if len(changes) == 0 {
return nil, false
}
changedEvent, err := org.NewNotificationPolicyChangedEvent(ctx, aggregate, changes)
if err != nil {
return nil, false
}
return changedEvent, true
}

View File

@@ -0,0 +1,391 @@
package command
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/policy"
)
func TestCommandSide_AddNotificationPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
orgID string
passwordChange bool
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "org id missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
orgID: "",
passwordChange: true,
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "policy already existing, already exists error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
true,
),
),
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: true,
},
res: res{
err: caos_errs.IsErrorAlreadyExists,
},
},
{
name: "add policy,ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
true,
),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: true,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "add policy empty, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
false,
),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: false,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.AddNotificationPolicy(tt.args.ctx, tt.args.orgID, tt.args.passwordChange)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_ChangeNotificationPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
orgID string
passwordChange bool
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "org id missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
passwordChange: true,
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "policy not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: true,
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "no changes, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
true,
),
),
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: true,
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
true,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newNotificationPolicyChangedEvent(context.Background(), "org1", false),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: false,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.ChangeNotificationPolicy(tt.args.ctx, tt.args.orgID, tt.args.passwordChange)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_RemoveNotificationPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
orgID string
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "org id missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "policy not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "remove, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
true,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewNotificationPolicyRemovedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.RemoveNotificationPolicy(tt.args.ctx, tt.args.orgID)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func newNotificationPolicyChangedEvent(ctx context.Context, orgID string, passwordChange bool) *org.NotificationPolicyChangedEvent {
event, _ := org.NewNotificationPolicyChangedEvent(ctx,
&org.NewAggregate(orgID).Aggregate,
[]policy.NotificationPolicyChanges{
policy.ChangePasswordChange(passwordChange),
},
)
return event
}

View File

@@ -0,0 +1,31 @@
package command
import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/policy"
)
type NotificationPolicyWriteModel struct {
eventstore.WriteModel
PasswordChange bool
State domain.PolicyState
}
func (wm *NotificationPolicyWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *policy.NotificationPolicyAddedEvent:
wm.PasswordChange = e.PasswordChange
wm.State = domain.PolicyStateActive
case *policy.NotificationPolicyChangedEvent:
if e.PasswordChange != nil {
wm.PasswordChange = *e.PasswordChange
}
case *policy.NotificationPolicyRemovedEvent:
wm.State = domain.PolicyStateRemoved
}
}
return wm.WriteModel.Reduce()
}

View File

@@ -196,6 +196,23 @@ func (c *Commands) PasswordCodeSent(ctx context.Context, orgID, userID string) (
return err
}
func (c *Commands) PasswordChangeSent(ctx context.Context, orgID, userID string) (err error) {
if userID == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-pqlm2n", "Errors.User.UserIDMissing")
}
existingPassword, err := c.passwordWriteModel(ctx, userID, orgID)
if err != nil {
return err
}
if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-x902b2v", "Errors.User.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
_, err = c.eventstore.Push(ctx, user.NewHumanPasswordChangeSentEvent(ctx, userAgg))
return err
}
func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, password string, authRequest *domain.AuthRequest, lockoutPolicy *domain.LockoutPolicy) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()