Fix SMTP e-mail send when SMTP server has credentials (#10758)

#9609

This PR also fixes #10777.

The issue is: We were using `svc.AppConfig` instead of
`svc.ds.AppConfig` to retrieve the SMTP credentials.
`svc.AppConfig` obfuscates credentials, whereas `svc.ds.AppConfig` does
not.
To help prevent this from happening again I've renamed `svc.AppConfig`
to `svc.AppConfigObfuscated`.
I've also added a new test SMTP server
(https://github.com/axllent/mailpit) that supports Basic Authentication
and tests that make use of it to catch these kind of bugs (the tests are
executed when running `go test` with `MAIL_TEST=1`).

- [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)).~
This commit is contained in:
Lucas Manuel Rodriguez 2023-03-28 15:23:15 -03:00 committed by GitHub
parent 477bb53f90
commit 40265d0e6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 293 additions and 58 deletions

View File

@ -60,7 +60,7 @@ jobs:
# Pre-starting dependencies here means they are ready to go when we need them. # Pre-starting dependencies here means they are ready to go when we need them.
- name: Start Infra Dependencies - name: Start Infra Dependencies
# Use & to background this # Use & to background this
run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker-compose up -d mysql_test redis redis-cluster-1 redis-cluster-2 redis-cluster-3 redis-cluster-4 redis-cluster-5 redis-cluster-6 redis-cluster-setup minio saml_idp & run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker-compose up -d mysql_test redis redis-cluster-1 redis-cluster-2 redis-cluster-3 redis-cluster-4 redis-cluster-5 redis-cluster-6 redis-cluster-setup minio saml_idp mailhog mailpit &
# It seems faster not to cache Go dependencies # It seems faster not to cache Go dependencies
- name: Install Go Dependencies - name: Install Go Dependencies
@ -95,6 +95,7 @@ jobs:
MYSQL_TEST=1 \ MYSQL_TEST=1 \
MINIO_STORAGE_TEST=1 \ MINIO_STORAGE_TEST=1 \
SAML_IDP_TEST=1 \ SAML_IDP_TEST=1 \
MAIL_TEST=1 \
NETWORK_TEST_GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \ NETWORK_TEST_GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \
make test-go 2>&1 | tee /tmp/gotest.log make test-go 2>&1 | tee /tmp/gotest.log

View File

@ -0,0 +1 @@
* Fix e-mail sending on user invites and user e-mail change when SMTP server has credentials.

View File

@ -55,12 +55,27 @@ services:
- /var/lib/mysql:rw,noexec,nosuid - /var/lib/mysql:rw,noexec,nosuid
- /tmpfs - /tmpfs
# Unauthenticated SMTP server.
mailhog: mailhog:
image: mailhog/mailhog:latest image: mailhog/mailhog:latest
ports: ports:
- "8025:8025" - "8025:8025"
- "1025:1025" - "1025:1025"
# SMTP server with Basic Authentication.
mailpit:
image: axllent/mailpit:latest
ports:
- "8026:8025"
- "1026:1025"
volumes:
- ./tools/mailpit/auth.txt:/auth.txt
command:
[
"--smtp-auth-file=/auth.txt",
"--smtp-auth-allow-insecure=true"
]
redis: redis:
image: redis:5 image: redis:5
ports: ports:

View File

@ -19,7 +19,9 @@
- [Command line](#command-line) - [Command line](#command-line)
- [Test hosts](#test-hosts) - [Test hosts](#test-hosts)
- [Email](#email) - [Email](#email)
- [Manually testing email with MailHog](#manually-testing-email-with-mailhog) - [Manually testing email with MailHog and Mailpit](#manually-testing-email-with-mailhog-and-mailpit)
- [MailHog SMTP server without authentication](#mailhog-smtp-server-without-authentication)
- [Mailpit SMTP server with plain authentication](#mailpit-smtp-server-with-plain-authentication)
- [Development database management](#development-database-management) - [Development database management](#development-database-management)
- [MySQL shell](#mysql-shell) - [MySQL shell](#mysql-shell)
- [Redis REPL](#redis-repl) - [Redis REPL](#redis-repl)
@ -30,8 +32,7 @@
- [Telemetry](#telemetry) - [Telemetry](#telemetry)
- [MDM setup and testing](#mdm-setup-and-testing) - [MDM setup and testing](#mdm-setup-and-testing)
- [ABM setup](#abm-setup) - [ABM setup](#abm-setup)
- [Private key + certificate](#private-key--certificate) - [Private key, certificate, and encrypted token](#private-key-certificate-and-encrypted-token)
- [Encrypted token](#encrypted-token)
- [APNs and SCEP setup](#apns-and-scep-setup) - [APNs and SCEP setup](#apns-and-scep-setup)
- [Running the server](#running-the-server) - [Running the server](#running-the-server)
- [Testing MDM](#testing-mdm) - [Testing MDM](#testing-mdm)
@ -232,7 +233,9 @@ The Fleet repo includes tools to start testing osquery hosts. Please see the doc
## Email ## Email
### Manually testing email with MailHog ### Manually testing email with MailHog and Mailpit
#### MailHog SMTP server without authentication
To intercept sent emails while running a Fleet development environment, first, as an Admin in the Fleet UI, navigate to the Organization settings. To intercept sent emails while running a Fleet development environment, first, as an Admin in the Fleet UI, navigate to the Organization settings.
@ -240,6 +243,17 @@ Then, in the "SMTP options" section, enter any email address in the "Sender addr
Visit [localhost:8025](http://localhost:8025) to view MailHog's admin interface displaying all emails sent using the simulated mail server. Visit [localhost:8025](http://localhost:8025) to view MailHog's admin interface displaying all emails sent using the simulated mail server.
#### Mailpit SMTP server with plain authentication
Alternatively, if you need to test a SMTP server with plain basic authentication enabled, set:
- "SMTP server" to `localhost` on port `1026`
- "Authentication type" to `Plain`.
- "SMTP username" to `mailpit-username`.
- "SMTP password" to `mailpit-password`.
- Note that you may use any active or inactive sender address.
Visit [localhost:8026](http://localhost:8026) to view Mailpit's admin interface displaying all emails sent using the simulated mail server.
## Development database management ## Development database management
In the course of development (particularly when crafting database migrations), it may be useful to In the course of development (particularly when crafting database migrations), it may be useful to
@ -574,3 +588,5 @@ Reference the [Apple DEP Profile documentation](https://developer.apple.com/docu
2. In ABM, look for the computer with the serial number that matches the one your VM has, click on it and click on "Edit MDM Server" to assign that computer to your MDM server. 2. In ABM, look for the computer with the serial number that matches the one your VM has, click on it and click on "Edit MDM Server" to assign that computer to your MDM server.
3. Boot the machine, it should automatically enroll into MDM. 3. Boot the machine, it should automatically enroll into MDM.
<meta name="pageOrderInSection" value="1500">

View File

@ -27,7 +27,7 @@ func (svc *Service) GetAppleBM(ctx context.Context) (*fleet.AppleBM, error) {
return nil, notFoundError{} return nil, notFoundError{}
} }
appCfg, err := svc.AppConfig(ctx) appCfg, err := svc.AppConfigObfuscated(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -507,7 +507,7 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec,
} }
} }
appConfig, err := svc.AppConfig(ctx) appConfig, err := svc.AppConfigObfuscated(ctx)
if err != nil { if err != nil {
return err return err
} }

View File

@ -351,7 +351,8 @@ type Service interface {
// AppConfigService provides methods for configuring the Fleet application // AppConfigService provides methods for configuring the Fleet application
NewAppConfig(ctx context.Context, p AppConfig) (info *AppConfig, err error) NewAppConfig(ctx context.Context, p AppConfig) (info *AppConfig, err error)
AppConfig(ctx context.Context) (info *AppConfig, err error) // AppConfigObfuscated returns the global application config with obfuscated credentials.
AppConfigObfuscated(ctx context.Context) (info *AppConfig, err error)
ModifyAppConfig(ctx context.Context, p []byte, applyOpts ApplySpecOptions) (info *AppConfig, err error) ModifyAppConfig(ctx context.Context, p []byte, applyOpts ApplySpecOptions) (info *AppConfig, err error)
SandboxEnabled() bool SandboxEnabled() bool

View File

@ -10,30 +10,23 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type mockMailer struct{}
func (m *mockMailer) SendEmail(e fleet.Email) error {
return nil
}
func getMailer() fleet.MailService {
if os.Getenv("MAIL_TEST") == "" {
return &mockMailer{}
}
return NewService()
}
var testFunctions = [...]func(*testing.T, fleet.MailService){ var testFunctions = [...]func(*testing.T, fleet.MailService){
testSMTPPlainAuth, testSMTPPlainAuth,
testSMTPPlainAuthInvalidCreds,
testSMTPSkipVerify, testSMTPSkipVerify,
testSMTPNoAuth, testSMTPNoAuth,
testMailTest, testMailTest,
} }
func TestMail(t *testing.T) { func TestMail(t *testing.T) {
// This mail test requires mailhog unauthenticated running on localhost:1025
// and mailpit running on localhost:1026.
if _, ok := os.LookupEnv("MAIL_TEST"); !ok {
t.Skip("Mail tests are disabled")
}
for _, f := range testFunctions { for _, f := range testFunctions {
r := getMailer() r := NewService()
t.Run(test.FunctionName(f), func(t *testing.T) { t.Run(test.FunctionName(f), func(t *testing.T) {
f(t, r) f(t, r)
@ -50,12 +43,12 @@ func testSMTPPlainAuth(t *testing.T, mailer fleet.MailService) {
SMTPConfigured: true, SMTPConfigured: true,
SMTPAuthenticationType: fleet.AuthTypeNameUserNamePassword, SMTPAuthenticationType: fleet.AuthTypeNameUserNamePassword,
SMTPAuthenticationMethod: fleet.AuthMethodNamePlain, SMTPAuthenticationMethod: fleet.AuthMethodNamePlain,
SMTPUserName: "bob", SMTPUserName: "mailpit-username",
SMTPPassword: "secret", SMTPPassword: "mailpit-password",
SMTPEnableTLS: true, SMTPEnableTLS: true,
SMTPVerifySSLCerts: true, SMTPVerifySSLCerts: true,
SMTPEnableStartTLS: true, SMTPEnableStartTLS: true,
SMTPPort: 1025, SMTPPort: 1026,
SMTPServer: "localhost", SMTPServer: "localhost",
SMTPSenderAddress: "test@example.com", SMTPSenderAddress: "test@example.com",
}, },
@ -69,6 +62,34 @@ func testSMTPPlainAuth(t *testing.T, mailer fleet.MailService) {
assert.Nil(t, err) assert.Nil(t, err)
} }
func testSMTPPlainAuthInvalidCreds(t *testing.T, mailer fleet.MailService) {
mail := fleet.Email{
Subject: "smtp plain auth with invalid credentials",
To: []string{"john@fleet.co"},
Config: &fleet.AppConfig{
SMTPSettings: fleet.SMTPSettings{
SMTPConfigured: true,
SMTPAuthenticationType: fleet.AuthTypeNameUserNamePassword,
SMTPAuthenticationMethod: fleet.AuthMethodNamePlain,
SMTPUserName: "mailpit-username",
SMTPPassword: "wrong",
SMTPEnableTLS: true,
SMTPVerifySSLCerts: true,
SMTPEnableStartTLS: true,
SMTPPort: 1026,
SMTPServer: "localhost",
SMTPSenderAddress: "test@example.com",
},
},
Mailer: &SMTPTestMailer{
BaseURL: "https://localhost:8080",
},
}
err := mailer.SendEmail(mail)
assert.Error(t, err)
}
func testSMTPSkipVerify(t *testing.T, mailer fleet.MailService) { func testSMTPSkipVerify(t *testing.T, mailer fleet.MailService) {
mail := fleet.Email{ mail := fleet.Email{
Subject: "skip verify", Subject: "skip verify",
@ -78,8 +99,8 @@ func testSMTPSkipVerify(t *testing.T, mailer fleet.MailService) {
SMTPConfigured: true, SMTPConfigured: true,
SMTPAuthenticationType: fleet.AuthTypeNameUserNamePassword, SMTPAuthenticationType: fleet.AuthTypeNameUserNamePassword,
SMTPAuthenticationMethod: fleet.AuthMethodNamePlain, SMTPAuthenticationMethod: fleet.AuthMethodNamePlain,
SMTPUserName: "bob", SMTPUserName: "mailpit-username",
SMTPPassword: "secret", SMTPPassword: "mailpit-password",
SMTPEnableTLS: true, SMTPEnableTLS: true,
SMTPVerifySSLCerts: false, SMTPVerifySSLCerts: false,
SMTPEnableStartTLS: true, SMTPEnableStartTLS: true,
@ -127,13 +148,16 @@ func testMailTest(t *testing.T, mailer fleet.MailService) {
To: []string{"bob@foo.com"}, To: []string{"bob@foo.com"},
Config: &fleet.AppConfig{ Config: &fleet.AppConfig{
SMTPSettings: fleet.SMTPSettings{ SMTPSettings: fleet.SMTPSettings{
SMTPConfigured: true, SMTPConfigured: true,
SMTPAuthenticationType: fleet.AuthTypeNameNone, SMTPAuthenticationType: fleet.AuthTypeNameUserNamePassword,
SMTPEnableTLS: true, SMTPAuthenticationMethod: fleet.AuthMethodNamePlain,
SMTPVerifySSLCerts: true, SMTPUserName: "mailpit-username",
SMTPPort: 1025, SMTPPassword: "mailpit-password",
SMTPServer: "localhost", SMTPEnableTLS: true,
SMTPSenderAddress: "test@example.com", SMTPVerifySSLCerts: true,
SMTPPort: 1026,
SMTPServer: "localhost",
SMTPSenderAddress: "test@example.com",
}, },
}, },
Mailer: &SMTPTestMailer{ Mailer: &SMTPTestMailer{
@ -142,7 +166,6 @@ func testMailTest(t *testing.T, mailer fleet.MailService) {
} }
err := Test(mailer, mail) err := Test(mailer, mail)
assert.Nil(t, err) assert.Nil(t, err)
} }
func TestTemplateProcessor(t *testing.T) { func TestTemplateProcessor(t *testing.T) {

View File

@ -70,7 +70,7 @@ func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Se
if !ok { if !ok {
return nil, errors.New("could not fetch user") return nil, errors.New("could not fetch user")
} }
config, err := svc.AppConfig(ctx) config, err := svc.AppConfigObfuscated(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -144,7 +144,7 @@ func (svc *Service) SandboxEnabled() bool {
return svc.config.Server.SandboxEnabled return svc.config.Server.SandboxEnabled
} }
func (svc *Service) AppConfig(ctx context.Context) (*fleet.AppConfig, error) { func (svc *Service) AppConfigObfuscated(ctx context.Context) (*fleet.AppConfig, error) {
if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) { if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) {
if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil { if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil {
return nil, err return nil, err
@ -340,7 +340,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
return nil, legacyUsedWarning return nil, legacyUsedWarning
} }
// must reload to get the unchanged app config // must reload to get the unchanged app config
return svc.AppConfig(ctx) return svc.AppConfigObfuscated(ctx)
} }
// ignore the values for SMTPEnabled and SMTPConfigured // ignore the values for SMTPEnabled and SMTPConfigured
@ -398,7 +398,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
} }
// retrieve new app config with obfuscated secrets // retrieve new app config with obfuscated secrets
obfuscatedConfig, err := svc.AppConfig(ctx) obfuscatedConfig, err := svc.AppConfigObfuscated(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -667,7 +667,7 @@ func getCertificateEndpoint(ctx context.Context, request interface{}, svc fleet.
// Certificate returns the PEM encoded certificate chain for osqueryd TLS termination. // Certificate returns the PEM encoded certificate chain for osqueryd TLS termination.
func (svc *Service) CertificateChain(ctx context.Context) ([]byte, error) { func (svc *Service) CertificateChain(ctx context.Context) ([]byte, error) {
config, err := svc.AppConfig(ctx) config, err := svc.AppConfigObfuscated(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -98,7 +98,7 @@ func TestAppConfigAuth(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user}) ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
_, err := svc.AppConfig(ctx) _, err := svc.AppConfigObfuscated(ctx)
checkAuthErr(t, tt.shouldFailRead, err) checkAuthErr(t, tt.shouldFailRead, err)
_, err = svc.ModifyAppConfig(ctx, []byte(`{}`), fleet.ApplySpecOptions{}) _, err = svc.ModifyAppConfig(ctx, []byte(`{}`), fleet.ApplySpecOptions{})
@ -423,7 +423,7 @@ func TestAppConfigSecretsObfuscated(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user}) ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
ac, err := svc.AppConfig(ctx) ac, err := svc.AppConfigObfuscated(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, ac.SMTPSettings.SMTPPassword, fleet.MaskedPassword) require.Equal(t, ac.SMTPSettings.SMTPPassword, fleet.MaskedPassword)
require.Equal(t, ac.Integrations.Jira[0].APIToken, fleet.MaskedPassword) require.Equal(t, ac.Integrations.Jira[0].APIToken, fleet.MaskedPassword)
@ -564,7 +564,7 @@ func TestTransparencyURL(t *testing.T) {
return nil return nil
} }
ac, err := svc.AppConfig(ctx) ac, err := svc.AppConfigObfuscated(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tt.initialURL, ac.FleetDesktop.TransparencyURL) require.Equal(t, tt.initialURL, ac.FleetDesktop.TransparencyURL)
@ -576,7 +576,7 @@ func TestTransparencyURL(t *testing.T) {
if modified != nil { if modified != nil {
require.Equal(t, tt.expectedURL, modified.FleetDesktop.TransparencyURL) require.Equal(t, tt.expectedURL, modified.FleetDesktop.TransparencyURL)
ac, err = svc.AppConfig(ctx) ac, err = svc.AppConfigObfuscated(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tt.expectedURL, ac.FleetDesktop.TransparencyURL) require.Equal(t, tt.expectedURL, ac.FleetDesktop.TransparencyURL)
} }
@ -613,7 +613,7 @@ func TestTransparencyURLDowngradeLicense(t *testing.T) {
return nil return nil
} }
ac, err := svc.AppConfig(ctx) ac, err := svc.AppConfigObfuscated(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "https://example.com/transparency", ac.FleetDesktop.TransparencyURL) require.Equal(t, "https://example.com/transparency", ac.FleetDesktop.TransparencyURL)
@ -633,7 +633,7 @@ func TestTransparencyURLDowngradeLicense(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, modified) require.NotNil(t, modified)
require.Equal(t, "", modified.FleetDesktop.TransparencyURL) require.Equal(t, "", modified.FleetDesktop.TransparencyURL)
ac, err = svc.AppConfig(ctx) ac, err = svc.AppConfigObfuscated(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "f1337", ac.OrgInfo.OrgName) require.Equal(t, "f1337", ac.OrgInfo.OrgName)
require.Equal(t, "", ac.FleetDesktop.TransparencyURL) require.Equal(t, "", ac.FleetDesktop.TransparencyURL)
@ -716,7 +716,7 @@ func TestService_ModifyAppConfig_MDM(t *testing.T) {
return nil, errors.New(notFoundErr) return nil, errors.New(notFoundErr)
} }
ac, err := svc.AppConfig(ctx) ac, err := svc.AppConfigObfuscated(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tt.oldMDM, ac.MDM) require.Equal(t, tt.oldMDM, ac.MDM)
@ -731,7 +731,7 @@ func TestService_ModifyAppConfig_MDM(t *testing.T) {
} }
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tt.expectedMDM, modified.MDM) require.Equal(t, tt.expectedMDM, modified.MDM)
ac, err = svc.AppConfig(ctx) ac, err = svc.AppConfigObfuscated(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tt.expectedMDM, ac.MDM) require.Equal(t, tt.expectedMDM, ac.MDM)
}) })

View File

@ -1470,7 +1470,7 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm
return ctxerr.Wrap(ctx, err) return ctxerr.Wrap(ctx, err)
} }
appCfg, err := svc.AppConfig(ctx) appCfg, err := svc.AppConfigObfuscated(ctx)
if err != nil { if err != nil {
return ctxerr.Wrap(ctx, err) return ctxerr.Wrap(ctx, err)
} }
@ -1600,7 +1600,7 @@ func (svc *Service) UpdateMDMAppleSettings(ctx context.Context, payload fleet.MD
} }
func (svc *Service) updateAppConfigMDMAppleSettings(ctx context.Context, payload fleet.MDMAppleSettingsPayload) error { func (svc *Service) updateAppConfigMDMAppleSettings(ctx context.Context, payload fleet.MDMAppleSettingsPayload) error {
ac, err := svc.AppConfig(ctx) ac, err := svc.AppConfigObfuscated(ctx)
if err != nil { if err != nil {
return err return err
} }

View File

@ -127,7 +127,7 @@ func getDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.S
// the org logo URL config is required by the frontend to render the page; // the org logo URL config is required by the frontend to render the page;
// we need to be careful with what we return from AppConfig in the response // we need to be careful with what we return from AppConfig in the response
// as this is a weakly authenticated endpoint (with the device auth token). // as this is a weakly authenticated endpoint (with the device auth token).
ac, err := svc.AppConfig(ctx) ac, err := svc.AppConfigObfuscated(ctx)
if err != nil { if err != nil {
return getDeviceHostResponse{Err: err}, nil return getDeviceHostResponse{Err: err}, nil
} }
@ -329,7 +329,7 @@ func (r transparencyURLResponse) hijackRender(ctx context.Context, w http.Respon
func (r transparencyURLResponse) error() error { return r.Err } func (r transparencyURLResponse) error() error { return r.Err }
func transparencyURL(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { func transparencyURL(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
config, err := svc.AppConfig(ctx) config, err := svc.AppConfigObfuscated(ctx)
if err != nil { if err != nil {
return transparencyURLResponse{Err: err}, nil return transparencyURLResponse{Err: err}, nil
} }

View File

@ -95,7 +95,7 @@ func (svc *Service) InviteNewUser(ctx context.Context, payload fleet.InvitePaylo
return nil, err return nil, err
} }
config, err := svc.AppConfig(ctx) config, err := svc.ds.AppConfig(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }

171
server/service/mail_test.go Normal file
View File

@ -0,0 +1,171 @@
package service
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/stretchr/testify/require"
"gopkg.in/guregu/null.v3"
)
type notTestFoundError struct{}
func (e *notTestFoundError) Error() string {
return "not found"
}
func (e *notTestFoundError) IsNotFound() bool {
return true
}
func newTestNotFoundError() *notTestFoundError {
return &notTestFoundError{}
}
// Is is implemented so that errors.Is(err, sql.ErrNoRows) returns true for an
// error of type *notFoundError, without having to wrap sql.ErrNoRows
// explicitly.
func (e *notTestFoundError) Is(other error) bool {
return other == sql.ErrNoRows
}
func TestMailService(t *testing.T) {
// This mail test requires mailpit running on localhost:1026.
if _, ok := os.LookupEnv("MAIL_TEST"); !ok {
t.Skip("Mail tests are disabled")
}
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{
UseMailService: true,
})
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
SMTPSettings: fleet.SMTPSettings{
SMTPEnabled: true,
SMTPConfigured: true,
SMTPAuthenticationType: fleet.AuthTypeNameUserNamePassword,
SMTPAuthenticationMethod: fleet.AuthMethodNamePlain,
SMTPUserName: "mailpit-username",
SMTPPassword: "mailpit-password",
SMTPEnableTLS: true,
SMTPVerifySSLCerts: true,
SMTPPort: 1026,
SMTPServer: "localhost",
SMTPSenderAddress: "foobar@example.com",
},
}, nil
}
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
return nil, newTestNotFoundError()
}
var invite *fleet.Invite
ds.NewInviteFunc = func(ctx context.Context, i *fleet.Invite) (*fleet.Invite, error) {
invite = i
return invite, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) error {
return nil
}
ds.InviteFunc = func(ctx context.Context, id uint) (*fleet.Invite, error) {
return invite, nil
}
ctx = test.UserContext(ctx, test.UserAdmin)
// (1) Modifying the app config `sender_address` field to trigger a test e-mail send.
_, err := svc.ModifyAppConfig(ctx, []byte(`{
"org_info": {
"org_name": "Acme"
},
"server_settings": {
"server_url": "http://someurl"
},
"smtp_settings": {
"enable_smtp": true,
"configured": true,
"authentication_type": "authtype_username_password",
"authentication_method": "authmethod_plain",
"user_name": "mailpit-username",
"password": "mailpit-password",
"enable_ssl_tls": true,
"verify_ssl_certs": true,
"port": 1026,
"server": "127.0.0.1",
"sender_address": "foobar_updated@example.com"
}
}`), fleet.ApplySpecOptions{})
require.NoError(t, err)
getLastMailPitMessage := func() map[string]interface{} {
resp, err := http.Get("http://localhost:8026/api/v1/messages?limit=1")
require.NoError(t, err)
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var m map[string]interface{}
err = json.Unmarshal(b, &m)
require.NoError(t, err)
require.NotNil(t, m["messages"])
require.Len(t, m["messages"], 1)
lm := (m["messages"]).([]interface{})[0]
require.NotNil(t, lm)
lastMessage := lm.(map[string]interface{})
fmt.Printf("%+v\n", lastMessage)
return lastMessage
}
lastMessage := getLastMailPitMessage()
require.Equal(t, "Hello from Fleet", lastMessage["Subject"])
// (2) Inviting a user should send an e-mail to join.
_, err = svc.InviteNewUser(ctx, fleet.InvitePayload{
Email: ptr.String("foobar_recipient@example.com"),
Name: ptr.String("Foobar"),
GlobalRole: null.NewString("observer", true),
})
require.NoError(t, err)
lastMessage = getLastMailPitMessage()
require.Equal(t, "You are Invited to Fleet", lastMessage["Subject"])
ds.UserByIDFunc = func(ctx context.Context, id uint) (*fleet.User, error) {
if id == 1 {
return test.UserAdmin, nil
}
return nil, newNotFoundError()
}
ds.InviteByEmailFunc = func(ctx context.Context, email string) (*fleet.Invite, error) {
return nil, newTestNotFoundError()
}
ds.PendingEmailChangeFunc = func(ctx context.Context, userID uint, newEmail, token string) error {
return nil
}
ds.SaveUserFunc = func(ctx context.Context, user *fleet.User) error {
return nil
}
// (3) Changing e-mail address should send an e-mail for confirmation.
_, err = svc.ModifyUser(ctx, 1, fleet.UserPayload{
Email: ptr.String("useradmin_2@example.com"),
})
require.NoError(t, err)
lastMessage = getLastMailPitMessage()
require.Equal(t, "Confirm Fleet Email Change", lastMessage["Subject"])
}

View File

@ -22,7 +22,7 @@ func (mw metricsMiddleware) NewAppConfig(ctx context.Context, p fleet.AppConfig)
return info, err return info, err
} }
func (mw metricsMiddleware) AppConfig(ctx context.Context) (*fleet.AppConfig, error) { func (mw metricsMiddleware) AppConfigObfuscated(ctx context.Context) (*fleet.AppConfig, error) {
var ( var (
info *fleet.AppConfig info *fleet.AppConfig
err error err error
@ -32,7 +32,7 @@ func (mw metricsMiddleware) AppConfig(ctx context.Context) (*fleet.AppConfig, er
mw.requestCount.With(lvs...).Add(1) mw.requestCount.With(lvs...).Add(1)
mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
}(time.Now()) }(time.Now())
info, err = mw.Service.AppConfig(ctx) info, err = mw.Service.AppConfigObfuscated(ctx)
return info, err return info, err
} }

View File

@ -19,6 +19,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/logging" "github.com/fleetdm/fleet/v4/server/logging"
"github.com/fleetdm/fleet/v4/server/mail"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/async" "github.com/fleetdm/fleet/v4/server/service/async"
@ -44,7 +45,6 @@ func newTestService(t *testing.T, ds fleet.Datastore, rs fleet.QueryResultStore,
} }
func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig config.FleetConfig, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, opts ...*TestServerOpts) (fleet.Service, context.Context) { func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig config.FleetConfig, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, opts ...*TestServerOpts) (fleet.Service, context.Context) {
mailer := &mockMailService{SendEmailFn: func(e fleet.Email) error { return nil }}
lic := &fleet.LicenseInfo{Tier: fleet.TierFree} lic := &fleet.LicenseInfo{Tier: fleet.TierFree}
writer, err := logging.NewFilesystemLogWriter(fleetConfig.Filesystem.StatusLogFile, kitlog.NewNopLogger(), fleetConfig.Filesystem.EnableLogRotation, fleetConfig.Filesystem.EnableLogCompression, 500, 28, 3) writer, err := logging.NewFilesystemLogWriter(fleetConfig.Filesystem.StatusLogFile, kitlog.NewNopLogger(), fleetConfig.Filesystem.EnableLogRotation, fleetConfig.Filesystem.EnableLogCompression, 500, 28, 3)
require.NoError(t, err) require.NoError(t, err)
@ -61,6 +61,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
mdmStorage nanomdm_storage.AllStorage mdmStorage nanomdm_storage.AllStorage
depStorage nanodep_storage.AllStorage depStorage nanodep_storage.AllStorage
mdmPusher nanomdm_push.Pusher mdmPusher nanomdm_push.Pusher
mailer fleet.MailService = &mockMailService{SendEmailFn: func(e fleet.Email) error { return nil }}
) )
var c clock.Clock = clock.C var c clock.Clock = clock.C
if len(opts) > 0 { if len(opts) > 0 {
@ -94,6 +95,9 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
if opts[0].EnrollHostLimiter != nil { if opts[0].EnrollHostLimiter != nil {
enrollHostLimiter = opts[0].EnrollHostLimiter enrollHostLimiter = opts[0].EnrollHostLimiter
} }
if opts[0].UseMailService {
mailer = mail.NewService()
}
// allow to explicitly set installer store to nil // allow to explicitly set installer store to nil
is = opts[0].Is is = opts[0].Is
@ -253,6 +257,7 @@ type TestServerOpts struct {
MDMPusher nanomdm_push.Pusher MDMPusher nanomdm_push.Pusher
HTTPServerConfig *http.Server HTTPServerConfig *http.Server
StartCronSchedules []TestNewScheduleFunc StartCronSchedules []TestNewScheduleFunc
UseMailService bool
} }
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) { func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {

View File

@ -763,7 +763,7 @@ func (svc *Service) modifyEmailAddress(ctx context.Context, user *fleet.User, em
if err != nil { if err != nil {
return err return err
} }
config, err := svc.AppConfig(ctx) config, err := svc.ds.AppConfig(ctx)
if err != nil { if err != nil {
return err return err
} }

View File

@ -14,6 +14,7 @@ var (
UserAdmin = &fleet.User{ UserAdmin = &fleet.User{
ID: 2, ID: 2,
GlobalRole: ptr.String(fleet.RoleAdmin), GlobalRole: ptr.String(fleet.RoleAdmin),
Email: "useradmin@example.com",
} }
UserMaintainer = &fleet.User{ UserMaintainer = &fleet.User{
ID: 3, ID: 3,

1
tools/mailpit/auth.txt Normal file
View File

@ -0,0 +1 @@
mailpit-username:mailpit-password