diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml index 7676faf6d1..4a1bfba5c9 100644 --- a/.github/workflows/test-go.yaml +++ b/.github/workflows/test-go.yaml @@ -60,7 +60,7 @@ jobs: # Pre-starting dependencies here means they are ready to go when we need them. - name: Start Infra Dependencies # 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 - name: Install Go Dependencies @@ -95,6 +95,7 @@ jobs: MYSQL_TEST=1 \ MINIO_STORAGE_TEST=1 \ SAML_IDP_TEST=1 \ + MAIL_TEST=1 \ NETWORK_TEST_GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \ make test-go 2>&1 | tee /tmp/gotest.log diff --git a/changes/9609-fix-smtp-email-send b/changes/9609-fix-smtp-email-send new file mode 100644 index 0000000000..887bdccff9 --- /dev/null +++ b/changes/9609-fix-smtp-email-send @@ -0,0 +1 @@ +* Fix e-mail sending on user invites and user e-mail change when SMTP server has credentials. diff --git a/docker-compose.yml b/docker-compose.yml index 46ec11d4d7..f038b60a1d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,12 +55,27 @@ services: - /var/lib/mysql:rw,noexec,nosuid - /tmpfs + # Unauthenticated SMTP server. mailhog: image: mailhog/mailhog:latest ports: - "8025:8025" - "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: image: redis:5 ports: diff --git a/docs/Contributing/Testing-and-local-development.md b/docs/Contributing/Testing-and-local-development.md index 4932e649cf..1fe3756909 100644 --- a/docs/Contributing/Testing-and-local-development.md +++ b/docs/Contributing/Testing-and-local-development.md @@ -19,7 +19,9 @@ - [Command line](#command-line) - [Test hosts](#test-hosts) - [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) - [MySQL shell](#mysql-shell) - [Redis REPL](#redis-repl) @@ -30,8 +32,7 @@ - [Telemetry](#telemetry) - [MDM setup and testing](#mdm-setup-and-testing) - [ABM setup](#abm-setup) - - [Private key + certificate](#private-key--certificate) - - [Encrypted token](#encrypted-token) + - [Private key, certificate, and encrypted token](#private-key-certificate-and-encrypted-token) - [APNs and SCEP setup](#apns-and-scep-setup) - [Running the server](#running-the-server) - [Testing MDM](#testing-mdm) @@ -232,7 +233,9 @@ The Fleet repo includes tools to start testing osquery hosts. Please see the doc ## 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. @@ -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. +#### 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 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. 3. Boot the machine, it should automatically enroll into MDM. + + \ No newline at end of file diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 3314c4a021..9971d6d657 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -27,7 +27,7 @@ func (svc *Service) GetAppleBM(ctx context.Context) (*fleet.AppleBM, error) { return nil, notFoundError{} } - appCfg, err := svc.AppConfig(ctx) + appCfg, err := svc.AppConfigObfuscated(ctx) if err != nil { return nil, err } diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index ab75d2086f..35acae7c0d 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -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 { return err } diff --git a/server/fleet/service.go b/server/fleet/service.go index fe44094c4c..908f56bb1a 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -351,7 +351,8 @@ type Service interface { // AppConfigService provides methods for configuring the Fleet application 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) SandboxEnabled() bool diff --git a/server/mail/mail_test.go b/server/mail/mail_test.go index 38c727dd61..ba964899a5 100644 --- a/server/mail/mail_test.go +++ b/server/mail/mail_test.go @@ -10,30 +10,23 @@ import ( "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){ testSMTPPlainAuth, + testSMTPPlainAuthInvalidCreds, testSMTPSkipVerify, testSMTPNoAuth, testMailTest, } 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 { - r := getMailer() + r := NewService() t.Run(test.FunctionName(f), func(t *testing.T) { f(t, r) @@ -50,12 +43,12 @@ func testSMTPPlainAuth(t *testing.T, mailer fleet.MailService) { SMTPConfigured: true, SMTPAuthenticationType: fleet.AuthTypeNameUserNamePassword, SMTPAuthenticationMethod: fleet.AuthMethodNamePlain, - SMTPUserName: "bob", - SMTPPassword: "secret", + SMTPUserName: "mailpit-username", + SMTPPassword: "mailpit-password", SMTPEnableTLS: true, SMTPVerifySSLCerts: true, SMTPEnableStartTLS: true, - SMTPPort: 1025, + SMTPPort: 1026, SMTPServer: "localhost", SMTPSenderAddress: "test@example.com", }, @@ -69,6 +62,34 @@ func testSMTPPlainAuth(t *testing.T, mailer fleet.MailService) { 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) { mail := fleet.Email{ Subject: "skip verify", @@ -78,8 +99,8 @@ func testSMTPSkipVerify(t *testing.T, mailer fleet.MailService) { SMTPConfigured: true, SMTPAuthenticationType: fleet.AuthTypeNameUserNamePassword, SMTPAuthenticationMethod: fleet.AuthMethodNamePlain, - SMTPUserName: "bob", - SMTPPassword: "secret", + SMTPUserName: "mailpit-username", + SMTPPassword: "mailpit-password", SMTPEnableTLS: true, SMTPVerifySSLCerts: false, SMTPEnableStartTLS: true, @@ -127,13 +148,16 @@ func testMailTest(t *testing.T, mailer fleet.MailService) { To: []string{"bob@foo.com"}, Config: &fleet.AppConfig{ SMTPSettings: fleet.SMTPSettings{ - SMTPConfigured: true, - SMTPAuthenticationType: fleet.AuthTypeNameNone, - SMTPEnableTLS: true, - SMTPVerifySSLCerts: true, - SMTPPort: 1025, - SMTPServer: "localhost", - SMTPSenderAddress: "test@example.com", + SMTPConfigured: true, + SMTPAuthenticationType: fleet.AuthTypeNameUserNamePassword, + SMTPAuthenticationMethod: fleet.AuthMethodNamePlain, + SMTPUserName: "mailpit-username", + SMTPPassword: "mailpit-password", + SMTPEnableTLS: true, + SMTPVerifySSLCerts: true, + SMTPPort: 1026, + SMTPServer: "localhost", + SMTPSenderAddress: "test@example.com", }, }, Mailer: &SMTPTestMailer{ @@ -142,7 +166,6 @@ func testMailTest(t *testing.T, mailer fleet.MailService) { } err := Test(mailer, mail) assert.Nil(t, err) - } func TestTemplateProcessor(t *testing.T) { diff --git a/server/service/appconfig.go b/server/service/appconfig.go index ff31dd4a8e..0859e2cb4b 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -70,7 +70,7 @@ func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Se if !ok { return nil, errors.New("could not fetch user") } - config, err := svc.AppConfig(ctx) + config, err := svc.AppConfigObfuscated(ctx) if err != nil { return nil, err } @@ -144,7 +144,7 @@ func (svc *Service) SandboxEnabled() bool { 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 err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil { return nil, err @@ -340,7 +340,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle return nil, legacyUsedWarning } // must reload to get the unchanged app config - return svc.AppConfig(ctx) + return svc.AppConfigObfuscated(ctx) } // 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 - obfuscatedConfig, err := svc.AppConfig(ctx) + obfuscatedConfig, err := svc.AppConfigObfuscated(ctx) if err != nil { 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. func (svc *Service) CertificateChain(ctx context.Context) ([]byte, error) { - config, err := svc.AppConfig(ctx) + config, err := svc.AppConfigObfuscated(ctx) if err != nil { return nil, err } diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index f293b590b5..a2ad4d72de 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -98,7 +98,7 @@ func TestAppConfigAuth(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user}) - _, err := svc.AppConfig(ctx) + _, err := svc.AppConfigObfuscated(ctx) checkAuthErr(t, tt.shouldFailRead, err) _, err = svc.ModifyAppConfig(ctx, []byte(`{}`), fleet.ApplySpecOptions{}) @@ -423,7 +423,7 @@ func TestAppConfigSecretsObfuscated(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user}) - ac, err := svc.AppConfig(ctx) + ac, err := svc.AppConfigObfuscated(ctx) require.NoError(t, err) require.Equal(t, ac.SMTPSettings.SMTPPassword, fleet.MaskedPassword) require.Equal(t, ac.Integrations.Jira[0].APIToken, fleet.MaskedPassword) @@ -564,7 +564,7 @@ func TestTransparencyURL(t *testing.T) { return nil } - ac, err := svc.AppConfig(ctx) + ac, err := svc.AppConfigObfuscated(ctx) require.NoError(t, err) require.Equal(t, tt.initialURL, ac.FleetDesktop.TransparencyURL) @@ -576,7 +576,7 @@ func TestTransparencyURL(t *testing.T) { if modified != nil { require.Equal(t, tt.expectedURL, modified.FleetDesktop.TransparencyURL) - ac, err = svc.AppConfig(ctx) + ac, err = svc.AppConfigObfuscated(ctx) require.NoError(t, err) require.Equal(t, tt.expectedURL, ac.FleetDesktop.TransparencyURL) } @@ -613,7 +613,7 @@ func TestTransparencyURLDowngradeLicense(t *testing.T) { return nil } - ac, err := svc.AppConfig(ctx) + ac, err := svc.AppConfigObfuscated(ctx) require.NoError(t, err) require.Equal(t, "https://example.com/transparency", ac.FleetDesktop.TransparencyURL) @@ -633,7 +633,7 @@ func TestTransparencyURLDowngradeLicense(t *testing.T) { require.NoError(t, err) require.NotNil(t, modified) require.Equal(t, "", modified.FleetDesktop.TransparencyURL) - ac, err = svc.AppConfig(ctx) + ac, err = svc.AppConfigObfuscated(ctx) require.NoError(t, err) require.Equal(t, "f1337", ac.OrgInfo.OrgName) require.Equal(t, "", ac.FleetDesktop.TransparencyURL) @@ -716,7 +716,7 @@ func TestService_ModifyAppConfig_MDM(t *testing.T) { return nil, errors.New(notFoundErr) } - ac, err := svc.AppConfig(ctx) + ac, err := svc.AppConfigObfuscated(ctx) require.NoError(t, err) require.Equal(t, tt.oldMDM, ac.MDM) @@ -731,7 +731,7 @@ func TestService_ModifyAppConfig_MDM(t *testing.T) { } require.NoError(t, err) require.Equal(t, tt.expectedMDM, modified.MDM) - ac, err = svc.AppConfig(ctx) + ac, err = svc.AppConfigObfuscated(ctx) require.NoError(t, err) require.Equal(t, tt.expectedMDM, ac.MDM) }) diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 86b0e7bcce..40ff722b1c 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -1470,7 +1470,7 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm return ctxerr.Wrap(ctx, err) } - appCfg, err := svc.AppConfig(ctx) + appCfg, err := svc.AppConfigObfuscated(ctx) if err != nil { 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 { - ac, err := svc.AppConfig(ctx) + ac, err := svc.AppConfigObfuscated(ctx) if err != nil { return err } diff --git a/server/service/devices.go b/server/service/devices.go index abb6747af6..9f680bc33b 100644 --- a/server/service/devices.go +++ b/server/service/devices.go @@ -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; // 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). - ac, err := svc.AppConfig(ctx) + ac, err := svc.AppConfigObfuscated(ctx) if 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 transparencyURL(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { - config, err := svc.AppConfig(ctx) + config, err := svc.AppConfigObfuscated(ctx) if err != nil { return transparencyURLResponse{Err: err}, nil } diff --git a/server/service/invites.go b/server/service/invites.go index 9b9600a822..d42497023f 100644 --- a/server/service/invites.go +++ b/server/service/invites.go @@ -95,7 +95,7 @@ func (svc *Service) InviteNewUser(ctx context.Context, payload fleet.InvitePaylo return nil, err } - config, err := svc.AppConfig(ctx) + config, err := svc.ds.AppConfig(ctx) if err != nil { return nil, err } diff --git a/server/service/mail_test.go b/server/service/mail_test.go new file mode 100644 index 0000000000..d330034220 --- /dev/null +++ b/server/service/mail_test.go @@ -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 ¬TestFoundError{} +} + +// 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"]) +} diff --git a/server/service/metrics_appconfig.go b/server/service/metrics_appconfig.go index e8828ea049..972ea6c92a 100644 --- a/server/service/metrics_appconfig.go +++ b/server/service/metrics_appconfig.go @@ -22,7 +22,7 @@ func (mw metricsMiddleware) NewAppConfig(ctx context.Context, p fleet.AppConfig) return info, err } -func (mw metricsMiddleware) AppConfig(ctx context.Context) (*fleet.AppConfig, error) { +func (mw metricsMiddleware) AppConfigObfuscated(ctx context.Context) (*fleet.AppConfig, error) { var ( info *fleet.AppConfig err error @@ -32,7 +32,7 @@ func (mw metricsMiddleware) AppConfig(ctx context.Context) (*fleet.AppConfig, er mw.requestCount.With(lvs...).Add(1) mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) }(time.Now()) - info, err = mw.Service.AppConfig(ctx) + info, err = mw.Service.AppConfigObfuscated(ctx) return info, err } diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index 5f14a32f26..2867a5d328 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -19,6 +19,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/logging" + "github.com/fleetdm/fleet/v4/server/mail" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/ptr" "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) { - mailer := &mockMailService{SendEmailFn: func(e fleet.Email) error { return nil }} lic := &fleet.LicenseInfo{Tier: fleet.TierFree} writer, err := logging.NewFilesystemLogWriter(fleetConfig.Filesystem.StatusLogFile, kitlog.NewNopLogger(), fleetConfig.Filesystem.EnableLogRotation, fleetConfig.Filesystem.EnableLogCompression, 500, 28, 3) require.NoError(t, err) @@ -61,6 +61,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf mdmStorage nanomdm_storage.AllStorage depStorage nanodep_storage.AllStorage mdmPusher nanomdm_push.Pusher + mailer fleet.MailService = &mockMailService{SendEmailFn: func(e fleet.Email) error { return nil }} ) var c clock.Clock = clock.C if len(opts) > 0 { @@ -94,6 +95,9 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf if opts[0].EnrollHostLimiter != nil { enrollHostLimiter = opts[0].EnrollHostLimiter } + if opts[0].UseMailService { + mailer = mail.NewService() + } // allow to explicitly set installer store to nil is = opts[0].Is @@ -253,6 +257,7 @@ type TestServerOpts struct { MDMPusher nanomdm_push.Pusher HTTPServerConfig *http.Server StartCronSchedules []TestNewScheduleFunc + UseMailService bool } func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) { diff --git a/server/service/users.go b/server/service/users.go index 33f72d77d3..f7e43bc0ab 100644 --- a/server/service/users.go +++ b/server/service/users.go @@ -763,7 +763,7 @@ func (svc *Service) modifyEmailAddress(ctx context.Context, user *fleet.User, em if err != nil { return err } - config, err := svc.AppConfig(ctx) + config, err := svc.ds.AppConfig(ctx) if err != nil { return err } diff --git a/server/test/users.go b/server/test/users.go index 18095c5b7d..15748490b3 100644 --- a/server/test/users.go +++ b/server/test/users.go @@ -14,6 +14,7 @@ var ( UserAdmin = &fleet.User{ ID: 2, GlobalRole: ptr.String(fleet.RoleAdmin), + Email: "useradmin@example.com", } UserMaintainer = &fleet.User{ ID: 3, diff --git a/tools/mailpit/auth.txt b/tools/mailpit/auth.txt new file mode 100644 index 0000000000..0d785a765a --- /dev/null +++ b/tools/mailpit/auth.txt @@ -0,0 +1 @@ +mailpit-username:mailpit-password