diff --git a/controller/config/config.go b/controller/config/config.go index d2ac3eb6..24422afd 100644 --- a/controller/config/config.go +++ b/controller/config/config.go @@ -1,6 +1,7 @@ package config import ( + "github.com/openziti/zrok/controller/emailUi" "github.com/openziti/zrok/controller/env" "github.com/openziti/zrok/controller/limits" "github.com/openziti/zrok/controller/metrics" @@ -19,7 +20,7 @@ type Config struct { Admin *AdminConfig Bridge *metrics.BridgeConfig Endpoint *EndpointConfig - Email *EmailConfig + Email *emailUi.Config Limits *limits.Config Maintenance *MaintenanceConfig Metrics *metrics.Config @@ -39,14 +40,6 @@ type EndpointConfig struct { Port int } -type EmailConfig struct { - Host string - Port int - Username string - Password string `cf:"+secret"` - From string -} - type RegistrationConfig struct { RegistrationUrlTemplate string TokenStrategy string diff --git a/controller/controller.go b/controller/controller.go index 987e2b4a..1d05e128 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -84,7 +84,7 @@ func Run(inCfg *config.Config) error { defer func() { ma.Stop() }() if cfg.Limits != nil && cfg.Limits.Enforcing { - limitsAgent, err = limits.NewAgent(cfg.Limits, cfg.Metrics.Influx, cfg.Ziti, str) + limitsAgent, err = limits.NewAgent(cfg.Limits, cfg.Metrics.Influx, cfg.Ziti, cfg.Email, str) if err != nil { return errors.Wrap(err, "error creating limits agent") } diff --git a/controller/emailUi/config.go b/controller/emailUi/config.go new file mode 100644 index 00000000..a97f141a --- /dev/null +++ b/controller/emailUi/config.go @@ -0,0 +1,9 @@ +package emailUi + +type Config struct { + Host string + Port int + Username string + Password string `cf:"+secret"` + From string +} diff --git a/controller/emailUi/model.go b/controller/emailUi/model.go new file mode 100644 index 00000000..8e30b336 --- /dev/null +++ b/controller/emailUi/model.go @@ -0,0 +1,24 @@ +package emailUi + +import ( + "bytes" + "github.com/pkg/errors" + "html/template" +) + +type WarningEmail struct { + EmailAddress string + Detail string +} + +func (we WarningEmail) MergeTemplate(filename string) (string, error) { + t, err := template.ParseFS(FS, filename) + if err != nil { + return "", errors.Wrapf(err, "error parsing warning email template '%v'", filename) + } + buf := new(bytes.Buffer) + if err := t.Execute(buf, we); err != nil { + return "", errors.Wrapf(err, "error executing warning email template '%v'", filename) + } + return buf.String(), nil +} diff --git a/controller/limits/accounttWarningAction.go b/controller/limits/accountWarningAction.go similarity index 51% rename from controller/limits/accounttWarningAction.go rename to controller/limits/accountWarningAction.go index 9a79341d..39fb9582 100644 --- a/controller/limits/accounttWarningAction.go +++ b/controller/limits/accountWarningAction.go @@ -3,20 +3,28 @@ package limits import ( "github.com/jmoiron/sqlx" "github.com/openziti/edge/rest_management_api_client" + "github.com/openziti/zrok/controller/emailUi" "github.com/openziti/zrok/controller/store" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) type accountWarningAction struct { str *store.Store edge *rest_management_api_client.ZitiEdgeManagement + cfg *emailUi.Config } -func newAccountWarningAction(str *store.Store, edge *rest_management_api_client.ZitiEdgeManagement) *accountWarningAction { - return &accountWarningAction{str, edge} +func newAccountWarningAction(cfg *emailUi.Config, str *store.Store, edge *rest_management_api_client.ZitiEdgeManagement) *accountWarningAction { + return &accountWarningAction{str, edge, cfg} } func (a *accountWarningAction) HandleAccount(acct *store.Account, rxBytes, txBytes int64, limit *BandwidthPerPeriod, trx *sqlx.Tx) error { logrus.Infof("warning '%v'", acct.Email) + + if err := sendLimitWarningEmail(a.cfg, acct.Email, limit, rxBytes, txBytes); err != nil { + return errors.Wrapf(err, "error sending limit warning email to '%v'", acct.Email) + } + return nil } diff --git a/controller/limits/agent.go b/controller/limits/agent.go index ba22cec3..3a8b9915 100644 --- a/controller/limits/agent.go +++ b/controller/limits/agent.go @@ -3,6 +3,7 @@ package limits import ( "fmt" "github.com/jmoiron/sqlx" + "github.com/openziti/zrok/controller/emailUi" "github.com/openziti/zrok/controller/metrics" "github.com/openziti/zrok/controller/store" "github.com/openziti/zrok/controller/zrokEdgeSdk" @@ -31,7 +32,7 @@ type Agent struct { join chan struct{} } -func NewAgent(cfg *Config, ifxCfg *metrics.InfluxConfig, zCfg *zrokEdgeSdk.Config, str *store.Store) (*Agent, error) { +func NewAgent(cfg *Config, ifxCfg *metrics.InfluxConfig, zCfg *zrokEdgeSdk.Config, emailCfg *emailUi.Config, str *store.Store) (*Agent, error) { edge, err := zrokEdgeSdk.Client(zCfg) if err != nil { return nil, err @@ -42,13 +43,13 @@ func NewAgent(cfg *Config, ifxCfg *metrics.InfluxConfig, zCfg *zrokEdgeSdk.Confi zCfg: zCfg, str: str, queue: make(chan *metrics.Usage, 1024), - acctWarningActions: []AccountAction{newAccountWarningAction(str, edge)}, + acctWarningActions: []AccountAction{newAccountWarningAction(emailCfg, str, edge)}, acctLimitActions: []AccountAction{newAccountLimitAction(str, edge)}, acctRelaxActions: []AccountAction{newAccountRelaxAction(str, edge)}, - envWarningActions: []EnvironmentAction{newEnvironmentWarningAction(str, edge)}, + envWarningActions: []EnvironmentAction{newEnvironmentWarningAction(emailCfg, str, edge)}, envLimitActions: []EnvironmentAction{newEnvironmentLimitAction(str, edge)}, envRelaxActions: []EnvironmentAction{newEnvironmentRelaxAction(str, edge)}, - shrWarningActions: []ShareAction{newShareWarningAction(str, edge)}, + shrWarningActions: []ShareAction{newShareWarningAction(emailCfg, str, edge)}, shrLimitActions: []ShareAction{newShareLimitAction(str, edge)}, shrRelaxActions: []ShareAction{newShareRelaxAction(str, edge)}, close: make(chan struct{}), @@ -550,7 +551,7 @@ func (a *Agent) checkShareLimit(shrToken string) (enforce, warning bool, rxBytes enforce, warning = a.checkLimit(limit, rx, tx) if enforce || warning { - logrus.Debugf("'%v': %v", shrToken, a.describeLimit(limit, rx, tx)) + logrus.Debugf("'%v': %v", shrToken, describeLimit(limit, rx, tx)) } return enforce, warning, rx, tx, nil @@ -580,7 +581,7 @@ func (a *Agent) checkLimit(cfg *BandwidthPerPeriod, rx, tx int64) (enforce, warn return false, false } -func (a *Agent) describeLimit(cfg *BandwidthPerPeriod, rx, tx int64) string { +func describeLimit(cfg *BandwidthPerPeriod, rx, tx int64) string { out := "" if cfg.Limit.Rx != Unlimited && rx > cfg.Limit.Rx { diff --git a/controller/limits/email.go b/controller/limits/email.go new file mode 100644 index 00000000..56680aff --- /dev/null +++ b/controller/limits/email.go @@ -0,0 +1,58 @@ +package limits + +import ( + "github.com/openziti/zrok/controller/emailUi" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/wneessen/go-mail" +) + +func sendLimitWarningEmail(cfg *emailUi.Config, emailTo string, limit *BandwidthPerPeriod, rxBytes, txBytes int64) error { + emailData := &emailUi.WarningEmail{ + EmailAddress: emailTo, + Detail: describeLimit(limit, rxBytes, txBytes), + } + + plainBody, err := emailData.MergeTemplate("limitWarning.gotext") + if err != nil { + return err + } + htmlBody, err := emailData.MergeTemplate("resetPassword.gohtml") + if err != nil { + return err + } + + msg := mail.NewMsg() + if err := msg.From(cfg.From); err != nil { + return errors.Wrap(err, "failed to set from address in limit warning email") + } + if err := msg.To(emailTo); err != nil { + return errors.Wrap(err, "failed to set to address in limit warning email") + } + + msg.Subject("Limit Warning Notification") + msg.SetDate() + msg.SetMessageID() + msg.SetBulk() + msg.SetImportance(mail.ImportanceHigh) + msg.SetBodyString(mail.TypeTextPlain, plainBody) + msg.SetBodyString(mail.TypeTextHTML, htmlBody) + + client, err := mail.NewClient(cfg.Host, + mail.WithPort(cfg.Port), + mail.WithSMTPAuth(mail.SMTPAuthPlain), + mail.WithUsername(cfg.Username), + mail.WithPassword(cfg.Password), + mail.WithTLSPolicy(mail.TLSMandatory), + ) + + if err != nil { + return errors.Wrap(err, "error creating limit warning email client") + } + if err := client.DialAndSend(msg); err != nil { + return errors.Wrap(err, "error sending limit warning email") + } + + logrus.Infof("limit warning email sent to '%v'", emailTo) + return nil +} diff --git a/controller/limits/environmentWarningAction.go b/controller/limits/environmentWarningAction.go index 2295a45f..c3f25903 100644 --- a/controller/limits/environmentWarningAction.go +++ b/controller/limits/environmentWarningAction.go @@ -3,20 +3,35 @@ package limits import ( "github.com/jmoiron/sqlx" "github.com/openziti/edge/rest_management_api_client" + "github.com/openziti/zrok/controller/emailUi" "github.com/openziti/zrok/controller/store" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) type environmentWarningAction struct { str *store.Store edge *rest_management_api_client.ZitiEdgeManagement + cfg *emailUi.Config } -func newEnvironmentWarningAction(str *store.Store, edge *rest_management_api_client.ZitiEdgeManagement) *environmentWarningAction { - return &environmentWarningAction{str, edge} +func newEnvironmentWarningAction(cfg *emailUi.Config, str *store.Store, edge *rest_management_api_client.ZitiEdgeManagement) *environmentWarningAction { + return &environmentWarningAction{str, edge, cfg} } -func (a *environmentWarningAction) HandleEnvironment(e *store.Environment, rxBytes, txBytes int64, limit *BandwidthPerPeriod, trx *sqlx.Tx) error { - logrus.Infof("warning '%v'", e.ZId) +func (a *environmentWarningAction) HandleEnvironment(env *store.Environment, rxBytes, txBytes int64, limit *BandwidthPerPeriod, trx *sqlx.Tx) error { + logrus.Infof("warning '%v'", env.ZId) + + if env.AccountId != nil { + acct, err := a.str.GetAccount(*env.AccountId, trx) + if err != nil { + return err + } + + if err := sendLimitWarningEmail(a.cfg, acct.Email, limit, rxBytes, txBytes); err != nil { + return errors.Wrapf(err, "error sending limit warning email to '%v'", acct.Email) + } + } + return nil } diff --git a/controller/limits/shareWarningAction.go b/controller/limits/shareWarningAction.go index 90119879..c8ca9e65 100644 --- a/controller/limits/shareWarningAction.go +++ b/controller/limits/shareWarningAction.go @@ -3,20 +3,38 @@ package limits import ( "github.com/jmoiron/sqlx" "github.com/openziti/edge/rest_management_api_client" + "github.com/openziti/zrok/controller/emailUi" "github.com/openziti/zrok/controller/store" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) type shareWarningAction struct { str *store.Store edge *rest_management_api_client.ZitiEdgeManagement + cfg *emailUi.Config } -func newShareWarningAction(str *store.Store, edge *rest_management_api_client.ZitiEdgeManagement) *shareWarningAction { - return &shareWarningAction{str, edge} +func newShareWarningAction(cfg *emailUi.Config, str *store.Store, edge *rest_management_api_client.ZitiEdgeManagement) *shareWarningAction { + return &shareWarningAction{str, edge, cfg} } -func (a *shareWarningAction) HandleShare(s *store.Share, rxBytes, txBytes int64, limit *BandwidthPerPeriod, trx *sqlx.Tx) error { - logrus.Infof("warning '%v'", s.Token) +func (a *shareWarningAction) HandleShare(shr *store.Share, rxBytes, txBytes int64, limit *BandwidthPerPeriod, trx *sqlx.Tx) error { + logrus.Infof("warning '%v'", shr.Token) + + env, err := a.str.GetEnvironment(shr.EnvironmentId, trx) + if err != nil { + return err + } + + acct, err := a.str.GetAccount(env.Id, trx) + if err != nil { + return err + } + + if err := sendLimitWarningEmail(a.cfg, acct.Email, limit, rxBytes, txBytes); err != nil { + return errors.Wrapf(err, "error sending limit warning email to '%v'", acct.Email) + } + return nil }