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

@@ -252,6 +252,54 @@ func (s *Server) ResetCustomDomainClaimedMessageTextToDefault(ctx context.Contex
}, nil
}
func (s *Server) GetDefaultPasswordChangeMessageText(ctx context.Context, req *admin_pb.GetDefaultPasswordChangeMessageTextRequest) (*admin_pb.GetDefaultPasswordChangeMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.PasswordChangeMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultPasswordChangeMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomPasswordChangeMessageText(ctx context.Context, req *admin_pb.GetCustomPasswordChangeMessageTextRequest) (*admin_pb.GetCustomPasswordChangeMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.PasswordChangeMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomPasswordChangeMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultPasswordChangeMessageText(ctx context.Context, req *admin_pb.SetDefaultPasswordChangeMessageTextRequest) (*admin_pb.SetDefaultPasswordChangeMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetPasswordChangeCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultPasswordChangeMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomPasswordChangeMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomPasswordChangeMessageTextToDefaultRequest) (*admin_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.PasswordChangeMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultPasswordlessRegistrationMessageText(ctx context.Context, req *admin_pb.GetDefaultPasswordlessRegistrationMessageTextRequest) (*admin_pb.GetDefaultPasswordlessRegistrationMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.PasswordlessRegistrationMessageType, req.Language)
if err != nil {

View File

@@ -83,6 +83,21 @@ func SetDomainClaimedCustomTextToDomain(msg *admin_pb.SetDefaultDomainClaimedMes
}
}
func SetPasswordChangeCustomTextToDomain(msg *admin_pb.SetDefaultPasswordChangeMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.PasswordChangeMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetPasswordlessRegistrationCustomTextToDomain(msg *admin_pb.SetDefaultPasswordlessRegistrationMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{

View File

@@ -0,0 +1,46 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) AddNotificationPolicy(ctx context.Context, req *admin_pb.AddNotificationPolicyRequest) (*admin_pb.AddNotificationPolicyResponse, error) {
result, err := s.command.AddDefaultNotificationPolicy(ctx, authz.GetInstance(ctx).InstanceID(), req.GetPasswordChange())
if err != nil {
return nil, err
}
return &admin_pb.AddNotificationPolicyResponse{
Details: object.AddToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetNotificationPolicy(ctx context.Context, _ *admin_pb.GetNotificationPolicyRequest) (*admin_pb.GetNotificationPolicyResponse, error) {
policy, err := s.query.DefaultNotificationPolicy(ctx, true)
if err != nil {
return nil, err
}
return &admin_pb.GetNotificationPolicyResponse{Policy: policy_grpc.ModelNotificationPolicyToPb(policy)}, nil
}
func (s *Server) UpdateNotificationPolicy(ctx context.Context, req *admin_pb.UpdateNotificationPolicyRequest) (*admin_pb.UpdateNotificationPolicyResponse, error) {
result, err := s.command.ChangeDefaultNotificationPolicy(ctx, authz.GetInstance(ctx).InstanceID(), req.GetPasswordChange())
if err != nil {
return nil, err
}
return &admin_pb.UpdateNotificationPolicyResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}

View File

@@ -252,6 +252,54 @@ func (s *Server) ResetCustomDomainClaimedMessageTextToDefault(ctx context.Contex
}, nil
}
func (s *Server) GetCustomPasswordChangeMessageText(ctx context.Context, req *mgmt_pb.GetCustomPasswordChangeMessageTextRequest) (*mgmt_pb.GetCustomPasswordChangeMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordChangeMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &mgmt_pb.GetCustomPasswordChangeMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetDefaultPasswordChangeMessageText(ctx context.Context, req *mgmt_pb.GetDefaultPasswordChangeMessageTextRequest) (*mgmt_pb.GetDefaultPasswordChangeMessageTextResponse, error) {
msg, err := s.query.IAMMessageTextByTypeAndLanguage(ctx, domain.PasswordChangeMessageType, req.Language)
if err != nil {
return nil, err
}
return &mgmt_pb.GetDefaultPasswordChangeMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetCustomPasswordChangeMessageCustomText(ctx context.Context, req *mgmt_pb.SetCustomPasswordChangeMessageTextRequest) (*mgmt_pb.SetCustomPasswordChangeMessageTextResponse, error) {
result, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, SetPasswordChangeCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &mgmt_pb.SetCustomPasswordChangeMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomPasswordChangeMessageTextToDefault(ctx context.Context, req *mgmt_pb.ResetCustomPasswordChangeMessageTextToDefaultRequest) (*mgmt_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveOrgMessageTexts(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordChangeMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &mgmt_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetCustomPasswordlessRegistrationMessageText(ctx context.Context, req *mgmt_pb.GetCustomPasswordlessRegistrationMessageTextRequest) (*mgmt_pb.GetCustomPasswordlessRegistrationMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordlessRegistrationMessageType, req.Language, false)
if err != nil {

View File

@@ -83,6 +83,21 @@ func SetDomainClaimedCustomTextToDomain(msg *mgmt_pb.SetCustomDomainClaimedMessa
}
}
func SetPasswordChangeCustomTextToDomain(msg *mgmt_pb.SetCustomPasswordChangeMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.PasswordChangeMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetPasswordlessRegistrationCustomTextToDomain(msg *mgmt_pb.SetCustomPasswordlessRegistrationMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{

View File

@@ -0,0 +1,64 @@
package management
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management"
)
func (s *Server) GetNotificationPolicy(ctx context.Context, _ *mgmt_pb.GetNotificationPolicyRequest) (*mgmt_pb.GetNotificationPolicyResponse, error) {
policy, err := s.query.NotificationPolicyByOrg(ctx, true, authz.GetCtxData(ctx).OrgID, false)
if err != nil {
return nil, err
}
return &mgmt_pb.GetNotificationPolicyResponse{Policy: policy_grpc.ModelNotificationPolicyToPb(policy)}, nil
}
func (s *Server) GetDefaultNotificationPolicy(ctx context.Context, _ *mgmt_pb.GetDefaultNotificationPolicyRequest) (*mgmt_pb.GetDefaultNotificationPolicyResponse, error) {
policy, err := s.query.DefaultNotificationPolicy(ctx, true)
if err != nil {
return nil, err
}
return &mgmt_pb.GetDefaultNotificationPolicyResponse{Policy: policy_grpc.ModelNotificationPolicyToPb(policy)}, nil
}
func (s *Server) AddCustomNotificationPolicy(ctx context.Context, req *mgmt_pb.AddCustomNotificationPolicyRequest) (*mgmt_pb.AddCustomNotificationPolicyResponse, error) {
result, err := s.command.AddNotificationPolicy(ctx, authz.GetCtxData(ctx).OrgID, req.GetPasswordChange())
if err != nil {
return nil, err
}
return &mgmt_pb.AddCustomNotificationPolicyResponse{
Details: object.AddToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) UpdateCustomNotificationPolicy(ctx context.Context, req *mgmt_pb.UpdateCustomNotificationPolicyRequest) (*mgmt_pb.UpdateCustomNotificationPolicyResponse, error) {
result, err := s.command.ChangeNotificationPolicy(ctx, authz.GetCtxData(ctx).OrgID, req.GetPasswordChange())
if err != nil {
return nil, err
}
return &mgmt_pb.UpdateCustomNotificationPolicyResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetNotificationPolicyToDefault(ctx context.Context, _ *mgmt_pb.ResetNotificationPolicyToDefaultRequest) (*mgmt_pb.ResetNotificationPolicyToDefaultResponse, error) {
objectDetails, err := s.command.RemoveNotificationPolicy(ctx, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
return &mgmt_pb.ResetNotificationPolicyToDefaultResponse{
Details: object.DomainToChangeDetailsPb(objectDetails),
}, nil
}

View File

@@ -0,0 +1,20 @@
package policy
import (
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/query"
policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy"
)
func ModelNotificationPolicyToPb(policy *query.NotificationPolicy) *policy_pb.NotificationPolicy {
return &policy_pb.NotificationPolicy{
IsDefault: policy.IsDefault,
PasswordChange: policy.PasswordChange,
Details: object.ToViewDetailsPb(
policy.Sequence,
policy.CreationDate,
policy.ChangeDate,
policy.ResourceOwner,
),
}
}

View File

@@ -52,6 +52,10 @@ var (
}
)
func LoginHintLink(origin, username string) string {
return origin + HandlerPrefix + "?login_hint=" + username
}
func (i *spaHandler) Open(name string) (http.File, error) {
ret, err := i.fileSystem.Open(name)
if !os.IsNotExist(err) || path.Ext(name) != "" {

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) }()

View File

@@ -13,6 +13,7 @@ const (
VerifyPhoneMessageType = "VerifyPhone"
DomainClaimedMessageType = "DomainClaimed"
PasswordlessRegistrationMessageType = "PasswordlessRegistration"
PasswordChangeMessageType = "PasswordChange"
MessageTitle = "Title"
MessagePreHeader = "PreHeader"
MessageSubject = "Subject"
@@ -29,6 +30,7 @@ type MessageTexts struct {
VerifyPhone CustomMessageText
DomainClaimed CustomMessageText
PasswordlessRegistration CustomMessageText
PasswordChange CustomMessageText
}
type CustomMessageText struct {
@@ -65,6 +67,8 @@ func (m *MessageTexts) GetMessageTextByType(msgType string) *CustomMessageText {
return &m.DomainClaimed
case PasswordlessRegistrationMessageType:
return &m.PasswordlessRegistration
case PasswordChangeMessageType:
return &m.PasswordChange
}
return nil
}
@@ -75,5 +79,6 @@ func IsMessageTextType(textType string) bool {
textType == VerifyEmailMessageType ||
textType == VerifyPhoneMessageType ||
textType == DomainClaimedMessageType ||
textType == PasswordlessRegistrationMessageType
textType == PasswordlessRegistrationMessageType ||
textType == PasswordChangeMessageType
}

View File

@@ -137,6 +137,10 @@ func (p *notificationsProjection) reducers() []handler.AggregateReducer {
Event: user.HumanPhoneCodeAddedType,
Reduce: p.reducePhoneCodeAdded,
},
{
Event: user.HumanPasswordChangedType,
Reduce: p.reducePasswordChanged,
},
},
},
}
@@ -463,6 +467,74 @@ func (p *notificationsProjection) reducePasswordlessCodeRequested(event eventsto
return crdb.NewNoOpStatement(e), nil
}
func (p *notificationsProjection) reducePasswordChanged(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanPasswordChangedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Yko2z8", "reduce.wrong.event.type %s", user.HumanPasswordChangedType)
}
ctx := setNotificationContext(event.Aggregate())
alreadyHandled, err := p.checkIfAlreadyHandled(ctx, event, nil, user.HumanPasswordChangeSentType)
if err != nil {
return nil, err
}
if alreadyHandled {
return crdb.NewNoOpStatement(e), nil
}
notificationPolicy, err := p.queries.NotificationPolicyByOrg(ctx, true, e.Aggregate().ResourceOwner, false)
if errors.IsNotFound(err) {
return crdb.NewNoOpStatement(e), nil
}
if err != nil {
return nil, err
}
if notificationPolicy.PasswordChange {
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
if err != nil {
return nil, err
}
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
if err != nil {
return nil, err
}
notifyUser, err := p.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID, false)
if err != nil {
return nil, err
}
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordChangeMessageType)
if err != nil {
return nil, err
}
ctx, origin, err := p.origin(ctx)
if err != nil {
return nil, err
}
err = types.SendEmail(
ctx,
string(template.Template),
translator,
notifyUser,
p.getSMTPConfig,
p.getFileSystemProvider,
p.getLogProvider,
colors,
p.assetsPrefix(ctx),
).SendPasswordChange(notifyUser, origin)
if err != nil {
return nil, err
}
err = p.commands.PasswordChangeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
if err != nil {
return nil, err
}
}
return crdb.NewNoOpStatement(e), nil
}
func (p *notificationsProjection) reducePhoneCodeAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanPhoneCodeAddedEvent)
if !ok {

View File

@@ -40,3 +40,10 @@ PasswordlessRegistration:
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Wir haben eine Anfrage für das Hinzufügen eines Token für den passwortlosen Login erhalten. Du kannst den untenstehenden Button verwenden, um dein Token oder Gerät hinzuzufügen.
ButtonText: Passwortlosen Login hinzufügen
PasswordChange:
Title: ZITADEL - Passwort von Benutzer wurde geändert
PreHeader: Passwort Änderung
Subject: Passwort von Benutzer wurde geändert
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Das Password vom Benutzer wurde geändert, wenn diese Änderung von jemand anderem gemacht wurde, empfehlen wir die sofortige Zurücksetzung ihres Passworts.
ButtonText: Login

View File

@@ -40,3 +40,10 @@ PasswordlessRegistration:
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: We received a request to add a token for passwordless login. Please use the button below to add your token or device for passwordless login.
ButtonText: Add Passwordless Login
PasswordChange:
Title: ZITADEL - Password of user has changed
PreHeader: Change password
Subject: Password of user has changed
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: The password of your user has changed, if this change was not done by you, please be advised to immediately reset your password.
ButtonText: Login

View File

@@ -40,3 +40,10 @@ PasswordlessRegistration:
Greeting: Bonjour {{.FirstName}} {{.LastName}},
Text: Nous avons reçu une demande d'ajout d'un jeton pour la connexion sans mot de passe. Veuillez utiliser le bouton ci-dessous pour ajouter votre jeton ou dispositif pour la connexion sans mot de passe.
ButtonText: Ajouter une connexion sans mot de passe
PasswordChange:
Title: ZITADEL - Le mot de passe de l'utilisateur a changé
PreHeader: Modifier le mot de passe
Subject: Le mot de passe de l'utilisateur a changé
Greeting: Bonjour {{.FirstName}} {{.LastName}},
Text: Le mot de passe de votre utilisateur a changé, si ce changement n'a pas été fait par vous, nous vous conseillons de réinitialiser immédiatement votre mot de passe.
ButtonText: Login

View File

@@ -40,3 +40,10 @@ PasswordlessRegistration:
Greeting: 'Ciao {{.FirstName}} {{.LastName}},'
Text: Abbiamo ricevuto una richiesta per aggiungere l'autenticazione passwordless. Usa il pulsante qui sotto per aggiungere il tuo token o dispositivo per il login senza password.
ButtonText: Attiva passwordless
PasswordChange:
Title: ZITADEL - La password dell'utente è stata modificata
PreHeader: Modifica della password
Subject: La password dell'utente è stata modificata
Greeting: Ciao {{.FirstName}} {{.LastName}},
Text: La password del vostro utente è cambiata; se questa modifica non è stata fatta da voi, vi consigliamo di reimpostare immediatamente la vostra password.
ButtonText: Login

View File

@@ -40,3 +40,10 @@ PasswordlessRegistration:
Greeting: 你好 {{.FirstName}} {{.LastName}},
Text: 我们收到了为无密码登录添加令牌的请求。请使用下面的按钮添加您的令牌或设备以进行无密码登录。
ButtonText: 添加无密码登录
PasswordChange:
Title: ZITADEL - 用户的密码已经改变
PreHeader: 更改密码
Subject: 用户的密码已经改变
Greeting: 你好 {{.FirstName}} {{.LastName}},
Text: 您的用户的密码已经改变,如果这个改变不是由您做的,请注意立即重新设置您的密码。
ButtonText: 登录

View File

@@ -0,0 +1,13 @@
package types
import (
"github.com/zitadel/zitadel/internal/api/ui/console"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
)
func (notify Notify) SendPasswordChange(user *query.NotifyUser, origin string) error {
url := console.LoginHintLink(origin, user.PreferredLoginName)
args := make(map[string]interface{})
return notify(url, args, domain.PasswordChangeMessageType, true)
}

View File

@@ -29,6 +29,7 @@ type MessageTexts struct {
VerifyPhone MessageText
DomainClaimed MessageText
PasswordlessRegistration MessageText
PasswordChange MessageText
}
type MessageText struct {
@@ -330,6 +331,8 @@ func (m *MessageTexts) GetMessageTextByType(msgType string) *MessageText {
return &m.DomainClaimed
case domain.PasswordlessRegistrationMessageType:
return &m.PasswordlessRegistration
case domain.PasswordChangeMessageType:
return &m.PasswordChange
}
return nil
}

View File

@@ -0,0 +1,166 @@
package query
import (
"context"
"database/sql"
errs "errors"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
type NotificationPolicy struct {
ID string
Sequence uint64
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
State domain.PolicyState
PasswordChange bool
IsDefault bool
}
var (
notificationPolicyTable = table{
name: projection.NotificationPolicyProjectionTable,
instanceIDCol: projection.NotificationPolicyColumnInstanceID,
}
NotificationPolicyColID = Column{
name: projection.NotificationPolicyColumnID,
table: notificationPolicyTable,
}
NotificationPolicyColSequence = Column{
name: projection.NotificationPolicyColumnSequence,
table: notificationPolicyTable,
}
NotificationPolicyColCreationDate = Column{
name: projection.NotificationPolicyColumnCreationDate,
table: notificationPolicyTable,
}
NotificationPolicyColChangeDate = Column{
name: projection.NotificationPolicyColumnChangeDate,
table: notificationPolicyTable,
}
NotificationPolicyColResourceOwner = Column{
name: projection.NotificationPolicyColumnResourceOwner,
table: notificationPolicyTable,
}
NotificationPolicyColInstanceID = Column{
name: projection.NotificationPolicyColumnInstanceID,
table: notificationPolicyTable,
}
NotificationPolicyColPasswordChange = Column{
name: projection.NotificationPolicyColumnPasswordChange,
table: notificationPolicyTable,
}
NotificationPolicyColIsDefault = Column{
name: projection.NotificationPolicyColumnIsDefault,
table: notificationPolicyTable,
}
NotificationPolicyColState = Column{
name: projection.NotificationPolicyColumnStateCol,
table: notificationPolicyTable,
}
NotificationPolicyColOwnerRemoved = Column{
name: projection.NotificationPolicyColumnOwnerRemoved,
table: notificationPolicyTable,
}
)
func (q *Queries) NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, orgID string, withOwnerRemoved bool) (_ *NotificationPolicy, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
if err := projection.NotificationPolicyProjection.Trigger(ctx); err != nil {
return nil, err
}
}
eq := sq.Eq{NotificationPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
if !withOwnerRemoved {
eq[NotificationPolicyColOwnerRemoved.identifier()] = false
}
stmt, scan := prepareNotificationPolicyQuery()
query, args, err := stmt.Where(
sq.And{
eq,
sq.Or{
sq.Eq{NotificationPolicyColID.identifier(): orgID},
sq.Eq{NotificationPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID()},
},
}).
OrderBy(NotificationPolicyColIsDefault.identifier()).Limit(1).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-Xuoapqm", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, query, args...)
return scan(row)
}
func (q *Queries) DefaultNotificationPolicy(ctx context.Context, shouldTriggerBulk bool) (_ *NotificationPolicy, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
if err := projection.NotificationPolicyProjection.Trigger(ctx); err != nil {
return nil, err
}
}
stmt, scan := prepareNotificationPolicyQuery()
query, args, err := stmt.Where(sq.Eq{
NotificationPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID(),
NotificationPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}).
OrderBy(NotificationPolicyColIsDefault.identifier()).
Limit(1).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-xlqp209", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, query, args...)
return scan(row)
}
func prepareNotificationPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*NotificationPolicy, error)) {
return sq.Select(
NotificationPolicyColID.identifier(),
NotificationPolicyColSequence.identifier(),
NotificationPolicyColCreationDate.identifier(),
NotificationPolicyColChangeDate.identifier(),
NotificationPolicyColResourceOwner.identifier(),
NotificationPolicyColPasswordChange.identifier(),
NotificationPolicyColIsDefault.identifier(),
NotificationPolicyColState.identifier(),
).
From(notificationPolicyTable.identifier()).PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*NotificationPolicy, error) {
policy := new(NotificationPolicy)
err := row.Scan(
&policy.ID,
&policy.Sequence,
&policy.CreationDate,
&policy.ChangeDate,
&policy.ResourceOwner,
&policy.PasswordChange,
&policy.IsDefault,
&policy.State,
)
if err != nil {
if errs.Is(err, sql.ErrNoRows) {
return nil, errors.ThrowNotFound(err, "QUERY-x0so2p", "Errors.NotificationPolicy.NotFound")
}
return nil, errors.ThrowInternal(err, "QUERY-Zixoooq", "Errors.Internal")
}
return policy, nil
}
}

View File

@@ -0,0 +1,116 @@
package query
import (
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"regexp"
"testing"
"github.com/zitadel/zitadel/internal/domain"
errs "github.com/zitadel/zitadel/internal/errors"
)
var notificationPolicyStmt = regexp.QuoteMeta(`SELECT projections.notification_policies.id,` +
` projections.notification_policies.sequence,` +
` projections.notification_policies.creation_date,` +
` projections.notification_policies.change_date,` +
` projections.notification_policies.resource_owner,` +
` projections.notification_policies.password_change,` +
` projections.notification_policies.is_default,` +
` projections.notification_policies.state` +
` FROM projections.notification_policies`)
func Test_NotificationPolicyPrepares(t *testing.T) {
type want struct {
sqlExpectations sqlExpectation
err checkErr
}
tests := []struct {
name string
prepare interface{}
want want
object interface{}
}{
{
name: "prepareNotificationPolicyQuery no result",
prepare: prepareNotificationPolicyQuery,
want: want{
sqlExpectations: mockQueries(
notificationPolicyStmt,
nil,
nil,
),
err: func(err error) (error, bool) {
if !errs.IsNotFound(err) {
return fmt.Errorf("err should be NotFoundError got: %w", err), false
}
return nil, true
},
},
object: (*NotificationPolicy)(nil),
},
{
name: "prepareNotificationPolicyQuery found",
prepare: prepareNotificationPolicyQuery,
want: want{
sqlExpectations: mockQuery(
notificationPolicyStmt,
[]string{
"id",
"sequence",
"creation_date",
"change_date",
"resource_owner",
"password_change",
"is_default",
"state",
},
[]driver.Value{
"pol-id",
uint64(20211109),
testNow,
testNow,
"ro",
true,
true,
domain.PolicyStateActive,
},
),
},
object: &NotificationPolicy{
ID: "pol-id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211109,
ResourceOwner: "ro",
State: domain.PolicyStateActive,
PasswordChange: true,
IsDefault: true,
},
},
{
name: "prepareNotificationPolicyQuery sql err",
prepare: prepareNotificationPolicyQuery,
want: want{
sqlExpectations: mockQueryErr(
notificationPolicyStmt,
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err)
})
}
}

View File

@@ -273,7 +273,8 @@ func isMessageTemplate(template string) bool {
template == domain.VerifyEmailMessageType ||
template == domain.VerifyPhoneMessageType ||
template == domain.DomainClaimedMessageType ||
template == domain.PasswordlessRegistrationMessageType
template == domain.PasswordlessRegistrationMessageType ||
template == domain.PasswordChangeMessageType
}
func isTitle(key string) bool {
return key == domain.MessageTitle

View File

@@ -0,0 +1,187 @@
package projection
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/policy"
)
const (
NotificationPolicyProjectionTable = "projections.notification_policies"
NotificationPolicyColumnID = "id"
NotificationPolicyColumnCreationDate = "creation_date"
NotificationPolicyColumnChangeDate = "change_date"
NotificationPolicyColumnResourceOwner = "resource_owner"
NotificationPolicyColumnInstanceID = "instance_id"
NotificationPolicyColumnSequence = "sequence"
NotificationPolicyColumnStateCol = "state"
NotificationPolicyColumnIsDefault = "is_default"
NotificationPolicyColumnPasswordChange = "password_change"
NotificationPolicyColumnOwnerRemoved = "owner_removed"
)
type notificationPolicyProjection struct {
crdb.StatementHandler
}
func newNotificationPolicyProjection(ctx context.Context, config crdb.StatementHandlerConfig) *notificationPolicyProjection {
p := new(notificationPolicyProjection)
config.ProjectionName = NotificationPolicyProjectionTable
config.Reducers = p.reducers()
config.InitCheck = crdb.NewTableCheck(
crdb.NewTable([]*crdb.Column{
crdb.NewColumn(NotificationPolicyColumnID, crdb.ColumnTypeText),
crdb.NewColumn(NotificationPolicyColumnCreationDate, crdb.ColumnTypeTimestamp),
crdb.NewColumn(NotificationPolicyColumnChangeDate, crdb.ColumnTypeTimestamp),
crdb.NewColumn(NotificationPolicyColumnResourceOwner, crdb.ColumnTypeText),
crdb.NewColumn(NotificationPolicyColumnInstanceID, crdb.ColumnTypeText),
crdb.NewColumn(NotificationPolicyColumnSequence, crdb.ColumnTypeInt64),
crdb.NewColumn(NotificationPolicyColumnStateCol, crdb.ColumnTypeEnum),
crdb.NewColumn(NotificationPolicyColumnIsDefault, crdb.ColumnTypeBool),
crdb.NewColumn(NotificationPolicyColumnPasswordChange, crdb.ColumnTypeBool),
crdb.NewColumn(NotificationPolicyColumnOwnerRemoved, crdb.ColumnTypeBool, crdb.Default(false)),
},
crdb.NewPrimaryKey(NotificationPolicyColumnInstanceID, NotificationPolicyColumnID),
),
)
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p
}
func (p *notificationPolicyProjection) reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: org.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: org.NotificationPolicyAddedEventType,
Reduce: p.reduceAdded,
},
{
Event: org.NotificationPolicyChangedEventType,
Reduce: p.reduceChanged,
},
{
Event: org.NotificationPolicyRemovedEventType,
Reduce: p.reduceRemoved,
},
{
Event: org.OrgRemovedEventType,
Reduce: p.reduceOwnerRemoved,
},
},
},
{
Aggregate: instance.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(NotificationPolicyColumnInstanceID),
},
{
Event: instance.NotificationPolicyAddedEventType,
Reduce: p.reduceAdded,
},
{
Event: instance.NotificationPolicyChangedEventType,
Reduce: p.reduceChanged,
},
},
},
}
}
func (p *notificationPolicyProjection) reduceAdded(event eventstore.Event) (*handler.Statement, error) {
var policyEvent policy.NotificationPolicyAddedEvent
var isDefault bool
switch e := event.(type) {
case *org.NotificationPolicyAddedEvent:
policyEvent = e.NotificationPolicyAddedEvent
isDefault = false
case *instance.NotificationPolicyAddedEvent:
policyEvent = e.NotificationPolicyAddedEvent
isDefault = true
default:
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-x02s1m", "reduce.wrong.event.type %v", []eventstore.EventType{org.NotificationPolicyAddedEventType, instance.NotificationPolicyAddedEventType})
}
return crdb.NewCreateStatement(
&policyEvent,
[]handler.Column{
handler.NewCol(NotificationPolicyColumnCreationDate, policyEvent.CreationDate()),
handler.NewCol(NotificationPolicyColumnChangeDate, policyEvent.CreationDate()),
handler.NewCol(NotificationPolicyColumnSequence, policyEvent.Sequence()),
handler.NewCol(NotificationPolicyColumnID, policyEvent.Aggregate().ID),
handler.NewCol(NotificationPolicyColumnStateCol, domain.PolicyStateActive),
handler.NewCol(NotificationPolicyColumnPasswordChange, policyEvent.PasswordChange),
handler.NewCol(NotificationPolicyColumnIsDefault, isDefault),
handler.NewCol(NotificationPolicyColumnResourceOwner, policyEvent.Aggregate().ResourceOwner),
handler.NewCol(NotificationPolicyColumnInstanceID, policyEvent.Aggregate().InstanceID),
}), nil
}
func (p *notificationPolicyProjection) reduceChanged(event eventstore.Event) (*handler.Statement, error) {
var policyEvent policy.NotificationPolicyChangedEvent
switch e := event.(type) {
case *org.NotificationPolicyChangedEvent:
policyEvent = e.NotificationPolicyChangedEvent
case *instance.NotificationPolicyChangedEvent:
policyEvent = e.NotificationPolicyChangedEvent
default:
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-psom2h19", "reduce.wrong.event.type %v", []eventstore.EventType{org.NotificationPolicyChangedEventType, instance.NotificationPolicyChangedEventType})
}
cols := []handler.Column{
handler.NewCol(NotificationPolicyColumnChangeDate, policyEvent.CreationDate()),
handler.NewCol(NotificationPolicyColumnSequence, policyEvent.Sequence()),
}
if policyEvent.PasswordChange != nil {
cols = append(cols, handler.NewCol(NotificationPolicyColumnPasswordChange, *policyEvent.PasswordChange))
}
return crdb.NewUpdateStatement(
&policyEvent,
cols,
[]handler.Condition{
handler.NewCond(NotificationPolicyColumnID, policyEvent.Aggregate().ID),
handler.NewCond(NotificationPolicyColumnInstanceID, policyEvent.Aggregate().InstanceID),
}), nil
}
func (p *notificationPolicyProjection) reduceRemoved(event eventstore.Event) (*handler.Statement, error) {
policyEvent, ok := event.(*org.NotificationPolicyRemovedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-Po2iso2", "reduce.wrong.event.type %s", org.NotificationPolicyRemovedEventType)
}
return crdb.NewDeleteStatement(
policyEvent,
[]handler.Condition{
handler.NewCond(NotificationPolicyColumnID, policyEvent.Aggregate().ID),
handler.NewCond(NotificationPolicyColumnInstanceID, policyEvent.Aggregate().InstanceID),
}), nil
}
func (p *notificationPolicyProjection) reduceOwnerRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*org.OrgRemovedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-poxi9a", "reduce.wrong.event.type %s", org.OrgRemovedEventType)
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(DomainPolicyChangeDateCol, e.CreationDate()),
handler.NewCol(DomainPolicySequenceCol, e.Sequence()),
handler.NewCol(DomainPolicyOwnerRemovedCol, true),
},
[]handler.Condition{
handler.NewCond(DomainPolicyInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCond(DomainPolicyResourceOwnerCol, e.Aggregate().ID),
},
), nil
}

View File

@@ -0,0 +1,258 @@
package projection
import (
"testing"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
)
func TestNotificationPolicyProjection_reduces(t *testing.T) {
type args struct {
event func(t *testing.T) eventstore.Event
}
tests := []struct {
name string
args args
reduce func(event eventstore.Event) (*handler.Statement, error)
want wantReduce
}{
{
name: "org reduceAdded",
args: args{
event: getEvent(testEvent(
repository.EventType(org.NotificationPolicyAddedEventType),
org.AggregateType,
[]byte(`{
"passwordChange": true
}`),
), org.NotificationPolicyAddedEventMapper),
},
reduce: (&notificationPolicyProjection{}).reduceAdded,
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.notification_policies (creation_date, change_date, sequence, id, state, password_change, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
uint64(15),
"agg-id",
domain.PolicyStateActive,
true,
false,
"ro-id",
"instance-id",
},
},
},
},
},
},
{
name: "org reduceChanged",
reduce: (&notificationPolicyProjection{}).reduceChanged,
args: args{
event: getEvent(testEvent(
repository.EventType(org.NotificationPolicyChangedEventType),
org.AggregateType,
[]byte(`{
"passwordChange": true
}`),
), org.NotificationPolicyChangedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.notification_policies SET (change_date, sequence, password_change) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
true,
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "org reduceRemoved",
reduce: (&notificationPolicyProjection{}).reduceRemoved,
args: args{
event: getEvent(testEvent(
repository.EventType(org.NotificationPolicyRemovedEventType),
org.AggregateType,
nil,
), org.NotificationPolicyRemovedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.notification_policies WHERE (id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
},
},
},
},
},
}, {
name: "instance reduceInstanceRemoved",
args: args{
event: getEvent(testEvent(
repository.EventType(instance.InstanceRemovedEventType),
instance.AggregateType,
nil,
), instance.InstanceRemovedEventMapper),
},
reduce: reduceInstanceRemovedHelper(NotificationPolicyColumnInstanceID),
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.notification_policies WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},
},
},
},
},
},
{
name: "instance reduceAdded",
reduce: (&notificationPolicyProjection{}).reduceAdded,
args: args{
event: getEvent(testEvent(
repository.EventType(instance.NotificationPolicyAddedEventType),
instance.AggregateType,
[]byte(`{
"passwordChange": true
}`),
), instance.NotificationPolicyAddedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.notification_policies (creation_date, change_date, sequence, id, state, password_change, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
uint64(15),
"agg-id",
domain.PolicyStateActive,
true,
true,
"ro-id",
"instance-id",
},
},
},
},
},
},
{
name: "instance reduceChanged",
reduce: (&notificationPolicyProjection{}).reduceChanged,
args: args{
event: getEvent(testEvent(
repository.EventType(instance.NotificationPolicyChangedEventType),
instance.AggregateType,
[]byte(`{
"passwordChange": true
}`),
), instance.NotificationPolicyChangedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.notification_policies SET (change_date, sequence, password_change) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
true,
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "org.reduceOwnerRemoved",
reduce: (&notificationPolicyProjection{}).reduceOwnerRemoved,
args: args{
event: getEvent(testEvent(
repository.EventType(org.OrgRemovedEventType),
org.AggregateType,
nil,
), org.OrgRemovedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.notification_policies SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (resource_owner = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
true,
"instance-id",
"agg-id",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := baseEvent(t)
got, err := tt.reduce(event)
if ok := errors.IsErrorInvalidArgument(err); !ok {
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
}
event = tt.args.event(t)
got, err = tt.reduce(event)
assertReduce(t, got, err, NotificationPolicyProjectionTable, tt.want)
})
}
}

View File

@@ -60,6 +60,7 @@ var (
DebugNotificationProviderProjection *debugNotificationProviderProjection
KeyProjection *keyProjection
SecurityPolicyProjection *securityPolicyProjection
NotificationPolicyProjection *notificationPolicyProjection
NotificationsProjection interface{}
)
@@ -133,6 +134,7 @@ func Create(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, c
DebugNotificationProviderProjection = newDebugNotificationProviderProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["debug_notification_provider"]))
KeyProjection = newKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["keys"]), keyEncryptionAlgorithm, certEncryptionAlgorithm)
SecurityPolicyProjection = newSecurityPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["security_policies"]))
NotificationPolicyProjection = newNotificationPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["notification_policies"]))
newProjectionsList()
return nil
}
@@ -224,5 +226,6 @@ func newProjectionsList() {
DebugNotificationProviderProjection,
KeyProjection,
SecurityPolicyProjection,
NotificationPolicyProjection,
}
}

View File

@@ -89,5 +89,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, InstanceDomainRemovedEventType, DomainRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, InstanceAddedEventType, InstanceAddedEventMapper).
RegisterFilterEventMapper(AggregateType, InstanceChangedEventType, InstanceChangedEventMapper).
RegisterFilterEventMapper(AggregateType, InstanceRemovedEventType, InstanceRemovedEventMapper)
RegisterFilterEventMapper(AggregateType, InstanceRemovedEventType, InstanceRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper).
RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper)
}

View File

@@ -0,0 +1,73 @@
package instance
import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/policy"
)
const (
NotificationPolicyAddedEventType = instanceEventTypePrefix + policy.NotificationPolicyAddedEventType
NotificationPolicyChangedEventType = instanceEventTypePrefix + policy.NotificationPolicyChangedEventType
)
type NotificationPolicyAddedEvent struct {
policy.NotificationPolicyAddedEvent
}
func NewNotificationPolicyAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
passwordChange bool,
) *NotificationPolicyAddedEvent {
return &NotificationPolicyAddedEvent{
NotificationPolicyAddedEvent: *policy.NewNotificationPolicyAddedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
NotificationPolicyAddedEventType),
passwordChange),
}
}
func NotificationPolicyAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := policy.NotificationPolicyAddedEventMapper(event)
if err != nil {
return nil, err
}
return &NotificationPolicyAddedEvent{NotificationPolicyAddedEvent: *e.(*policy.NotificationPolicyAddedEvent)}, nil
}
type NotificationPolicyChangedEvent struct {
policy.NotificationPolicyChangedEvent
}
func NewNotificationPolicyChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
changes []policy.NotificationPolicyChanges,
) (*NotificationPolicyChangedEvent, error) {
changedEvent, err := policy.NewNotificationPolicyChangedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
NotificationPolicyChangedEventType),
changes,
)
if err != nil {
return nil, err
}
return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *changedEvent}, nil
}
func NotificationPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := policy.NotificationPolicyChangedEventMapper(event)
if err != nil {
return nil, err
}
return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *e.(*policy.NotificationPolicyChangedEvent)}, nil
}

View File

@@ -83,5 +83,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, FlowClearedEventType, FlowClearedEventMapper).
RegisterFilterEventMapper(AggregateType, MetadataSetType, MetadataSetEventMapper).
RegisterFilterEventMapper(AggregateType, MetadataRemovedType, MetadataRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, MetadataRemovedAllType, MetadataRemovedAllEventMapper)
RegisterFilterEventMapper(AggregateType, MetadataRemovedAllType, MetadataRemovedAllEventMapper).
RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper).
RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper).
RegisterFilterEventMapper(AggregateType, NotificationPolicyRemovedEventType, NotificationPolicyRemovedEventMapper)
}

View File

@@ -0,0 +1,102 @@
package org
import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/policy"
)
var (
NotificationPolicyAddedEventType = orgEventTypePrefix + policy.NotificationPolicyAddedEventType
NotificationPolicyChangedEventType = orgEventTypePrefix + policy.NotificationPolicyChangedEventType
NotificationPolicyRemovedEventType = orgEventTypePrefix + policy.NotificationPolicyRemovedEventType
)
type NotificationPolicyAddedEvent struct {
policy.NotificationPolicyAddedEvent
}
func NewNotificationPolicyAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
passwordChange bool,
) *NotificationPolicyAddedEvent {
return &NotificationPolicyAddedEvent{
NotificationPolicyAddedEvent: *policy.NewNotificationPolicyAddedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
NotificationPolicyAddedEventType),
passwordChange,
),
}
}
func NotificationPolicyAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := policy.NotificationPolicyAddedEventMapper(event)
if err != nil {
return nil, err
}
return &NotificationPolicyAddedEvent{NotificationPolicyAddedEvent: *e.(*policy.NotificationPolicyAddedEvent)}, nil
}
type NotificationPolicyChangedEvent struct {
policy.NotificationPolicyChangedEvent
}
func NewNotificationPolicyChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
changes []policy.NotificationPolicyChanges,
) (*NotificationPolicyChangedEvent, error) {
changedEvent, err := policy.NewNotificationPolicyChangedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
NotificationPolicyChangedEventType),
changes,
)
if err != nil {
return nil, err
}
return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *changedEvent}, nil
}
func NotificationPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := policy.NotificationPolicyChangedEventMapper(event)
if err != nil {
return nil, err
}
return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *e.(*policy.NotificationPolicyChangedEvent)}, nil
}
type NotificationPolicyRemovedEvent struct {
policy.NotificationPolicyRemovedEvent
}
func NewNotificationPolicyRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *NotificationPolicyRemovedEvent {
return &NotificationPolicyRemovedEvent{
NotificationPolicyRemovedEvent: *policy.NewNotificationPolicyRemovedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
NotificationPolicyRemovedEventType),
),
}
}
func NotificationPolicyRemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := policy.NotificationPolicyRemovedEventMapper(event)
if err != nil {
return nil, err
}
return &NotificationPolicyRemovedEvent{NotificationPolicyRemovedEvent: *e.(*policy.NotificationPolicyRemovedEvent)}, nil
}

View File

@@ -0,0 +1,127 @@
package policy
import (
"encoding/json"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
)
const (
NotificationPolicyAddedEventType = "policy.notification.added"
NotificationPolicyChangedEventType = "policy.notification.changed"
NotificationPolicyRemovedEventType = "policy.notification.removed"
)
type NotificationPolicyAddedEvent struct {
eventstore.BaseEvent `json:"-"`
PasswordChange bool `json:"passwordChange,omitempty"`
}
func (e *NotificationPolicyAddedEvent) Data() interface{} {
return e
}
func (e *NotificationPolicyAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewNotificationPolicyAddedEvent(
base *eventstore.BaseEvent,
passwordChange bool,
) *NotificationPolicyAddedEvent {
return &NotificationPolicyAddedEvent{
BaseEvent: *base,
PasswordChange: passwordChange,
}
}
func NotificationPolicyAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &NotificationPolicyAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "POLIC-0sp2nios", "unable to unmarshal policy")
}
return e, nil
}
type NotificationPolicyChangedEvent struct {
eventstore.BaseEvent `json:"-"`
PasswordChange *bool `json:"passwordChange,omitempty"`
}
func (e *NotificationPolicyChangedEvent) Data() interface{} {
return e
}
func (e *NotificationPolicyChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewNotificationPolicyChangedEvent(
base *eventstore.BaseEvent,
changes []NotificationPolicyChanges,
) (*NotificationPolicyChangedEvent, error) {
if len(changes) == 0 {
return nil, errors.ThrowPreconditionFailed(nil, "POLICY-09sp2m", "Errors.NoChangesFound")
}
changeEvent := &NotificationPolicyChangedEvent{
BaseEvent: *base,
}
for _, change := range changes {
change(changeEvent)
}
return changeEvent, nil
}
type NotificationPolicyChanges func(*NotificationPolicyChangedEvent)
func ChangePasswordChange(passwordChange bool) func(*NotificationPolicyChangedEvent) {
return func(e *NotificationPolicyChangedEvent) {
e.PasswordChange = &passwordChange
}
}
func NotificationPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &NotificationPolicyChangedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "POLIC-09s2oss", "unable to unmarshal policy")
}
return e, nil
}
type NotificationPolicyRemovedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *NotificationPolicyRemovedEvent) Data() interface{} {
return nil
}
func (e *NotificationPolicyRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewNotificationPolicyRemovedEvent(base *eventstore.BaseEvent) *NotificationPolicyRemovedEvent {
return &NotificationPolicyRemovedEvent{
BaseEvent: *base,
}
}
func NotificationPolicyRemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
return &NotificationPolicyRemovedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}

View File

@@ -59,6 +59,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, HumanPasswordChangedType, HumanPasswordChangedEventMapper).
RegisterFilterEventMapper(AggregateType, HumanPasswordCodeAddedType, HumanPasswordCodeAddedEventMapper).
RegisterFilterEventMapper(AggregateType, HumanPasswordCodeSentType, HumanPasswordCodeSentEventMapper).
RegisterFilterEventMapper(AggregateType, HumanPasswordChangeSentType, HumanPasswordChangeSentEventMapper).
RegisterFilterEventMapper(AggregateType, HumanPasswordCheckSucceededType, HumanPasswordCheckSucceededEventMapper).
RegisterFilterEventMapper(AggregateType, HumanPasswordCheckFailedType, HumanPasswordCheckFailedEventMapper).
RegisterFilterEventMapper(AggregateType, UserIDPLinkAddedType, UserIDPLinkAddedEventMapper).

View File

@@ -16,6 +16,7 @@ import (
const (
passwordEventPrefix = humanEventPrefix + "password."
HumanPasswordChangedType = passwordEventPrefix + "changed"
HumanPasswordChangeSentType = passwordEventPrefix + "change.sent"
HumanPasswordCodeAddedType = passwordEventPrefix + "code.added"
HumanPasswordCodeSentType = passwordEventPrefix + "code.sent"
HumanPasswordCheckSucceededType = passwordEventPrefix + "check.succeeded"
@@ -144,6 +145,34 @@ func HumanPasswordCodeSentEventMapper(event *repository.Event) (eventstore.Event
}, nil
}
type HumanPasswordChangeSentEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *HumanPasswordChangeSentEvent) Data() interface{} {
return nil
}
func (e *HumanPasswordChangeSentEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewHumanPasswordChangeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanPasswordChangeSentEvent {
return &HumanPasswordChangeSentEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanPasswordChangeSentType,
),
}
}
func HumanPasswordChangeSentEventMapper(event *repository.Event) (eventstore.Event, error) {
return &HumanPasswordChangeSentEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type HumanPasswordCheckSucceededEvent struct {
eventstore.BaseEvent `json:"-"`
*AuthRequestInfo

View File

@@ -225,6 +225,10 @@ Errors:
Empty: Org IAM Policy ist leer
NotExisting: Org IAM Policy existiert nicht
AlreadyExists: Org IAM Policy existiert bereits
NotificationPolicy:
NotFound: Notification Policy konnte nicht gefunden werden
NotChanged: Notification Policy wurde nicht verändert
AlreadyExists: Notification Policy existiert bereits
Project:
ProjectIDMissing: Project ID fehlt
AlreadyExists: Project existiert bereits auf der Organisation
@@ -351,6 +355,10 @@ Errors:
AlreadyExists: Default Org IAM Policy existiert bereits
Empty: Default Org IAM Policy leer
NotChanged: Default Org IAM Policy wurde nicht verändert
NotificationPolicy:
NotFound: Default Notification Policy konnte nicht gefunden werden
NotChanged: Default Notification Policy wurde nicht verändert
AlreadyExists: Default Notification Policy existiert bereits
Policy:
AlreadyExists: Policy existiert bereits
Label:
@@ -750,6 +758,10 @@ EventTypes:
added: Passwortaussperrrichtlinie hinzugefügt
changed: Passwortaussperrrichtlinie geändert
removed: Passwortaussperrrichtlinie gelöscht
notification:
added: Notifikation Richtlinie hinzugefügt
changed: Notifikation Richtlinie geändert
removed: Notifikation Richtlinie entfernt
flow:
trigger_actions:
set: Aktionen festgelegt
@@ -980,7 +992,7 @@ EventTypes:
removed: Instanzmitglied gelöscht
cascade:
removed: Instanzmitglied kaskadierend gelöscht
notification:
notification:
provider:
debug:
fileadded: Datei zu Debug Notification Provider hinzugefügt
@@ -1047,7 +1059,7 @@ EventTypes:
changed: Datenschutzrichtlinie geändert
security:
set: Sicherheitsrichtlinie gesetzt
removed: Instanz gelöscht
secret:
generator:

View File

@@ -225,6 +225,10 @@ Errors:
Empty: Org IAM Policy is empty
NotExisting: Org IAM Policy doesn't exist
AlreadyExists: Org IAM Policy already exists
NotificationPolicy:
NotFound: Notification Policy not found
NotChanged: Notification Policy not changed
AlreadyExists: Notification Policy already exists
Project:
ProjectIDMissing: Project Id missing
AlreadyExists: Project already exists on organization
@@ -351,6 +355,10 @@ Errors:
NotExisting: Org IAM Policy not existing
AlreadyExists: Org IAM Policy already exists
NotChanged: Org IAM Policy has not been changed
NotificationPolicy:
NotFound: Default Notification Policy not found
NotChanged: Default Notification Policy not changed
AlreadyExists: Default Notification Policy already exists
Policy:
AlreadyExists: Policy already exists
Label:
@@ -750,6 +758,10 @@ EventTypes:
added: Lockout policy added
changed: Lockout policy changed
removed: Lockout policy removed
notification:
added: Notification policy added
changed: Notification policy changed
removed: Notification policy removed
flow:
trigger_actions:
set: Action set
@@ -1047,7 +1059,7 @@ EventTypes:
changed: Privacy policy changed
security:
set: Security policy set
removed: Instance removed
secret:
generator:

View File

@@ -225,6 +225,10 @@ Errors:
Empty: La politique IAM d'Org est vide
NotExisting: La politique Org IAM n'existe pas
AlreadyExists: La politique IAM d'Org existe déjà
NotificationPolicy:
NotFound: La politique notification n'a pas été trouvée
NotChanged: La politique notification n'a pas été modifiée
AlreadyExists: La politique notification existe déjà
Project:
ProjectIDMissing: Id de projet manquant
AlreadyExists: Le projet existe déjà dans l'organisation
@@ -351,6 +355,10 @@ Errors:
NotExisting: La politique IAM d'Org n'existe pas
AlreadyExists: La politique IAM d'Org existe déjà
NotChanged: La politique IAM d'Org n'a pas été modifiée
NotificationPolicy:
NotFound: La politique de notification par défaut n'a pas été trouvée
NotChanged: La politique de notification par défaut n'a pas été modifiée
AlreadyExists: La ppolitique de notification par défaut existe déjà
Policy:
AlreadyExists: La politique existe déjà
Label:
@@ -725,6 +733,10 @@ EventTypes:
added: Politique de confidentialité et CGU ajoutés
changed: Politique de confidentialité et CGU modifiées
removed: Politique de confidentialité et conditions d'utilisation supprimées
notification:
added: Politique de notification ajoutée
changed: Politique de notification modifiée
removed: Politique de notification supprimée
flow:
trigger_actions:
set: Action set

View File

@@ -225,6 +225,10 @@ Errors:
Empty: Mancano le impostazioni Org IAM
NotExisting: Impostazioni Org IAM non esistenti
AlreadyExists: Impostazioni Org IAM già esistenti
NotificationPolicy:
NotFound: Impostazioni di notifica non trovate
NotChanged: Impostazioni di notifica non è stato cambiato
AlreadyExists: Impostazioni di notifica già esistente
Project:
ProjectIDMissing: ID del progetto mancante
AlreadyExists: Il progetto è già stato creato nell'organizzazione
@@ -351,6 +355,10 @@ Errors:
NotExisting: Impostazioni Org IAM non esistenti
AlreadyExists: Impostazioni Org IAM già esistenti
NotChanged: Impostazioni Org IAM non sono state cambiate
NotificationPolicy:
NotFound: Impostazioni di notifica predefinite non trovate
NotChanged: Impostazioni di notifica predefinite non è stato cambiato
AlreadyExists: Impostazioni di notifica predefinite già esistente
Policy:
AlreadyExists: Impostazioni già esistenti
Label:
@@ -725,6 +733,10 @@ EventTypes:
added: Informativa sulla privacy e termini e condizioni aggiunti
changed: Informativa sulla privacy e termini e condizioni cambiati
removed: Informativa sulla privacy e termini e condizioni rimossi
notification:
added: Impostazione di notifica creata
changed: Impostazione di notifica cambiata
removed: Impostazione di notifica rimossa
flow:
trigger_actions:
set: azioni salvate

View File

@@ -225,6 +225,10 @@ Errors:
Empty: 组织 IAM 策略为空
NotExisting: 组织 IAM 策略不存在
AlreadyExists: 组织 IAM 策略已存在
NotificationPolicy:
NotFound: 未找到通知政策
NotChanged: 通知政策没有改变
AlreadyExists: 已经存在的通知政策
Project:
ProjectIDMissing: P缺少项目 ID
AlreadyExists: 项目以存在于组织中
@@ -351,6 +355,10 @@ Errors:
NotExisting: 组织 IAM 策略不存在
AlreadyExists: 组织 IAM 策略已存在
NotChanged: 组织 IAM 策略未更改
NotificationPolicy:
NotFound: 没有找到默认的通知政策
NotChanged: 默认的通知政策没有改变
AlreadyExists: 默认的通知政策已经存在
Policy:
AlreadyExists: 策略已存在
Label:
@@ -715,6 +723,10 @@ EventTypes:
added: 添加隐私政策和服务条款
changed: 更改隐私政策和服务条款
removed: 删除隐私政策和服务条款
notification:
added: 增加了通知政策
changed: 通知政策改变
removed: 删除了通知政策
flow:
trigger_actions:
set: 设置动作