fleet/server/mail/mail.go
Lucas Manuel Rodriguez 2a532ede94
Do not return empty SSO and SMTP settings for non-global-admins (#12180)
#11266

PS: I first attempted a serialization trick by introducing a new
`appConfigResponse` and implementing `json.Marshal` to exclude these
fields but it was too hacky and hard to maintain moving forward, so I'm
bitting the bullet now. Happy to hear other ideas.

- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- ~[ ] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)~
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
  - ~For Orbit and Fleet Desktop changes:~
- ~[ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.~
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-06-07 16:06:36 -03:00

295 lines
7.4 KiB
Go

// Package mail provides implementations of the Fleet MailService
package mail
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"html/template"
"net"
"net/smtp"
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/bindata"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/fleet"
)
func NewService(config config.FleetConfig) (fleet.MailService, error) {
switch strings.ToLower(config.Email.EmailBackend) {
case "ses":
return NewSESSender(config.SES.Region, config.SES.EndpointURL, config.SES.AccessKeyID, config.SES.SecretAccessKey, config.SES.StsAssumeRoleArn, config.SES.SourceArn)
default:
return &mailService{}, nil
}
}
type mailService struct{}
type sender interface {
sendMail(e fleet.Email, msg []byte) error
}
func Test(mailer fleet.MailService, e fleet.Email) error {
mailBody, err := getMessageBody(e, getFrom)
if err != nil {
return fmt.Errorf("failed to get message body: %w", err)
}
svc, ok := mailer.(sender)
if !ok {
return nil
}
err = svc.sendMail(e, mailBody)
if err != nil {
return fmt.Errorf("sending mail: %w", err)
}
return nil
}
const (
PortSSL = 465
PortTLS = 587
)
type fromFunc func(e fleet.Email) (string, error)
func getMessageBody(e fleet.Email, f fromFunc) ([]byte, error) {
body, err := e.Mailer.Message()
if err != nil {
return nil, fmt.Errorf("get mailer message: %w", err)
}
mime := `MIME-version: 1.0;` + "\r\n"
content := `Content-Type: text/html; charset="UTF-8";` + "\r\n"
subject := "Subject: " + e.Subject + "\r\n"
from, err := f(e)
if err != nil {
return nil, fmt.Errorf("failed to obtain from address: %w", err)
}
msg := []byte(subject + from + mime + content + "\r\n" + string(body) + "\r\n")
return msg, nil
}
func getFrom(e fleet.Email) (string, error) {
return "From: " + e.SMTPSettings.SMTPSenderAddress + "\r\n", nil
}
func (m mailService) SendEmail(e fleet.Email) error {
if !e.SMTPSettings.SMTPConfigured {
return errors.New("email not configured")
}
msg, err := getMessageBody(e, getFrom)
if err != nil {
return err
}
return m.sendMail(e, msg)
}
type loginauth struct {
username string
password string
host string
}
func LoginAuth(username, password, host string) smtp.Auth {
return &loginauth{username: username, password: password, host: host}
}
func isLocalhost(name string) bool {
return name == "localhost" || name == "127.0.0.1" || name == "::1"
}
func (l *loginauth) Start(server *smtp.ServerInfo) (proto string, toServer []byte, err error) {
if !server.TLS && !isLocalhost(server.Name) {
return "", nil, errors.New("unencrypted connection")
}
if server.Name != l.host {
return "", nil, errors.New("wrong host name")
}
return "LOGIN", nil, nil
}
func (l *loginauth) Next(fromServer []byte, more bool) (toServer []byte, err error) {
if !more {
return nil, nil
}
prompt := strings.TrimSpace(string(fromServer))
switch prompt {
case "Username:":
return []byte(l.username), nil
case "Password:":
return []byte(l.password), nil
default:
return nil, errors.New("unexpected LOGIN prompt from server")
}
}
func smtpAuth(e fleet.Email) (smtp.Auth, error) {
if e.SMTPSettings.SMTPAuthenticationType != fleet.AuthTypeNameUserNamePassword {
return nil, nil
}
username := e.SMTPSettings.SMTPUserName
password := e.SMTPSettings.SMTPPassword
server := e.SMTPSettings.SMTPServer
authMethod := e.SMTPSettings.SMTPAuthenticationMethod
var auth smtp.Auth
switch authMethod {
case fleet.AuthMethodNameCramMD5:
auth = smtp.CRAMMD5Auth(username, password)
case fleet.AuthMethodNamePlain:
auth = smtp.PlainAuth("", username, password, server)
case fleet.AuthMethodNameLogin:
auth = LoginAuth(username, password, server)
default:
return nil, fmt.Errorf("unknown SMTP auth type '%s'", authMethod)
}
return auth, nil
}
func (m mailService) sendMail(e fleet.Email, msg []byte) error {
smtpHost := fmt.Sprintf(
"%s:%d", e.SMTPSettings.SMTPServer, e.SMTPSettings.SMTPPort)
auth, err := smtpAuth(e)
if err != nil {
return fmt.Errorf("failed to get smtp auth: %w", err)
}
if e.SMTPSettings.SMTPAuthenticationMethod == fleet.AuthMethodNameCramMD5 {
err = smtp.SendMail(smtpHost, auth, e.SMTPSettings.SMTPSenderAddress, e.To, msg)
if err != nil {
return fmt.Errorf("failed to send mail. crammd5 auth method: %w", err)
}
return nil
}
client, err := dialTimeout(smtpHost)
if err != nil {
return fmt.Errorf("could not dial smtp host: %w", err)
}
defer client.Close()
if e.SMTPSettings.SMTPEnableStartTLS {
if ok, _ := client.Extension("STARTTLS"); ok {
config := &tls.Config{
ServerName: e.SMTPSettings.SMTPServer,
InsecureSkipVerify: !e.SMTPSettings.SMTPVerifySSLCerts,
}
if err = client.StartTLS(config); err != nil {
return fmt.Errorf("startTLS error: %w", err)
}
}
}
if auth != nil {
if err = client.Auth(auth); err != nil {
return fmt.Errorf("client auth error: %w", err)
}
}
if err = client.Mail(e.SMTPSettings.SMTPSenderAddress); err != nil {
return fmt.Errorf("could not issue mail to provided address: %w", err)
}
for _, recip := range e.To {
if err = client.Rcpt(recip); err != nil {
return fmt.Errorf("failed to get recipient: %w", err)
}
}
writer, err := client.Data()
if err != nil {
return fmt.Errorf("getting client data: %w", err)
}
_, err = writer.Write(msg)
if err != nil {
return fmt.Errorf("failed to write: %w", err)
}
if err = writer.Close(); err != nil {
return fmt.Errorf("failed to close writer: %w", err)
}
if err := client.Quit(); err != nil {
return fmt.Errorf("error on client quit: %w", err)
}
return nil
}
// dialTimeout sets a timeout on net.Dial to prevent email from attempting to
// send indefinitely.
func dialTimeout(addr string) (client *smtp.Client, err error) {
// Ensure that errors are always returned after at least 5s to
// eliminate (some) timing attacks (in which a malicious user tries to
// port scan using the email functionality in Fleet)
c := time.After(30 * time.Second)
defer func() {
if err != nil {
// Wait until timer has elapsed to return anything
<-c
}
}()
conn, err := net.DialTimeout("tcp", addr, 28*time.Second)
if err != nil {
return nil, fmt.Errorf("dialing with timeout: %w", err)
}
host, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, fmt.Errorf("split host port: %w", err)
}
// Set a deadline to ensure we time out quickly when there is a TCP
// server listening but it's not an SMTP server (otherwise this seems
// to time out in 20s)
_ = conn.SetDeadline(time.Now().Add(28 * time.Second))
client, err = smtp.NewClient(conn, host)
if err != nil {
return nil, fmt.Errorf("SMTP connection error: %w", err)
}
// Clear deadlines
_ = conn.SetDeadline(time.Time{})
return client, nil
}
// SMTPTestMailer is used to build an email message that will be used as
// a test message when testing SMTP configuration
type SMTPTestMailer struct {
BaseURL template.URL
AssetURL template.URL
}
func (m *SMTPTestMailer) Message() ([]byte, error) {
t, err := getTemplate("server/mail/templates/smtp_setup.html")
if err != nil {
return nil, err
}
var msg bytes.Buffer
if err = t.Execute(&msg, m); err != nil {
return nil, err
}
return msg.Bytes(), nil
}
func getTemplate(templatePath string) (*template.Template, error) {
templateData, err := bindata.Asset(templatePath)
if err != nil {
return nil, err
}
t, err := template.New("email_template").Parse(string(templateData))
if err != nil {
return nil, err
}
return t, nil
}