[FEATURE] Notifier Startup Checks (#889)
* implement SMTP notifier startup check * check dial, starttls, auth, mail from, rcpt to, reset, and quit * log the error on failure * implement mock * misc optimizations, adjustments, and refactoring * implement validate_skip config option * fix comments to end with period * fix suites that used smtp notifier without a smtp container * add docs * add file notifier startup check * move file mode into const.go * disable gosec linting on insecureskipverify since it's intended, warned, and discouraged * minor PR commentary adjustment * apply suggestions from code review Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>pull/892/head
parent
a26ddf9c65
commit
9e9dee43ac
|
@ -8,10 +8,10 @@ on('pull_request.opened')
|
|||
context =>
|
||||
context.payload.pull_request.head.ref.slice(0, 11) !== 'dependabot/'
|
||||
)
|
||||
.comment(`# Artifacts
|
||||
.comment(`## Artifacts
|
||||
These changes are published for testing on Buildkite and DockerHub.
|
||||
|
||||
## Docker Container
|
||||
### Docker Container
|
||||
* \`docker pull authelia/authelia:{{ pull_request.head.ref }}\``)
|
||||
|
||||
// PR commentary for third party based contributions
|
||||
|
@ -21,10 +21,11 @@ on('pull_request.opened')
|
|||
context.payload.pull_request.head.label.slice(0, 9) !== 'authelia:'
|
||||
)
|
||||
.comment(`Thanks for choosing to contribute. We lint all PR's with golangci-lint, autheliabot may add a review to your PR with some suggestions.
|
||||
|
||||
You are free to apply the changes if you're comfortable, alternatively you are welcome to ask a team member for advice.
|
||||
|
||||
# Artifacts
|
||||
## Artifacts
|
||||
These changes once approved by a team member will be published for testing on Buildkite and DockerHub.
|
||||
|
||||
## Docker Container
|
||||
### Docker Container
|
||||
* \`docker pull authelia/authelia:PR{{ pull_request.number }}\``)
|
|
@ -89,6 +89,12 @@ func startServer() {
|
|||
} else {
|
||||
log.Fatalf("Unrecognized notifier")
|
||||
}
|
||||
if !config.Notifier.DisableStartupCheck {
|
||||
_, err := notifier.StartupCheck()
|
||||
if err != nil {
|
||||
log.Fatalf("Error during notifier startup check: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
clock := utils.RealClock{}
|
||||
authorizer := authorization.NewAuthorizer(config.AccessControl)
|
||||
|
|
|
@ -350,8 +350,11 @@ storage:
|
|||
#
|
||||
# Notifications are sent to users when they require a password reset, a u2f
|
||||
# registration or a TOTP registration.
|
||||
# Use only an available configuration: filesystem, gmail
|
||||
# Use only an available configuration: filesystem, smtp.
|
||||
notifier:
|
||||
# You can disable the notifier startup check by setting this to true.
|
||||
disable_startup_check: false
|
||||
|
||||
# For testing purpose, notifications can be sent in a file
|
||||
## filesystem:
|
||||
## filename: /tmp/authelia/notification.txt
|
||||
|
@ -377,9 +380,11 @@ notifier:
|
|||
# Subject configuration of the emails sent.
|
||||
# {title} is replaced by the text from the notifier
|
||||
subject: "[Authelia] {title}"
|
||||
# This address is used during the startup check to verify the email configuration is correct. It's not important what it is except if your email server only allows local delivery.
|
||||
## startup_check_address: test@authelia.com
|
||||
## trusted_cert: ""
|
||||
## disable_require_tls: false
|
||||
## disable_verify_cert: false
|
||||
## trusted_cert: ""
|
||||
|
||||
# Sending an email using a Gmail account is as simple as the next section.
|
||||
# You need to create an app password by following: https://support.google.com/accounts/answer/185833?hl=en
|
||||
|
|
|
@ -12,7 +12,16 @@ With this configuration, the message will be sent to a file. This option
|
|||
should only be used for testing purposes.
|
||||
|
||||
```yaml
|
||||
# Configuration of the notification system.
|
||||
#
|
||||
# Notifications are sent to users when they require a password reset, a U2F
|
||||
# registration or a TOTP registration.
|
||||
# Use only an available configuration: filesystem, smtp.
|
||||
notifier:
|
||||
# You can disable the notifier startup check by setting this to true.
|
||||
disable_startup_check: false
|
||||
|
||||
# For testing purpose, notifications can be sent in a file.
|
||||
filesystem:
|
||||
filename: /tmp/authelia/notification.txt
|
||||
```
|
|
@ -10,3 +10,20 @@ has_children: true
|
|||
|
||||
**Authelia** sometimes needs to send messages to users in order to
|
||||
verify their identity.
|
||||
|
||||
## Startup Check
|
||||
|
||||
The notifier has a startup check which validates the specified provider
|
||||
configuration is correct and will be able to send emails. This can be
|
||||
disabled with the `disable_startup_check` option:
|
||||
|
||||
```yaml
|
||||
# Configuration of the notification system.
|
||||
#
|
||||
# Notifications are sent to users when they require a password reset, a u2f
|
||||
# registration or a TOTP registration.
|
||||
# Use only an available configuration: filesystem, smtp.
|
||||
notifier:
|
||||
# You can disable the notifier startup check by setting this to true
|
||||
disable_startup_check: false
|
||||
```
|
||||
|
|
|
@ -16,9 +16,12 @@ It can be configured as described below.
|
|||
#
|
||||
# Notifications are sent to users when they require a password reset, a u2f
|
||||
# registration or a TOTP registration.
|
||||
# Use only an available configuration: filesystem, smtp
|
||||
# Use only an available configuration: filesystem, smtp.
|
||||
notifier:
|
||||
# For testing purpose, notifications can be sent in a file
|
||||
# You can disable the notifier startup check by setting this to true.
|
||||
disable_startup_check: false
|
||||
|
||||
# For testing purpose, notifications can be sent in a file.
|
||||
## filesystem:
|
||||
## filename: /tmp/authelia/notification.txt
|
||||
|
||||
|
@ -43,9 +46,11 @@ notifier:
|
|||
# Subject configuration of the emails sent.
|
||||
# {title} is replaced by the text from the notifier
|
||||
subject: "[Authelia] {title}"
|
||||
# This address is used during the startup check to verify the email configuration is correct. It's not important what it is except if your email server only allows local delivery.
|
||||
## startup_check_address: test@authelia.com
|
||||
## trusted_cert: ""
|
||||
## disable_require_tls: false
|
||||
## disable_verify_cert: false
|
||||
## trusted_cert: ""
|
||||
```
|
||||
|
||||
## Using Gmail
|
||||
|
|
|
@ -7,19 +7,21 @@ type FileSystemNotifierConfiguration struct {
|
|||
|
||||
// SMTPNotifierConfiguration represents the configuration of the SMTP server to send emails with.
|
||||
type SMTPNotifierConfiguration struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
Sender string `mapstructure:"sender"`
|
||||
Subject string `mapstructure:"subject"`
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
TrustedCert string `mapstructure:"trusted_cert"`
|
||||
StartupCheckAddress string `mapstructure:"startup_check_address"`
|
||||
DisableVerifyCert bool `mapstructure:"disable_verify_cert"`
|
||||
DisableRequireTLS bool `mapstructure:"disable_require_tls"`
|
||||
}
|
||||
|
||||
// NotifierConfiguration represents the configuration of the notifier to use when sending notifications to users.
|
||||
type NotifierConfiguration struct {
|
||||
DisableStartupCheck bool `mapstructure:"disable_startup_check"`
|
||||
FileSystem *FileSystemNotifierConfiguration `mapstructure:"filesystem"`
|
||||
SMTP *SMTPNotifierConfiguration `mapstructure:"smtp"`
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ func ValidateNotifier(configuration *schema.NotifierConfiguration, validator *sc
|
|||
}
|
||||
|
||||
if configuration.SMTP != nil {
|
||||
if configuration.SMTP.StartupCheckAddress == "" {
|
||||
configuration.SMTP.StartupCheckAddress = "test@authelia.com"
|
||||
}
|
||||
if configuration.SMTP.Host == "" {
|
||||
validator.Push(fmt.Errorf("Host of SMTP notifier must be provided"))
|
||||
}
|
||||
|
|
|
@ -10,30 +10,35 @@ import (
|
|||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockNotifier is a mock of Notifier interface
|
||||
// MockNotifier is a mock of Notifier interface.
|
||||
type MockNotifier struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockNotifierMockRecorder
|
||||
}
|
||||
|
||||
// MockNotifierMockRecorder is the mock recorder for MockNotifier
|
||||
// MockNotifierMockRecorder is the mock recorder for MockNotifier.
|
||||
type MockNotifierMockRecorder struct {
|
||||
mock *MockNotifier
|
||||
}
|
||||
|
||||
// NewMockNotifier creates a new mock instance
|
||||
// NewMockNotifier creates a new mock instance.
|
||||
func NewMockNotifier(ctrl *gomock.Controller) *MockNotifier {
|
||||
mock := &MockNotifier{ctrl: ctrl}
|
||||
mock.recorder = &MockNotifierMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockNotifier) EXPECT() *MockNotifierMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Send mocks base method
|
||||
// StartupCheck mocks base method.
|
||||
func (m *MockNotifier) StartupCheck() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Send mocks base method.
|
||||
func (m *MockNotifier) Send(arg0, arg1, arg2 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Send", arg0, arg1, arg2)
|
||||
|
@ -41,7 +46,7 @@ func (m *MockNotifier) Send(arg0, arg1, arg2 string) error {
|
|||
return ret0
|
||||
}
|
||||
|
||||
// Send indicates an expected call of Send
|
||||
// Send indicates an expected call of Send.
|
||||
func (mr *MockNotifierMockRecorder) Send(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockNotifier)(nil).Send), arg0, arg1, arg2)
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package notification
|
||||
|
||||
const fileNotifierMode = 0755
|
|
@ -3,6 +3,8 @@ package notification
|
|||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
|
@ -20,11 +22,44 @@ func NewFileNotifier(configuration schema.FileSystemNotifierConfiguration) *File
|
|||
}
|
||||
}
|
||||
|
||||
// StartupCheck checks the file provider can write to the specified file
|
||||
func (n *FileNotifier) StartupCheck() (ok bool, err error) {
|
||||
ok = true
|
||||
dir := filepath.Dir(n.path)
|
||||
if _, err = os.Stat(dir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(dir, fileNotifierMode); err != nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
if err = ioutil.WriteFile(n.path, []byte(""), fileNotifierMode); err != nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
} else if _, err = os.Stat(n.path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err = ioutil.WriteFile(n.path, []byte(""), fileNotifierMode); err != nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
}
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Send send a identity verification link to a user.
|
||||
func (n *FileNotifier) Send(recipient, subject, body string) error {
|
||||
content := fmt.Sprintf("Date: %s\nRecipient: %s\nSubject: %s\nBody: %s", time.Now(), recipient, subject, body)
|
||||
|
||||
err := ioutil.WriteFile(n.path, []byte(content), 0755)
|
||||
err := ioutil.WriteFile(n.path, []byte(content), fileNotifierMode)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -3,4 +3,5 @@ package notification
|
|||
// Notifier interface for sending the identity verification link.
|
||||
type Notifier interface {
|
||||
Send(recipient, subject, body string) error
|
||||
StartupCheck() (bool, error)
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
"github.com/authelia/authelia/internal/utils"
|
||||
)
|
||||
|
||||
// SMTPNotifier a notifier to send emails to SMTP servers
|
||||
// SMTPNotifier a notifier to send emails to SMTP servers.
|
||||
type SMTPNotifier struct {
|
||||
username string
|
||||
password string
|
||||
|
@ -27,11 +27,12 @@ type SMTPNotifier struct {
|
|||
disableRequireTLS bool
|
||||
address string
|
||||
subject string
|
||||
startupCheckAddress string
|
||||
client *smtp.Client
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
// NewSMTPNotifier create an SMTPNotifier targeting a given address
|
||||
// NewSMTPNotifier creates a SMTPNotifier using the notifier configuration.
|
||||
func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration) *SMTPNotifier {
|
||||
notifier := &SMTPNotifier{
|
||||
username: configuration.Username,
|
||||
|
@ -44,6 +45,7 @@ func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration) *SMTPNotifi
|
|||
disableRequireTLS: configuration.DisableRequireTLS,
|
||||
address: fmt.Sprintf("%s:%d", configuration.Host, configuration.Port),
|
||||
subject: configuration.Subject,
|
||||
startupCheckAddress: configuration.StartupCheckAddress,
|
||||
}
|
||||
notifier.initializeTLSConfig()
|
||||
return notifier
|
||||
|
@ -53,10 +55,6 @@ func (n *SMTPNotifier) initializeTLSConfig() {
|
|||
// Do not allow users to disable verification of certs if they have also set a trusted cert that was loaded
|
||||
// The second part of this check happens in the Configure Cert Pool code block
|
||||
log.Debug("Notifier SMTP client initializing TLS configuration")
|
||||
insecureSkipVerify := false
|
||||
if n.disableVerifyCert {
|
||||
insecureSkipVerify = true
|
||||
}
|
||||
|
||||
//Configure Cert Pool
|
||||
certPool, err := x509.SystemCertPool()
|
||||
|
@ -77,7 +75,7 @@ func (n *SMTPNotifier) initializeTLSConfig() {
|
|||
log.Debug("Notifier SMTP successfully loaded certificate")
|
||||
if n.disableVerifyCert {
|
||||
log.Warn("Notifier SMTP when trusted_cert is specified we force disable_verify_cert to false, if you want to disable certificate validation please comment/delete trusted_cert from your config")
|
||||
insecureSkipVerify = false
|
||||
n.disableVerifyCert = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,13 +84,13 @@ func (n *SMTPNotifier) initializeTLSConfig() {
|
|||
}
|
||||
}
|
||||
n.tlsConfig = &tls.Config{
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
InsecureSkipVerify: n.disableVerifyCert, //nolint:gosec // This is an intended config, we never default true, provide alternate options, and we constantly warn the user.
|
||||
ServerName: n.host,
|
||||
RootCAs: certPool,
|
||||
}
|
||||
}
|
||||
|
||||
// Do startTLS if available (some servers only provide the auth extension after, and encryption is preferred)
|
||||
// Do startTLS if available (some servers only provide the auth extension after, and encryption is preferred).
|
||||
func (n *SMTPNotifier) startTLS() (bool, error) {
|
||||
// Only start if not already encrypted
|
||||
if _, ok := n.client.TLSConnectionState(); ok {
|
||||
|
@ -117,23 +115,23 @@ func (n *SMTPNotifier) startTLS() (bool, error) {
|
|||
return ok, nil
|
||||
}
|
||||
|
||||
// Attempt Authentication
|
||||
// Attempt Authentication.
|
||||
func (n *SMTPNotifier) auth() (bool, error) {
|
||||
// Attempt AUTH if password is specified only
|
||||
// Attempt AUTH if password is specified only.
|
||||
if n.password != "" {
|
||||
_, ok := n.client.TLSConnectionState()
|
||||
if !ok {
|
||||
return false, errors.New("Notifier SMTP client does not support authentication over plain text and the connection is currently plain text")
|
||||
}
|
||||
|
||||
// Check the server supports AUTH, and get the mechanisms
|
||||
// Check the server supports AUTH, and get the mechanisms.
|
||||
ok, m := n.client.Extension("AUTH")
|
||||
if ok {
|
||||
log.Debugf("Notifier SMTP server supports authentication with the following mechanisms: %s", m)
|
||||
mechanisms := strings.Split(m, " ")
|
||||
var auth smtp.Auth
|
||||
|
||||
// Adaptively select the AUTH mechanism to use based on what the server advertised
|
||||
// Adaptively select the AUTH mechanism to use based on what the server advertised.
|
||||
if utils.IsStringInSlice("PLAIN", mechanisms) {
|
||||
auth = smtp.PlainAuth("", n.username, n.password, n.host)
|
||||
log.Debug("Notifier SMTP client attempting AUTH PLAIN with server")
|
||||
|
@ -142,12 +140,12 @@ func (n *SMTPNotifier) auth() (bool, error) {
|
|||
log.Debug("Notifier SMTP client attempting AUTH LOGIN with server")
|
||||
}
|
||||
|
||||
// Throw error since AUTH extension is not supported
|
||||
// Throw error since AUTH extension is not supported.
|
||||
if auth == nil {
|
||||
return false, fmt.Errorf("notifier SMTP server does not advertise a AUTH mechanism that are supported by Authelia (PLAIN or LOGIN are supported, but server advertised %s mechanisms)", m)
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
// Authenticate.
|
||||
err := n.client.Auth(auth)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -195,7 +193,7 @@ func (n *SMTPNotifier) compose(recipient, subject, body string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Dial the SMTP server with the SMTPNotifier config
|
||||
// Dial the SMTP server with the SMTPNotifier config.
|
||||
func (n *SMTPNotifier) dial() error {
|
||||
log.Debugf("Notifier SMTP client attempting connection to %s", n.address)
|
||||
if n.port == 465 {
|
||||
|
@ -220,7 +218,7 @@ func (n *SMTPNotifier) dial() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Closes the connection properly
|
||||
// Closes the connection properly.
|
||||
func (n *SMTPNotifier) cleanup() {
|
||||
err := n.client.Quit()
|
||||
if err != nil {
|
||||
|
@ -228,17 +226,56 @@ func (n *SMTPNotifier) cleanup() {
|
|||
}
|
||||
}
|
||||
|
||||
// Send an email
|
||||
// StartupCheck checks the server is functioning correctly and the configuration is correct.
|
||||
func (n *SMTPNotifier) StartupCheck() (ok bool, err error) {
|
||||
ok = true
|
||||
|
||||
if err = n.dial(); err != nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
defer n.cleanup()
|
||||
|
||||
if _, err = n.startTLS(); err != nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = n.auth(); err != nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
if err = n.client.Mail(n.sender); err != nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
if err = n.client.Rcpt(n.startupCheckAddress); err != nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
if err = n.client.Reset(); err != nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Send is used to send an email to a recipient.
|
||||
func (n *SMTPNotifier) Send(recipient, title, body string) error {
|
||||
subject := strings.ReplaceAll(n.subject, "{title}", title)
|
||||
if err := n.dial(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Always execute QUIT at the end once we're connected
|
||||
// Always execute QUIT at the end once we're connected.
|
||||
defer n.cleanup()
|
||||
|
||||
// Start TLS and then Authenticate
|
||||
// Start TLS and then Authenticate.
|
||||
if _, err := n.startTLS(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -246,7 +283,7 @@ func (n *SMTPNotifier) Send(recipient, title, body string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Set the sender and recipient first
|
||||
// Set the sender and recipient first.
|
||||
if err := n.client.Mail(n.sender); err != nil {
|
||||
log.Debugf("Notifier SMTP failed while sending MAIL FROM (using sender) with error: %s", err)
|
||||
return err
|
||||
|
@ -256,7 +293,7 @@ func (n *SMTPNotifier) Send(recipient, title, body string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Compose and send the email body to the server
|
||||
// Compose and send the email body to the server.
|
||||
if err := n.compose(recipient, subject, body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -97,9 +97,5 @@ regulation:
|
|||
ban_time: 900
|
||||
|
||||
notifier:
|
||||
# Use a SMTP server for sending notifications
|
||||
smtp:
|
||||
host: smtp
|
||||
port: 1025
|
||||
sender: admin@example.com
|
||||
disable_require_tls: true
|
||||
filesystem:
|
||||
filename: /tmp/notifier.html
|
|
@ -40,8 +40,5 @@ access_control:
|
|||
policy: bypass
|
||||
|
||||
notifier:
|
||||
smtp:
|
||||
host: smtp
|
||||
port: 1025
|
||||
sender: admin@example.com
|
||||
disable_require_tls: true
|
||||
filesystem:
|
||||
filename: /tmp/notifier.html
|
Loading…
Reference in New Issue