feat: add http as sms provider (#8540)

# Which Problems Are Solved

Send SMS messages as a HTTP call to a relay, for own logic on handling
different SMS providers.

# How the Problems Are Solved

Add HTTP as SMS provider type and handling of webhook messages in the
notification handlers.

# Additional Changes

Clean up old Twilio events, which were supposed to handle the general
SMS providers with deactivate, activate and remove.

# Additional Context

Partially closes #8270

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz
2024-09-06 15:11:36 +02:00
committed by GitHub
parent d2e0ac07f1
commit 5bdf1a4547
26 changed files with 2536 additions and 593 deletions

View File

@@ -5,203 +5,328 @@ import (
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) AddSMSConfigTwilio(ctx context.Context, instanceID string, config *twilio.Config) (string, *domain.ObjectDetails, error) {
id, err := c.idGenerator.Next()
if err != nil {
return "", nil, err
type AddTwilioConfig struct {
Details *domain.ObjectDetails
ResourceOwner string
ID string
Description string
SID string
Token string
SenderNumber string
}
func (c *Commands) AddSMSConfigTwilio(ctx context.Context, config *AddTwilioConfig) (err error) {
if config.ResourceOwner == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-ZLrZhKSKq0", "Errors.ResourceOwnerMissing")
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id)
if config.ID == "" {
config.ID, err = c.idGenerator.Next()
if err != nil {
return err
}
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID)
if err != nil {
return "", nil, err
return err
}
var token *crypto.CryptoValue
if config.Token != "" {
token, err = crypto.Encrypt([]byte(config.Token), c.smsEncryption)
if err != nil {
return "", nil, err
return err
}
}
iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigTwilioAddedEvent(
ctx,
iamAgg,
id,
config.SID,
config.SenderNumber,
token))
err = c.pushAppendAndReduce(ctx,
smsConfigWriteModel,
instance.NewSMSConfigTwilioAddedEvent(
ctx,
InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel),
config.ID,
config.Description,
config.SID,
config.SenderNumber,
token,
),
)
if err != nil {
return "", nil, err
return err
}
err = AppendAndReduce(smsConfigWriteModel, pushedEvents...)
if err != nil {
return "", nil, err
}
return id, writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil
config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel)
return nil
}
func (c *Commands) ChangeSMSConfigTwilio(ctx context.Context, instanceID, id string, config *twilio.Config) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "SMS-e9jwf", "Errors.IDMissing")
type ChangeTwilioConfig struct {
Details *domain.ObjectDetails
ResourceOwner string
ID string
Description *string
SID *string
Token *string
SenderNumber *string
}
func (c *Commands) ChangeSMSConfigTwilio(ctx context.Context, config *ChangeTwilioConfig) (err error) {
if config.ResourceOwner == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-RHXryJwmFG", "Errors.ResourceOwnerMissing")
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id)
if config.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-gMr93iNhTR", "Errors.IDMissing")
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID)
if err != nil {
return nil, err
return err
}
if !smsConfigWriteModel.State.Exists() || smsConfigWriteModel.Twilio == nil {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-2m9fw", "Errors.SMSConfig.NotFound")
return zerrors.ThrowNotFound(nil, "COMMAND-MUY0IFAf8O", "Errors.SMSConfig.NotFound")
}
iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel)
changedEvent, hasChanged, err := smsConfigWriteModel.NewChangedEvent(
changedEvent, hasChanged, err := smsConfigWriteModel.NewTwilioChangedEvent(
ctx,
iamAgg,
id,
InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel),
config.ID,
config.Description,
config.SID,
config.SenderNumber)
if err != nil {
return nil, err
return err
}
if !hasChanged {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-jf9wk", "Errors.NoChangesFound")
config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel)
return nil
}
pushedEvents, err := c.eventstore.Push(ctx, changedEvent)
err = c.pushAppendAndReduce(ctx,
smsConfigWriteModel,
changedEvent,
)
if err != nil {
return nil, err
return err
}
err = AppendAndReduce(smsConfigWriteModel, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil
config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel)
return nil
}
func (c *Commands) ChangeSMSConfigTwilioToken(ctx context.Context, instanceID, id, token string) (*domain.ObjectDetails, error) {
smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id)
func (c *Commands) ChangeSMSConfigTwilioToken(ctx context.Context, resourceOwner, id, token string) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-sLLA1HnMzj", "Errors.ResourceOwnerMissing")
}
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "SMS-PeNaqbC0r0", "Errors.IDMissing")
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id)
if err != nil {
return nil, err
}
if !smsConfigWriteModel.State.Exists() || smsConfigWriteModel.Twilio == nil {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-fj9wf", "Errors.SMSConfig.NotFound")
return nil, zerrors.ThrowNotFound(nil, "COMMAND-ij3NhEHATp", "Errors.SMSConfig.NotFound")
}
iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel)
newtoken, err := crypto.Encrypt([]byte(token), c.smsEncryption)
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigTokenChangedEvent(
ctx,
iamAgg,
id,
newtoken))
if err != nil {
return nil, err
}
err = AppendAndReduce(smsConfigWriteModel, pushedEvents...)
err = c.pushAppendAndReduce(ctx,
smsConfigWriteModel,
instance.NewSMSConfigTokenChangedEvent(
ctx,
InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel),
id,
newtoken,
),
)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil
}
func (c *Commands) ActivateSMSConfig(ctx context.Context, instanceID, id string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "SMS-dn93n", "Errors.IDMissing")
type AddSMSHTTP struct {
Details *domain.ObjectDetails
ResourceOwner string
ID string
Description string
Endpoint string
}
func (c *Commands) AddSMSConfigHTTP(ctx context.Context, config *AddSMSHTTP) (err error) {
if config.ResourceOwner == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-huy99qWjX4", "Errors.ResourceOwnerMissing")
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id)
if config.ID == "" {
config.ID, err = c.idGenerator.Next()
if err != nil {
return err
}
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID)
if err != nil {
return err
}
err = c.pushAppendAndReduce(ctx,
smsConfigWriteModel,
instance.NewSMSConfigHTTPAddedEvent(
ctx,
InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel),
config.ID,
config.Description,
config.Endpoint,
),
)
if err != nil {
return err
}
config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel)
return nil
}
type ChangeSMSHTTP struct {
Details *domain.ObjectDetails
ResourceOwner string
ID string
Description *string
Endpoint *string
}
func (c *Commands) ChangeSMSConfigHTTP(ctx context.Context, config *ChangeSMSHTTP) (err error) {
if config.ResourceOwner == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-M622CFQnwK", "Errors.ResourceOwnerMissing")
}
if config.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-phyb2e4Kll", "Errors.IDMissing")
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID)
if err != nil {
return err
}
if !smsConfigWriteModel.State.Exists() || smsConfigWriteModel.HTTP == nil {
return zerrors.ThrowNotFound(nil, "COMMAND-6NW4I5Kqzj", "Errors.SMSConfig.NotFound")
}
changedEvent, hasChanged, err := smsConfigWriteModel.NewHTTPChangedEvent(
ctx,
InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel),
config.ID,
config.Description,
config.Endpoint)
if err != nil {
return err
}
if !hasChanged {
config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel)
return nil
}
err = c.pushAppendAndReduce(ctx, smsConfigWriteModel, changedEvent)
if err != nil {
return err
}
config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel)
return nil
}
func (c *Commands) ActivateSMSConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-EFgoOg997V", "Errors.ResourceOwnerMissing")
}
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-jJ6TVqzvjp", "Errors.IDMissing")
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id)
if err != nil {
return nil, err
}
if !smsConfigWriteModel.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-sn9we", "Errors.SMSConfig.NotFound")
return nil, zerrors.ThrowNotFound(nil, "COMMAND-9ULtp9PH5E", "Errors.SMSConfig.NotFound")
}
if smsConfigWriteModel.State == domain.SMSConfigStateActive {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-sn9we", "Errors.SMSConfig.AlreadyActive")
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-B25GFeIvRi", "Errors.SMSConfig.AlreadyActive")
}
iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigTwilioActivatedEvent(
ctx,
iamAgg,
id))
if err != nil {
return nil, err
}
err = AppendAndReduce(smsConfigWriteModel, pushedEvents...)
err = c.pushAppendAndReduce(ctx, smsConfigWriteModel,
instance.NewSMSConfigActivatedEvent(
ctx,
InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel),
id,
),
)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil
}
func (c *Commands) DeactivateSMSConfig(ctx context.Context, instanceID, id string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "SMS-frkwf", "Errors.IDMissing")
func (c *Commands) DeactivateSMSConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-V9NWOZj8Gi", "Errors.ResourceOwnerMissing")
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id)
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xs1ah1v1CL", "Errors.IDMissing")
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id)
if err != nil {
return nil, err
}
if !smsConfigWriteModel.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-s39Kg", "Errors.SMSConfig.NotFound")
return nil, zerrors.ThrowNotFound(nil, "COMMAND-La91dGNhbM", "Errors.SMSConfig.NotFound")
}
if smsConfigWriteModel.State == domain.SMSConfigStateInactive {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-dm9e3", "Errors.SMSConfig.AlreadyDeactivated")
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-OSZAEkYvk7", "Errors.SMSConfig.AlreadyDeactivated")
}
iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigDeactivatedEvent(
ctx,
iamAgg,
id))
if err != nil {
return nil, err
}
err = AppendAndReduce(smsConfigWriteModel, pushedEvents...)
err = c.pushAppendAndReduce(ctx,
smsConfigWriteModel,
instance.NewSMSConfigDeactivatedEvent(
ctx,
InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel),
id,
),
)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil
}
func (c *Commands) RemoveSMSConfig(ctx context.Context, instanceID, id string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "SMS-3j9fs", "Errors.IDMissing")
func (c *Commands) RemoveSMSConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-cw0NSJsn1v", "Errors.ResourceOwnerMissing")
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id)
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Qrz7lvdC4c", "Errors.IDMissing")
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id)
if err != nil {
return nil, err
}
if !smsConfigWriteModel.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-sn9we", "Errors.SMSConfig.NotFound")
return nil, zerrors.ThrowNotFound(nil, "COMMAND-povEVHPCkV", "Errors.SMSConfig.NotFound")
}
iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigRemovedEvent(
ctx,
iamAgg,
id))
if err != nil {
return nil, err
}
err = AppendAndReduce(smsConfigWriteModel, pushedEvents...)
err = c.pushAppendAndReduce(ctx,
smsConfigWriteModel,
instance.NewSMSConfigRemovedEvent(
ctx,
InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel),
id,
),
)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil
}
func (c *Commands) getSMSConfig(ctx context.Context, instanceID, id string) (_ *IAMSMSConfigWriteModel, err error) {
writeModel := NewIAMSMSConfigWriteModel(instanceID, id)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}

View File

@@ -12,9 +12,11 @@ import (
type IAMSMSConfigWriteModel struct {
eventstore.WriteModel
ID string
Twilio *TwilioConfig
State domain.SMSConfigState
ID string
Description string
Twilio *TwilioConfig
HTTP *HTTPConfig
State domain.SMSConfigState
}
type TwilioConfig struct {
@@ -23,6 +25,10 @@ type TwilioConfig struct {
SenderNumber string
}
type HTTPConfig struct {
Endpoint string
}
func NewIAMSMSConfigWriteModel(instanceID, id string) *IAMSMSConfigWriteModel {
return &IAMSMSConfigWriteModel{
WriteModel: eventstore.WriteModel{
@@ -46,11 +52,15 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error {
Token: e.Token,
SenderNumber: e.SenderNumber,
}
wm.Description = e.Description
wm.State = domain.SMSConfigStateInactive
case *instance.SMSConfigTwilioChangedEvent:
if wm.ID != e.ID {
continue
}
if e.Description != nil {
wm.Description = *e.Description
}
if e.SID != nil {
wm.Twilio.SID = *e.SID
}
@@ -62,6 +72,42 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error {
continue
}
wm.Twilio.Token = e.Token
case *instance.SMSConfigHTTPAddedEvent:
if wm.ID != e.ID {
continue
}
wm.HTTP = &HTTPConfig{
Endpoint: e.Endpoint,
}
wm.Description = e.Description
wm.State = domain.SMSConfigStateInactive
case *instance.SMSConfigHTTPChangedEvent:
if wm.ID != e.ID {
continue
}
if e.Description != nil {
wm.Description = *e.Description
}
if e.Endpoint != nil {
wm.HTTP.Endpoint = *e.Endpoint
}
case *instance.SMSConfigTwilioActivatedEvent:
if wm.ID != e.ID {
continue
}
wm.State = domain.SMSConfigStateActive
case *instance.SMSConfigTwilioDeactivatedEvent:
if wm.ID != e.ID {
continue
}
wm.State = domain.SMSConfigStateInactive
case *instance.SMSConfigTwilioRemovedEvent:
if wm.ID != e.ID {
continue
}
wm.Twilio = nil
wm.HTTP = nil
wm.State = domain.SMSConfigStateRemoved
case *instance.SMSConfigActivatedEvent:
if wm.ID != e.ID {
continue
@@ -77,6 +123,7 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error {
continue
}
wm.Twilio = nil
wm.HTTP = nil
wm.State = domain.SMSConfigStateRemoved
}
}
@@ -92,21 +139,33 @@ func (wm *IAMSMSConfigWriteModel) Query() *eventstore.SearchQueryBuilder {
instance.SMSConfigTwilioAddedEventType,
instance.SMSConfigTwilioChangedEventType,
instance.SMSConfigTwilioTokenChangedEventType,
instance.SMSConfigHTTPAddedEventType,
instance.SMSConfigHTTPChangedEventType,
instance.SMSConfigTwilioActivatedEventType,
instance.SMSConfigTwilioDeactivatedEventType,
instance.SMSConfigTwilioRemovedEventType,
instance.SMSConfigActivatedEventType,
instance.SMSConfigDeactivatedEventType,
instance.SMSConfigRemovedEventType).
Builder()
}
func (wm *IAMSMSConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id, sid, senderNumber string) (*instance.SMSConfigTwilioChangedEvent, bool, error) {
func (wm *IAMSMSConfigWriteModel) NewTwilioChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string, description, sid, senderNumber *string) (*instance.SMSConfigTwilioChangedEvent, bool, error) {
changes := make([]instance.SMSConfigTwilioChanges, 0)
var err error
if wm.Twilio.SID != sid {
changes = append(changes, instance.ChangeSMSConfigTwilioSID(sid))
if wm.Twilio == nil {
return nil, false, nil
}
if wm.Twilio.SenderNumber != senderNumber {
changes = append(changes, instance.ChangeSMSConfigTwilioSenderNumber(senderNumber))
if description != nil && wm.Description != *description {
changes = append(changes, instance.ChangeSMSConfigTwilioDescription(*description))
}
if sid != nil && wm.Twilio.SID != *sid {
changes = append(changes, instance.ChangeSMSConfigTwilioSID(*sid))
}
if senderNumber != nil && wm.Twilio.SenderNumber != *senderNumber {
changes = append(changes, instance.ChangeSMSConfigTwilioSenderNumber(*senderNumber))
}
if len(changes) == 0 {
@@ -118,3 +177,28 @@ func (wm *IAMSMSConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate
}
return changeEvent, true, nil
}
func (wm *IAMSMSConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string, description, endpoint *string) (*instance.SMSConfigHTTPChangedEvent, bool, error) {
changes := make([]instance.SMSConfigHTTPChanges, 0)
var err error
if wm.HTTP == nil {
return nil, false, nil
}
if description != nil && wm.Description != *description {
changes = append(changes, instance.ChangeSMSConfigHTTPDescription(*description))
}
if endpoint != nil && wm.HTTP.Endpoint != *endpoint {
changes = append(changes, instance.ChangeSMSConfigHTTPEndpoint(*endpoint))
}
if len(changes) == 0 {
return nil, false, nil
}
changeEvent, err := instance.NewSMSConfigHTTPChangedEvent(ctx, aggregate, id, changes)
if err != nil {
return nil, false, err
}
return changeEvent, true, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -953,6 +953,49 @@ func TestCommandSide_ActivateSMTPConfig(t *testing.T) {
},
},
},
{
name: "activate smtp config, already active, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewSMTPConfigAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"ID",
"test",
true,
"from",
"name",
"",
"host:587",
"user",
&crypto.CryptoValue{},
),
),
),
expectPush(
instance.NewSMTPConfigActivatedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"ID",
),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
id: "ID",
instanceID: "INSTANCE",
activatedId: "",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {