From aa2a1848da05b992c9152ebc5340c605cc91fee1 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Thu, 6 Jan 2022 09:00:24 +0100 Subject: [PATCH] feat: add stdout and filesystem notification channels (#2925) * feat: add filesystem and stdout notification channels * configure through env vars * compile * feat: add compact option for debug notification channels * fix channel mock generation * avoid sensitive information in error message Co-authored-by: Livio Amstutz * add review improvements Co-authored-by: Livio Amstutz --- .gitignore | 1 + build/local/docker-compose-local.yml | 1 + build/local/local.env | 14 ++--- cmd/zitadel/system-defaults.yaml | 17 ++++-- go.mod | 1 + go.sum | 2 + .../config/systemdefaults/system_defaults.go | 20 +++--- internal/notification/channels/channel.go | 17 ++++++ .../provider.go => channels/chat/channel.go} | 53 +++++++--------- internal/notification/channels/chat/config.go | 9 +++ internal/notification/channels/fs/channel.go | 51 ++++++++++++++++ internal/notification/channels/fs/config.go | 7 +++ internal/notification/channels/gen_mock.go | 4 ++ internal/notification/channels/log/channel.go | 29 +++++++++ internal/notification/channels/log/config.go | 6 ++ .../channels/mock/channel.mock.go | 48 +++++++++++++++ .../mock/message.mock.go | 2 +- .../provider.go => channels/smtp/channel.go} | 30 +++++---- .../email => channels/smtp}/config.go | 2 +- .../notification/channels/twilio/channel.go | 28 +++++++++ .../{providers => channels}/twilio/config.go | 0 .../email/message.go => messages/email.go} | 10 ++- internal/notification/messages/sms.go | 15 +++++ .../notification/providers/chat/config.go | 6 -- .../notification/providers/chat/message.go | 9 --- internal/notification/providers/gen_mock.go | 4 -- internal/notification/providers/message.go | 5 -- .../providers/mock/provider.mock.go | 61 ------------------- internal/notification/providers/provider.go | 6 -- .../notification/providers/twilio/message.go | 11 ---- .../notification/providers/twilio/provider.go | 39 ------------ internal/notification/senders/chain.go | 24 ++++++++ internal/notification/senders/debug.go | 46 ++++++++++++++ internal/notification/senders/email.go | 25 ++++++++ internal/notification/senders/sms.go | 21 +++++++ internal/notification/types/user_email.go | 27 +++----- internal/notification/types/user_phone.go | 22 ++----- 37 files changed, 426 insertions(+), 247 deletions(-) create mode 100644 internal/notification/channels/channel.go rename internal/notification/{providers/chat/provider.go => channels/chat/channel.go} (56%) create mode 100644 internal/notification/channels/chat/config.go create mode 100644 internal/notification/channels/fs/channel.go create mode 100644 internal/notification/channels/fs/config.go create mode 100644 internal/notification/channels/gen_mock.go create mode 100644 internal/notification/channels/log/channel.go create mode 100644 internal/notification/channels/log/config.go create mode 100644 internal/notification/channels/mock/channel.mock.go rename internal/notification/{providers => channels}/mock/message.mock.go (93%) rename internal/notification/{providers/email/provider.go => channels/smtp/channel.go} (81%) rename internal/notification/{providers/email => channels/smtp}/config.go (94%) create mode 100644 internal/notification/channels/twilio/channel.go rename internal/notification/{providers => channels}/twilio/config.go (100%) rename internal/notification/{providers/email/message.go => messages/email.go} (84%) create mode 100644 internal/notification/messages/sms.go delete mode 100644 internal/notification/providers/chat/config.go delete mode 100644 internal/notification/providers/chat/message.go delete mode 100644 internal/notification/providers/gen_mock.go delete mode 100644 internal/notification/providers/message.go delete mode 100644 internal/notification/providers/mock/provider.mock.go delete mode 100644 internal/notification/providers/provider.go delete mode 100644 internal/notification/providers/twilio/message.go delete mode 100644 internal/notification/providers/twilio/provider.go create mode 100644 internal/notification/senders/chain.go create mode 100644 internal/notification/senders/debug.go create mode 100644 internal/notification/senders/email.go create mode 100644 internal/notification/senders/sms.go diff --git a/.gitignore b/.gitignore index 44d5c04628..1e366e9f27 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ openapi/**/*.json # local build/local/cloud.env migrations/cockroach/migrate_cloud.go +.notifications diff --git a/build/local/docker-compose-local.yml b/build/local/docker-compose-local.yml index a56c0bab50..0846b228d1 100644 --- a/build/local/docker-compose-local.yml +++ b/build/local/docker-compose-local.yml @@ -142,6 +142,7 @@ services: ENV: dev volumes: - ../../.keys:/go/src/github.com/caos/zitadel/.keys + - ../../.notifications:/go/src/github.com/caos/zitadel/.notifications env_file: - ./local.env environment: diff --git a/build/local/local.env b/build/local/local.env index cb5bc4f0fc..58e26ac00c 100644 --- a/build/local/local.env +++ b/build/local/local.env @@ -28,14 +28,12 @@ DEBUG_MODE=true CAOS_OIDC_DEV=true #sets the cookies insecure in login (never use this in production!) ZITADEL_CSRF_DEV=true - -#currently needed -TWILIO_SENDER_NAME=ZITADEL developer -SMTP_HOST=smtp.gmail.com:465 -SMTP_USER=zitadel@caos.ch -EMAIL_SENDER_ADDRESS=noreply@caos.ch -EMAIL_SENDER_NAME=CAOS AG -SMTP_TLS=true +LOG_NOTIFICATIONS_ENABLED=true +LOG_NOTIFICATIONS_COMPACT=true +FS_NOTIFICATIONS_ENABLED=true +FS_NOTIFICATIONS_PATH=./.notifications +FS_NOTIFICATIONS_COMPACT=false +CHAT_ENABLED=false #configuration for api/browser calls ZITADEL_DEFAULT_DOMAIN=localhost diff --git a/cmd/zitadel/system-defaults.yaml b/cmd/zitadel/system-defaults.yaml index 19572bd067..0b3e3a1894 100644 --- a/cmd/zitadel/system-defaults.yaml +++ b/cmd/zitadel/system-defaults.yaml @@ -83,9 +83,6 @@ SystemDefaults: DomainClaimed: '$ZITADEL_ACCOUNTS/login' PasswordlessRegistration: '$ZITADEL_ACCOUNTS/login/passwordless/init' Providers: - Chat: - Url: $CHAT_URL - SplitCount: 4000 Email: SMTP: Host: $SMTP_HOST @@ -98,6 +95,18 @@ SystemDefaults: SID: $TWILIO_SERVICE_SID Token: $TWILIO_TOKEN From: $TWILIO_SENDER_NAME + FileSystem: + Enabled: $FS_NOTIFICATIONS_ENABLED + Path: $FS_NOTIFICATIONS_PATH + Compact: $FS_NOTIFICATIONS_COMPACT + Log: + Enabled: $LOG_NOTIFICATIONS_ENABLED + Compact: $LOG_NOTIFICATIONS_COMPACT + Chat: + Enabled: $CHAT_ENABLED + Url: $CHAT_URL + Compact: $CHAT_COMPACT + SplitCount: 4000 TemplateData: InitCode: Title: 'InitCode.Title' @@ -146,4 +155,4 @@ SystemDefaults: EncryptionConfig: EncryptionKeyID: $ZITADEL_OIDC_KEYS_ID SigningKeyRotationCheck: 10s - SigningKeyGracefulPeriod: 10m \ No newline at end of file + SigningKeyGracefulPeriod: 10m diff --git a/go.mod b/go.mod index d1e2d0d185..a5f6d0ef7e 100644 --- a/go.mod +++ b/go.mod @@ -149,6 +149,7 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/json-iterator/go v1.1.11 // indirect + github.com/k3a/html2text v1.0.8 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7 // indirect github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c // indirect diff --git a/go.sum b/go.sum index 77e7513a4b..08d7be3069 100644 --- a/go.sum +++ b/go.sum @@ -629,6 +629,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/k3a/html2text v1.0.8 h1:rVanLhKilpnJUJs/CNKWzMC4YaQINGxK0rSG8ssmnV0= +github.com/k3a/html2text v1.0.8/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA= github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index c4d8e06079..2828ef679a 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -1,13 +1,15 @@ package systemdefaults import ( + "github.com/caos/zitadel/internal/notification/channels/log" "golang.org/x/text/language" "github.com/caos/zitadel/internal/config/types" "github.com/caos/zitadel/internal/crypto" - "github.com/caos/zitadel/internal/notification/providers/chat" - "github.com/caos/zitadel/internal/notification/providers/email" - "github.com/caos/zitadel/internal/notification/providers/twilio" + "github.com/caos/zitadel/internal/notification/channels/chat" + "github.com/caos/zitadel/internal/notification/channels/fs" + "github.com/caos/zitadel/internal/notification/channels/smtp" + "github.com/caos/zitadel/internal/notification/channels/twilio" "github.com/caos/zitadel/internal/notification/templates" ) @@ -69,7 +71,7 @@ type DomainVerification struct { type Notifications struct { DebugMode bool Endpoints Endpoints - Providers Providers + Providers Channels TemplateData TemplateData } @@ -81,10 +83,12 @@ type Endpoints struct { PasswordlessRegistration string } -type Providers struct { - Chat chat.ChatConfig - Email email.EmailConfig - Twilio twilio.TwilioConfig +type Channels struct { + Chat chat.ChatConfig + Email smtp.EmailConfig + Twilio twilio.TwilioConfig + FileSystem fs.FSConfig + Log log.LogConfig } type TemplateData struct { diff --git a/internal/notification/channels/channel.go b/internal/notification/channels/channel.go new file mode 100644 index 0000000000..41823e5274 --- /dev/null +++ b/internal/notification/channels/channel.go @@ -0,0 +1,17 @@ +package channels + +type Message interface { + GetContent() string +} + +type NotificationChannel interface { + HandleMessage(message Message) error +} + +var _ NotificationChannel = (HandleMessageFunc)(nil) + +type HandleMessageFunc func(message Message) error + +func (h HandleMessageFunc) HandleMessage(message Message) error { + return h(message) +} diff --git a/internal/notification/providers/chat/provider.go b/internal/notification/channels/chat/channel.go similarity index 56% rename from internal/notification/providers/chat/provider.go rename to internal/notification/channels/chat/channel.go index ae8ccdd02e..e1f05f9890 100644 --- a/internal/notification/providers/chat/provider.go +++ b/internal/notification/channels/chat/channel.go @@ -3,57 +3,48 @@ package chat import ( "bytes" "encoding/json" - "github.com/caos/logging" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/notification/providers" "io/ioutil" "net/http" "net/url" "unicode/utf8" + + "github.com/k3a/html2text" + + "github.com/caos/logging" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/notification/channels" ) -type Chat struct { - URL *url.URL - SplitCount int -} +func InitChatChannel(config ChatConfig) (channels.NotificationChannel, error) { -func InitChatProvider(config ChatConfig) (*Chat, error) { url, err := url.Parse(config.Url) if err != nil { return nil, err } - return &Chat{ - URL: url, - SplitCount: config.SplitCount, - }, nil -} -func (chat *Chat) CanHandleMessage(_ providers.Message) bool { - return true -} + logging.Log("NOTIF-kSvPp").Debug("successfully initialized chat email and sms channel") -func (chat *Chat) HandleMessage(message providers.Message) error { - contentText := message.GetContent() - for _, splittedMsg := range splitMessage(contentText, chat.SplitCount) { - chatMsg := &ChatMessage{Text: splittedMsg} - if err := chat.SendMessage(chatMsg); err != nil { - return err + return channels.HandleMessageFunc(func(message channels.Message) error { + contentText := message.GetContent() + if config.Compact { + contentText = html2text.HTML2Text(contentText) } - } - return nil + for _, splittedMsg := range splitMessage(contentText, config.SplitCount) { + if err := sendMessage(splittedMsg, url); err != nil { + return err + } + } + return nil + }), nil } -func (chat *Chat) SendMessage(message providers.Message) error { - chatMsg, ok := message.(*ChatMessage) - if !ok { - return caos_errs.ThrowInternal(nil, "EMAIL-s8JLs", "message is not ChatMessage") - } - req, err := json.Marshal(chatMsg) +func sendMessage(message string, chatUrl *url.URL) error { + req, err := json.Marshal(message) if err != nil { return caos_errs.ThrowInternal(err, "PROVI-s8uie", "Could not unmarshal content") } - response, err := http.Post(chat.URL.String(), "application/json; charset=UTF-8", bytes.NewReader(req)) + response, err := http.Post(chatUrl.String(), "application/json; charset=UTF-8", bytes.NewReader(req)) if err != nil { return caos_errs.ThrowInternal(err, "PROVI-si93s", "unable to send message") } diff --git a/internal/notification/channels/chat/config.go b/internal/notification/channels/chat/config.go new file mode 100644 index 0000000000..0c125d7cc8 --- /dev/null +++ b/internal/notification/channels/chat/config.go @@ -0,0 +1,9 @@ +package chat + +type ChatConfig struct { + // Defaults to true if DebugMode is set to true + Enabled *bool + Url string + SplitCount int + Compact bool +} diff --git a/internal/notification/channels/fs/channel.go b/internal/notification/channels/fs/channel.go new file mode 100644 index 0000000000..751d225360 --- /dev/null +++ b/internal/notification/channels/fs/channel.go @@ -0,0 +1,51 @@ +package fs + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/caos/logging" + + caos_errors "github.com/caos/zitadel/internal/errors" + + "github.com/k3a/html2text" + + "github.com/caos/zitadel/internal/notification/channels" + "github.com/caos/zitadel/internal/notification/messages" +) + +func InitFSChannel(config FSConfig) (channels.NotificationChannel, error) { + + if err := os.MkdirAll(config.Path, os.ModePerm); err != nil { + return nil, err + } + + logging.Log("NOTIF-kSvPp").Debug("successfully initialized filesystem email and sms channel") + + return channels.HandleMessageFunc(func(message channels.Message) error { + + fileName := fmt.Sprintf("%d_", time.Now().Unix()) + content := message.GetContent() + switch msg := message.(type) { + case *messages.Email: + recipients := make([]string, len(msg.Recipients)) + copy(recipients, msg.Recipients) + sort.Strings(recipients) + fileName = fileName + "mail_to_" + strings.Join(recipients, "_") + ".html" + if config.Compact { + content = html2text.HTML2Text(content) + } + case *messages.SMS: + fileName = fileName + "sms_to_" + msg.RecipientPhoneNumber + ".txt" + default: + return caos_errors.ThrowUnimplementedf(nil, "NOTIF-6f9a1", "filesystem provider doesn't support message type %T", message) + } + + return ioutil.WriteFile(filepath.Join(config.Path, fileName), []byte(content), 0666) + }), nil +} diff --git a/internal/notification/channels/fs/config.go b/internal/notification/channels/fs/config.go new file mode 100644 index 0000000000..eb64e1eecd --- /dev/null +++ b/internal/notification/channels/fs/config.go @@ -0,0 +1,7 @@ +package fs + +type FSConfig struct { + Enabled bool + Path string + Compact bool +} diff --git a/internal/notification/channels/gen_mock.go b/internal/notification/channels/gen_mock.go new file mode 100644 index 0000000000..4eb98bffcc --- /dev/null +++ b/internal/notification/channels/gen_mock.go @@ -0,0 +1,4 @@ +package channels + +//go:generate mockgen -package mock -destination ./mock/channel.mock.go github.com/caos/zitadel/internal/notification/channels NotificationChannel +//go:generate mockgen -package mock -destination ./mock/message.mock.go github.com/caos/zitadel/internal/notification/channels Message diff --git a/internal/notification/channels/log/channel.go b/internal/notification/channels/log/channel.go new file mode 100644 index 0000000000..2bb1bc6dec --- /dev/null +++ b/internal/notification/channels/log/channel.go @@ -0,0 +1,29 @@ +package log + +import ( + "fmt" + + "github.com/k3a/html2text" + + "github.com/caos/logging" + "github.com/caos/zitadel/internal/notification/channels" +) + +func InitStdoutChannel(config LogConfig) channels.NotificationChannel { + + logging.Log("NOTIF-D0164").Debug("successfully initialized stdout email and sms channel") + + return channels.HandleMessageFunc(func(message channels.Message) error { + + content := message.GetContent() + if config.Compact { + content = html2text.HTML2Text(content) + } + + logging.Log("NOTIF-c73ba").WithFields(map[string]interface{}{ + "type": fmt.Sprintf("%T", message), + "content": content, + }).Info("handling notification message") + return nil + }) +} diff --git a/internal/notification/channels/log/config.go b/internal/notification/channels/log/config.go new file mode 100644 index 0000000000..7e9946bc15 --- /dev/null +++ b/internal/notification/channels/log/config.go @@ -0,0 +1,6 @@ +package log + +type LogConfig struct { + Enabled bool + Compact bool +} diff --git a/internal/notification/channels/mock/channel.mock.go b/internal/notification/channels/mock/channel.mock.go new file mode 100644 index 0000000000..ec564371d2 --- /dev/null +++ b/internal/notification/channels/mock/channel.mock.go @@ -0,0 +1,48 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/caos/zitadel/internal/notification/channels (interfaces: NotificationChannel) + +// Package mock is a generated GoMock package. +package mock + +import ( + channels "github.com/caos/zitadel/internal/notification/channels" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockNotificationChannel is a mock of NotificationChannel interface +type MockNotificationChannel struct { + ctrl *gomock.Controller + recorder *MockNotificationChannelMockRecorder +} + +// MockNotificationChannelMockRecorder is the mock recorder for MockNotificationChannel +type MockNotificationChannelMockRecorder struct { + mock *MockNotificationChannel +} + +// NewMockNotificationChannel creates a new mock instance +func NewMockNotificationChannel(ctrl *gomock.Controller) *MockNotificationChannel { + mock := &MockNotificationChannel{ctrl: ctrl} + mock.recorder = &MockNotificationChannelMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockNotificationChannel) EXPECT() *MockNotificationChannelMockRecorder { + return m.recorder +} + +// HandleMessage mocks base method +func (m *MockNotificationChannel) HandleMessage(arg0 channels.Message) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HandleMessage", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// HandleMessage indicates an expected call of HandleMessage +func (mr *MockNotificationChannelMockRecorder) HandleMessage(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleMessage", reflect.TypeOf((*MockNotificationChannel)(nil).HandleMessage), arg0) +} diff --git a/internal/notification/providers/mock/message.mock.go b/internal/notification/channels/mock/message.mock.go similarity index 93% rename from internal/notification/providers/mock/message.mock.go rename to internal/notification/channels/mock/message.mock.go index 6c1f12961f..5db3bb7119 100644 --- a/internal/notification/providers/mock/message.mock.go +++ b/internal/notification/channels/mock/message.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/caos/zitadel/internal/notification/providers (interfaces: Message) +// Source: github.com/caos/zitadel/internal/notification/channels (interfaces: Message) // Package mock is a generated GoMock package. package mock diff --git a/internal/notification/providers/email/provider.go b/internal/notification/channels/smtp/channel.go similarity index 81% rename from internal/notification/providers/email/provider.go rename to internal/notification/channels/smtp/channel.go index 8c2a9a3efd..d57ee63968 100644 --- a/internal/notification/providers/email/provider.go +++ b/internal/notification/channels/smtp/channel.go @@ -1,44 +1,48 @@ -package email +package smtp import ( "crypto/tls" "net" "net/smtp" + "github.com/caos/zitadel/internal/notification/messages" + "github.com/caos/logging" caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/notification/providers" + "github.com/caos/zitadel/internal/notification/channels" "github.com/pkg/errors" ) +var _ channels.NotificationChannel = (*Email)(nil) + type Email struct { smtpClient *smtp.Client } -func InitEmailProvider(config EmailConfig) (*Email, error) { +func InitSMTPChannel(config EmailConfig) (*Email, error) { client, err := config.SMTP.connectToSMTP(config.Tls) if err != nil { return nil, err } + + logging.Log("NOTIF-4n4Ih").Debug("successfully initialized smtp email channel") + return &Email{ smtpClient: client, }, nil } -func (email *Email) CanHandleMessage(message providers.Message) bool { - msg, ok := message.(*EmailMessage) - if !ok { - return false - } - return msg.Content != "" && msg.Subject != "" && len(msg.Recipients) > 0 -} - -func (email *Email) HandleMessage(message providers.Message) error { +func (email *Email) HandleMessage(message channels.Message) error { defer email.smtpClient.Close() - emailMsg, ok := message.(*EmailMessage) + emailMsg, ok := message.(*messages.Email) if !ok { return caos_errs.ThrowInternal(nil, "EMAIL-s8JLs", "message is not EmailMessage") } + + if emailMsg.Content == "" || emailMsg.Subject == "" || len(emailMsg.Recipients) == 0 { + return caos_errs.ThrowInternalf(nil, "EMAIL-zGemZ", "subject, recipients and content must be set but got subject %s, recipients length %d and content length %d", emailMsg.Subject, len(emailMsg.Recipients), len(emailMsg.Content)) + } + // To && From if err := email.smtpClient.Mail(emailMsg.SenderEmail); err != nil { return caos_errs.ThrowInternalf(err, "EMAIL-s3is3", "could not set sender: %v", emailMsg.SenderEmail) diff --git a/internal/notification/providers/email/config.go b/internal/notification/channels/smtp/config.go similarity index 94% rename from internal/notification/providers/email/config.go rename to internal/notification/channels/smtp/config.go index e747ad8fc3..bfa5abda35 100644 --- a/internal/notification/providers/email/config.go +++ b/internal/notification/channels/smtp/config.go @@ -1,4 +1,4 @@ -package email +package smtp type EmailConfig struct { SMTP SMTP diff --git a/internal/notification/channels/twilio/channel.go b/internal/notification/channels/twilio/channel.go new file mode 100644 index 0000000000..765d03205b --- /dev/null +++ b/internal/notification/channels/twilio/channel.go @@ -0,0 +1,28 @@ +package twilio + +import ( + "github.com/caos/logging" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/notification/channels" + "github.com/caos/zitadel/internal/notification/messages" + twilio "github.com/kevinburke/twilio-go" +) + +func InitTwilioChannel(config TwilioConfig) channels.NotificationChannel { + client := twilio.NewClient(config.SID, config.Token, nil) + + logging.Log("NOTIF-KaxDZ").Debug("successfully initialized twilio sms channel") + + return channels.HandleMessageFunc(func(message channels.Message) error { + twilioMsg, ok := message.(*messages.SMS) + if !ok { + return caos_errs.ThrowInternal(nil, "TWILI-s0pLc", "message is not SMS") + } + m, err := client.Messages.SendMessage(twilioMsg.SenderPhoneNumber, twilioMsg.RecipientPhoneNumber, twilioMsg.GetContent(), nil) + if err != nil { + return caos_errs.ThrowInternal(err, "TWILI-osk3S", "could not send message") + } + logging.LogWithFields("SMS_-f335c523", "message_sid", m.Sid, "status", m.Status).Debug("sms sent") + return nil + }) +} diff --git a/internal/notification/providers/twilio/config.go b/internal/notification/channels/twilio/config.go similarity index 100% rename from internal/notification/providers/twilio/config.go rename to internal/notification/channels/twilio/config.go diff --git a/internal/notification/providers/email/message.go b/internal/notification/messages/email.go similarity index 84% rename from internal/notification/providers/email/message.go rename to internal/notification/messages/email.go index fcd9928355..51e151c1d5 100644 --- a/internal/notification/providers/email/message.go +++ b/internal/notification/messages/email.go @@ -1,9 +1,11 @@ -package email +package messages import ( "fmt" "regexp" "strings" + + "github.com/caos/zitadel/internal/notification/channels" ) var ( @@ -11,7 +13,9 @@ var ( lineBreak = "\r\n" ) -type EmailMessage struct { +var _ channels.Message = (*Email)(nil) + +type Email struct { Recipients []string BCC []string CC []string @@ -20,7 +24,7 @@ type EmailMessage struct { Content string } -func (msg *EmailMessage) GetContent() string { +func (msg *Email) GetContent() string { headers := make(map[string]string) headers["From"] = msg.SenderEmail headers["To"] = strings.Join(msg.Recipients, ", ") diff --git a/internal/notification/messages/sms.go b/internal/notification/messages/sms.go new file mode 100644 index 0000000000..128fbcf613 --- /dev/null +++ b/internal/notification/messages/sms.go @@ -0,0 +1,15 @@ +package messages + +import "github.com/caos/zitadel/internal/notification/channels" + +var _ channels.Message = (*SMS)(nil) + +type SMS struct { + SenderPhoneNumber string + RecipientPhoneNumber string + Content string +} + +func (msg *SMS) GetContent() string { + return msg.Content +} diff --git a/internal/notification/providers/chat/config.go b/internal/notification/providers/chat/config.go deleted file mode 100644 index 8a203b6db8..0000000000 --- a/internal/notification/providers/chat/config.go +++ /dev/null @@ -1,6 +0,0 @@ -package chat - -type ChatConfig struct { - Url string - SplitCount int -} diff --git a/internal/notification/providers/chat/message.go b/internal/notification/providers/chat/message.go deleted file mode 100644 index 115c4e2fb2..0000000000 --- a/internal/notification/providers/chat/message.go +++ /dev/null @@ -1,9 +0,0 @@ -package chat - -type ChatMessage struct { - Text string `json:"text"` -} - -func (msg *ChatMessage) GetContent() string { - return msg.Text -} diff --git a/internal/notification/providers/gen_mock.go b/internal/notification/providers/gen_mock.go deleted file mode 100644 index 83d9f5febc..0000000000 --- a/internal/notification/providers/gen_mock.go +++ /dev/null @@ -1,4 +0,0 @@ -package providers - -//go:generate mockgen -package mock -destination ./mock/provider.mock.go github.com/caos/zitadel/internal/notification/providers NotificationProvider -//go:generate mockgen -package mock -destination ./mock/message.mock.go github.com/caos/zitadel/internal/notification/providers Message diff --git a/internal/notification/providers/message.go b/internal/notification/providers/message.go deleted file mode 100644 index 04d2c9b2d3..0000000000 --- a/internal/notification/providers/message.go +++ /dev/null @@ -1,5 +0,0 @@ -package providers - -type Message interface { - GetContent() string -} diff --git a/internal/notification/providers/mock/provider.mock.go b/internal/notification/providers/mock/provider.mock.go deleted file mode 100644 index c0ecc4054f..0000000000 --- a/internal/notification/providers/mock/provider.mock.go +++ /dev/null @@ -1,61 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/caos/zitadel/internal/notification/providers (interfaces: NotificationProvider) - -// Package mock is a generated GoMock package. -package mock - -import ( - gomock "github.com/golang/mock/gomock" - reflect "reflect" -) - -// MockNotificationProvider is a mock of NotificationProvider interface -type MockNotificationProvider struct { - ctrl *gomock.Controller - recorder *MockNotificationProviderMockRecorder -} - -// MockNotificationProviderMockRecorder is the mock recorder for MockNotificationProvider -type MockNotificationProviderMockRecorder struct { - mock *MockNotificationProvider -} - -// NewMockNotificationProvider creates a new mock instance -func NewMockNotificationProvider(ctrl *gomock.Controller) *MockNotificationProvider { - mock := &MockNotificationProvider{ctrl: ctrl} - mock.recorder = &MockNotificationProviderMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockNotificationProvider) EXPECT() *MockNotificationProviderMockRecorder { - return m.recorder -} - -// CanHandleMessage mocks base method -func (m *MockNotificationProvider) CanHandleMessage() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CanHandleMessage") - ret0, _ := ret[0].(bool) - return ret0 -} - -// CanHandleMessage indicates an expected call of CanHandleMessage -func (mr *MockNotificationProviderMockRecorder) CanHandleMessage() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanHandleMessage", reflect.TypeOf((*MockNotificationProvider)(nil).CanHandleMessage)) -} - -// HandleMessage mocks base method -func (m *MockNotificationProvider) HandleMessage() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HandleMessage") - ret0, _ := ret[0].(error) - return ret0 -} - -// HandleMessage indicates an expected call of HandleMessage -func (mr *MockNotificationProviderMockRecorder) HandleMessage() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleMessage", reflect.TypeOf((*MockNotificationProvider)(nil).HandleMessage)) -} diff --git a/internal/notification/providers/provider.go b/internal/notification/providers/provider.go deleted file mode 100644 index 68c5b47984..0000000000 --- a/internal/notification/providers/provider.go +++ /dev/null @@ -1,6 +0,0 @@ -package providers - -type NotificationProvider interface { - CanHandleMessage() bool - HandleMessage() error -} diff --git a/internal/notification/providers/twilio/message.go b/internal/notification/providers/twilio/message.go deleted file mode 100644 index 56e4ef265c..0000000000 --- a/internal/notification/providers/twilio/message.go +++ /dev/null @@ -1,11 +0,0 @@ -package twilio - -type TwilioMessage struct { - SenderPhoneNumber string - RecipientPhoneNumber string - Content string -} - -func (msg *TwilioMessage) GetContent() string { - return msg.Content -} diff --git a/internal/notification/providers/twilio/provider.go b/internal/notification/providers/twilio/provider.go deleted file mode 100644 index 7e1aaba534..0000000000 --- a/internal/notification/providers/twilio/provider.go +++ /dev/null @@ -1,39 +0,0 @@ -package twilio - -import ( - "github.com/caos/logging" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/notification/providers" - twilio "github.com/kevinburke/twilio-go" -) - -type Twilio struct { - client *twilio.Client -} - -func InitTwilioProvider(config TwilioConfig) *Twilio { - return &Twilio{ - client: twilio.NewClient(config.SID, config.Token, nil), - } -} - -func (t *Twilio) CanHandleMessage(message providers.Message) bool { - twilioMsg, ok := message.(*TwilioMessage) - if !ok { - return false - } - return twilioMsg.Content != "" && twilioMsg.RecipientPhoneNumber != "" && twilioMsg.SenderPhoneNumber != "" -} - -func (t *Twilio) HandleMessage(message providers.Message) error { - twilioMsg, ok := message.(*TwilioMessage) - if !ok { - return caos_errs.ThrowInternal(nil, "TWILI-s0pLc", "message is not TwilioMessage") - } - m, err := t.client.Messages.SendMessage(twilioMsg.SenderPhoneNumber, twilioMsg.RecipientPhoneNumber, twilioMsg.GetContent(), nil) - if err != nil { - return caos_errs.ThrowInternal(err, "TWILI-osk3S", "could not send message") - } - logging.LogWithFields("SMS_-f335c523", "message_sid", m.Sid, "status", m.Status).Debug("sms sent") - return nil -} diff --git a/internal/notification/senders/chain.go b/internal/notification/senders/chain.go new file mode 100644 index 0000000000..07de483f47 --- /dev/null +++ b/internal/notification/senders/chain.go @@ -0,0 +1,24 @@ +package senders + +import "github.com/caos/zitadel/internal/notification/channels" + +var _ channels.NotificationChannel = (*Chain)(nil) + +type Chain struct { + channels []channels.NotificationChannel +} + +func chainChannels(channel ...channels.NotificationChannel) *Chain { + return &Chain{channels: channel} +} + +// HandleMessage returns a non nil error from a provider immediately if any occurs +// messages are sent to channels in the same order they were provided to chainChannels() +func (c *Chain) HandleMessage(message channels.Message) error { + for i := range c.channels { + if err := c.channels[i].HandleMessage(message); err != nil { + return err + } + } + return nil +} diff --git a/internal/notification/senders/debug.go b/internal/notification/senders/debug.go new file mode 100644 index 0000000000..c7257813af --- /dev/null +++ b/internal/notification/senders/debug.go @@ -0,0 +1,46 @@ +package senders + +import ( + "github.com/caos/zitadel/internal/config/systemdefaults" + "github.com/caos/zitadel/internal/notification/channels" + "github.com/caos/zitadel/internal/notification/channels/chat" + "github.com/caos/zitadel/internal/notification/channels/fs" + "github.com/caos/zitadel/internal/notification/channels/log" +) + +func debugChannels(config systemdefaults.Notifications) (channels.NotificationChannel, error) { + + var ( + providers []channels.NotificationChannel + enableChat bool + ) + + if config.Providers.Chat.Enabled != nil { + enableChat = *config.Providers.Chat.Enabled + } else { + // ensures backward compatible configuration + enableChat = config.DebugMode + } + + if enableChat { + p, err := chat.InitChatChannel(config.Providers.Chat) + if err != nil { + return nil, err + } + providers = append(providers, p) + } + + if config.Providers.FileSystem.Enabled { + p, err := fs.InitFSChannel(config.Providers.FileSystem) + if err != nil { + return nil, err + } + providers = append(providers, p) + } + + if config.Providers.Log.Enabled { + providers = append(providers, log.InitStdoutChannel(config.Providers.Log)) + } + + return chainChannels(providers...), nil +} diff --git a/internal/notification/senders/email.go b/internal/notification/senders/email.go new file mode 100644 index 0000000000..725be49af8 --- /dev/null +++ b/internal/notification/senders/email.go @@ -0,0 +1,25 @@ +package senders + +import ( + "github.com/caos/zitadel/internal/config/systemdefaults" + "github.com/caos/zitadel/internal/notification/channels" + "github.com/caos/zitadel/internal/notification/channels/smtp" +) + +func EmailChannels(config systemdefaults.Notifications) (channels.NotificationChannel, error) { + + debug, err := debugChannels(config) + if err != nil { + return nil, err + } + + if !config.DebugMode { + p, err := smtp.InitSMTPChannel(config.Providers.Email) + if err != nil { + return nil, err + } + return chainChannels(debug, p), nil + } + + return debug, nil +} diff --git a/internal/notification/senders/sms.go b/internal/notification/senders/sms.go new file mode 100644 index 0000000000..5da4a670ec --- /dev/null +++ b/internal/notification/senders/sms.go @@ -0,0 +1,21 @@ +package senders + +import ( + "github.com/caos/zitadel/internal/config/systemdefaults" + "github.com/caos/zitadel/internal/notification/channels" + "github.com/caos/zitadel/internal/notification/channels/twilio" +) + +func SMSChannels(config systemdefaults.Notifications) (channels.NotificationChannel, error) { + + debug, err := debugChannels(config) + if err != nil { + return nil, err + } + + if !config.DebugMode { + return chainChannels(debug, twilio.InitTwilioChannel(config.Providers.Twilio)), nil + } + + return debug, nil +} diff --git a/internal/notification/types/user_email.go b/internal/notification/types/user_email.go index 4dfe6c9ed0..9a54142c1d 100644 --- a/internal/notification/types/user_email.go +++ b/internal/notification/types/user_email.go @@ -3,21 +3,16 @@ package types import ( "html" + "github.com/caos/zitadel/internal/notification/messages" + "github.com/caos/zitadel/internal/notification/senders" + "github.com/caos/zitadel/internal/config/systemdefaults" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/notification/providers" - "github.com/caos/zitadel/internal/notification/providers/chat" - "github.com/caos/zitadel/internal/notification/providers/email" view_model "github.com/caos/zitadel/internal/user/repository/view/model" ) func generateEmail(user *view_model.NotifyUser, subject, content string, config systemdefaults.Notifications, lastEmail bool) error { - provider, err := email.InitEmailProvider(config.Providers.Email) - if err != nil { - return err - } content = html.UnescapeString(content) - message := &email.EmailMessage{ + message := &messages.Email{ SenderEmail: config.Providers.Email.From, Recipients: []string{user.VerifiedEmail}, Subject: subject, @@ -26,21 +21,13 @@ func generateEmail(user *view_model.NotifyUser, subject, content string, config if lastEmail { message.Recipients = []string{user.LastEmail} } - if provider.CanHandleMessage(message) { - if config.DebugMode { - return sendDebugEmail(message, config) - } - return provider.HandleMessage(message) - } - return caos_errs.ThrowInternalf(nil, "NOTIF-s8ipw", "Could not send init message: userid: %v", user.ID) -} -func sendDebugEmail(message providers.Message, config systemdefaults.Notifications) error { - provider, err := chat.InitChatProvider(config.Providers.Chat) + channels, err := senders.EmailChannels(config) if err != nil { return err } - return provider.HandleMessage(message) + + return channels.HandleMessage(message) } func mapNotifyUserToArgs(user *view_model.NotifyUser) map[string]interface{} { diff --git a/internal/notification/types/user_phone.go b/internal/notification/types/user_phone.go index 4e201ec45a..1d168f0345 100644 --- a/internal/notification/types/user_phone.go +++ b/internal/notification/types/user_phone.go @@ -2,16 +2,13 @@ package types import ( "github.com/caos/zitadel/internal/config/systemdefaults" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/notification/providers" - "github.com/caos/zitadel/internal/notification/providers/chat" - "github.com/caos/zitadel/internal/notification/providers/twilio" + "github.com/caos/zitadel/internal/notification/messages" + "github.com/caos/zitadel/internal/notification/senders" view_model "github.com/caos/zitadel/internal/user/repository/view/model" ) func generateSms(user *view_model.NotifyUser, content string, config systemdefaults.Notifications, lastPhone bool) error { - provider := twilio.InitTwilioProvider(config.Providers.Twilio) - message := &twilio.TwilioMessage{ + message := &messages.SMS{ SenderPhoneNumber: config.Providers.Twilio.From, RecipientPhoneNumber: user.VerifiedPhone, Content: content, @@ -19,19 +16,10 @@ func generateSms(user *view_model.NotifyUser, content string, config systemdefau if lastPhone { message.RecipientPhoneNumber = user.LastPhone } - if provider.CanHandleMessage(message) { - if config.DebugMode { - return sendDebugPhone(message, config) - } - return provider.HandleMessage(message) - } - return caos_errs.ThrowInternalf(nil, "NOTIF-s8ipw", "Could not send init message: userid: %v", user.ID) -} -func sendDebugPhone(message providers.Message, config systemdefaults.Notifications) error { - provider, err := chat.InitChatProvider(config.Providers.Chat) + channels, err := senders.SMSChannels(config) if err != nil { return err } - return provider.HandleMessage(message) + return channels.HandleMessage(message) }