diff --git a/cmd/server/assets/realmadmin/_form_email.html b/cmd/server/assets/realmadmin/_form_email.html index e6ad35dd4..2c80ec6f6 100644 --- a/cmd/server/assets/realmadmin/_form_email.html +++ b/cmd/server/assets/realmadmin/_form_email.html @@ -34,7 +34,7 @@
+ placeholder="SMTP account" value="{{if $emailConfig.SMTPAccount}}{{$emailConfig.SMTPAccount}}{{end}}" /> {{template "errorable" $emailConfig.ErrorsFor "smtpAccount"}} @@ -43,16 +43,9 @@
-
- - -
- - - -
-
+ + {{template "errorable" $emailConfig.ErrorsFor "smtpPassword"}} This is the password for your SMTP email. @@ -61,7 +54,7 @@
+ placeholder="SMTP host" value="{{if $emailConfig.SMTPHost}}{{$emailConfig.SMTPHost}}{{end}}" /> {{template "errorable" $emailConfig.ErrorsFor "smtpHost"}} @@ -71,7 +64,7 @@
+ placeholder="SMTP port" value="{{if $emailConfig.SMTPPort}}{{$emailConfig.SMTPPort}}{{else}}587{{end}}" /> {{template "errorable" $emailConfig.ErrorsFor "smtpPort"}} @@ -117,7 +110,7 @@
- +
diff --git a/cmd/server/assets/realmadmin/_form_sms.html b/cmd/server/assets/realmadmin/_form_sms.html index b269372f9..1e590d05d 100644 --- a/cmd/server/assets/realmadmin/_form_sms.html +++ b/cmd/server/assets/realmadmin/_form_sms.html @@ -35,7 +35,7 @@
+ placeholder="Twilio account" value="{{if $smsConfig.TwilioAccountSid}}{{$smsConfig.TwilioAccountSid}}{{end}}" /> {{template "errorable" $smsConfig.ErrorsFor "twilioAccountSid"}} @@ -44,16 +44,9 @@
-
- - -
- - - -
-
+ + {{template "errorable" $smsConfig.ErrorsFor "twilioAuthToken"}} This is the Twilio Auth Token. Get this value from the Twilio console. @@ -62,7 +55,7 @@
+ placeholder="Twilio number" value="{{if $smsConfig.TwilioFromNumber}}{{$smsConfig.TwilioFromNumber}}{{end}}" /> {{template "errorable" $smsConfig.ErrorsFor "twilioFromNumber"}} @@ -90,7 +83,7 @@
- +
diff --git a/pkg/controller/realmadmin/settings.go b/pkg/controller/realmadmin/settings.go index 67eea6e6e..cd67b4e08 100644 --- a/pkg/controller/realmadmin/settings.go +++ b/pkg/controller/realmadmin/settings.go @@ -26,6 +26,11 @@ import ( "github.com/google/exposure-notifications-verification-server/pkg/sms" ) +const ( + // passwordSentinel is the password string inserted into forms. + passwordSentinel = "very-nice-try-maybe-next-time" +) + var ( shortCodeLengths = []int{6, 7, 8} shortCodeMinutes = []int{} @@ -265,7 +270,9 @@ func (c *Controller) HandleSettings() http.Handler { // record. smsConfig.ProviderType = sms.ProviderTypeTwilio smsConfig.TwilioAccountSid = form.TwilioAccountSid - smsConfig.TwilioAuthToken = form.TwilioAuthToken + if v := form.TwilioAuthToken; v != passwordSentinel { + smsConfig.TwilioAuthToken = v + } smsConfig.TwilioFromNumber = form.TwilioFromNumber if err := c.db.SaveSMSConfig(smsConfig); err != nil { @@ -305,7 +312,9 @@ func (c *Controller) HandleSettings() http.Handler { // record. emailConfig.ProviderType = email.ProviderTypeSMTP emailConfig.SMTPAccount = form.SMTPAccount - emailConfig.SMTPPassword = form.SMTPPassword + if v := form.SMTPPassword; v != passwordSentinel { + emailConfig.SMTPPassword = form.SMTPPassword + } emailConfig.SMTPHost = form.SMTPHost emailConfig.SMTPPort = form.SMTPPort @@ -450,5 +459,7 @@ func (c *Controller) renderSettings( m["quotaLimit"] = quotaLimit m["quotaRemaining"] = quotaRemaining + m["passwordSentinel"] = passwordSentinel + c.h.RenderHTML(w, "realmadmin/edit", m) } diff --git a/pkg/controller/realmadmin/sms_test.go b/pkg/controller/realmadmin/sms_test.go new file mode 100644 index 000000000..77eef6e35 --- /dev/null +++ b/pkg/controller/realmadmin/sms_test.go @@ -0,0 +1,210 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package realmadmin_test + +import ( + "context" + "testing" + "time" + + "github.com/google/exposure-notifications-verification-server/internal/browser" + "github.com/google/exposure-notifications-verification-server/internal/envstest" + "github.com/google/exposure-notifications-verification-server/pkg/controller" + "github.com/google/exposure-notifications-verification-server/pkg/database" + + "github.com/chromedp/chromedp" +) + +func TestHandleSettings_SMS(t *testing.T) { + harness := envstest.NewServer(t) + + // Get the default realm + realm, err := harness.Database.FindRealm(1) + if err != nil { + t.Fatal(err) + } + + // Create a user + admin := &database.User{ + Email: "admin@example.com", + Name: "Admin", + Realms: []*database.Realm{realm}, + AdminRealms: []*database.Realm{realm}, + } + if err := harness.Database.SaveUser(admin, database.System); err != nil { + t.Fatal(err) + } + + // Log in the user. + session, err := harness.LoggedInSession(nil, admin.Email) + if err != nil { + t.Fatal(err) + } + + // Set the current realm. + controller.StoreSessionRealm(session, realm) + + // Mint a cookie for the session. + cookie, err := harness.SessionCookie(session) + if err != nil { + t.Fatal(err) + } + + // Create a browser runner. + browserCtx := browser.New(t) + taskCtx, done := context.WithTimeout(browserCtx, 30*time.Second) + defer done() + + var twilioAccountSid string + var twilioAuthToken string + var twilioFromNumber string + + if err := chromedp.Run(taskCtx, + // Pre-authenticate the user. + browser.SetCookie(cookie), + + // Visit page. + chromedp.Navigate(`http://`+harness.Server.Addr()+`/realm/settings#sms`), + + // Wait for render. + chromedp.WaitVisible(`div#sms`, chromedp.ByQuery), + + // Fill out the form. + chromedp.SetValue(`input#twilio-account-sid`, "accountSid", chromedp.ByQuery), + chromedp.SetValue(`input#twilio-auth-token`, "authToken", chromedp.ByQuery), + chromedp.SetValue(`input#twilio-from-number`, "1234567890", chromedp.ByQuery), + + // Click submit. + chromedp.Click(`input#update-sms`, chromedp.ByQuery), + + // Wait for the page to reload. + chromedp.WaitVisible(`div#sms`, chromedp.ByQuery), + + chromedp.Value(`input#twilio-account-sid`, &twilioAccountSid, chromedp.ByQuery), + chromedp.Value(`input#twilio-auth-token`, &twilioAuthToken, chromedp.ByQuery), + chromedp.Value(`input#twilio-from-number`, &twilioFromNumber, chromedp.ByQuery), + ); err != nil { + t.Fatal(err) + } + + // Check form + if got, want := twilioAccountSid, "accountSid"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + if got, want := twilioAuthToken, "very-nice-try-maybe-next-time"; got != want { + // very-nice-try-maybe-next-time comes from passwordSentinel + t.Errorf("expected %q to be %q", got, want) + } + if got, want := twilioFromNumber, "1234567890"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + + { + // Check database + smsConfig, err := realm.SMSConfig(harness.Database) + if err != nil { + t.Fatal(err) + } + if smsConfig == nil { + t.Fatal("expected smsConfig") + } + + if got, want := smsConfig.TwilioAccountSid, "accountSid"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + if got, want := smsConfig.TwilioAuthToken, "authToken"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + if got, want := smsConfig.TwilioFromNumber, "1234567890"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + } + + // Update + if err := chromedp.Run(taskCtx, + // Pre-authenticate the user. + browser.SetCookie(cookie), + + // Visit page. + chromedp.Navigate(`http://`+harness.Server.Addr()+`/realm/settings#sms`), + + // Wait for render. + chromedp.WaitVisible(`div#sms`, chromedp.ByQuery), + + // Fill out the form. + chromedp.SetValue(`input#twilio-account-sid`, "accountSid-new", chromedp.ByQuery), + chromedp.SetValue(`input#twilio-from-number`, "1234567890-new", chromedp.ByQuery), + + // Click submit. + chromedp.Click(`input#update-sms`, chromedp.ByQuery), + + // Wait for the page to reload. + chromedp.WaitVisible(`div#sms`, chromedp.ByQuery), + ); err != nil { + t.Fatal(err) + } + + { + // Check database again + smsConfig, err := realm.SMSConfig(harness.Database) + if err != nil { + t.Fatal(err) + } + if smsConfig == nil { + t.Fatal("expected smsConfig") + } + + if got, want := smsConfig.TwilioAccountSid, "accountSid-new"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + if got, want := smsConfig.TwilioAuthToken, "authToken"; got != want { + // should not change + t.Errorf("expected %q to be %q", got, want) + } + if got, want := smsConfig.TwilioFromNumber, "1234567890-new"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + } + + // Delete + if err := chromedp.Run(taskCtx, + // Pre-authenticate the user. + browser.SetCookie(cookie), + + // Visit page. + chromedp.Navigate(`http://`+harness.Server.Addr()+`/realm/settings#sms`), + + // Wait for render. + chromedp.WaitVisible(`div#sms`, chromedp.ByQuery), + + // Fill out the form. + chromedp.SetValue(`input#twilio-account-sid`, "", chromedp.ByQuery), + chromedp.SetValue(`input#twilio-auth-token`, "", chromedp.ByQuery), + chromedp.SetValue(`input#twilio-from-number`, "", chromedp.ByQuery), + + // Click submit. + chromedp.Click(`input#update-sms`, chromedp.ByQuery), + + // Wait for the page to reload. + chromedp.WaitVisible(`div#sms`, chromedp.ByQuery), + ); err != nil { + t.Fatal(err) + } + + // Check database again + if _, err := realm.SMSConfig(harness.Database); !database.IsNotFound(err) { + t.Fatal("expected smsConfig to be deleted") + } +} diff --git a/pkg/controller/realmadmin/smtp_test.go b/pkg/controller/realmadmin/smtp_test.go new file mode 100644 index 000000000..db8b25086 --- /dev/null +++ b/pkg/controller/realmadmin/smtp_test.go @@ -0,0 +1,209 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package realmadmin_test + +import ( + "context" + "testing" + "time" + + "github.com/google/exposure-notifications-verification-server/internal/browser" + "github.com/google/exposure-notifications-verification-server/internal/envstest" + "github.com/google/exposure-notifications-verification-server/pkg/controller" + "github.com/google/exposure-notifications-verification-server/pkg/database" + + "github.com/chromedp/chromedp" +) + +func TestHandleSettings_SMTP(t *testing.T) { + harness := envstest.NewServer(t) + + // Get the default realm + realm, err := harness.Database.FindRealm(1) + if err != nil { + t.Fatal(err) + } + + // Create a user + admin := &database.User{ + Email: "admin@example.com", + Name: "Admin", + Realms: []*database.Realm{realm}, + AdminRealms: []*database.Realm{realm}, + } + if err := harness.Database.SaveUser(admin, database.System); err != nil { + t.Fatal(err) + } + + // Log in the user. + session, err := harness.LoggedInSession(nil, admin.Email) + if err != nil { + t.Fatal(err) + } + + // Set the current realm. + controller.StoreSessionRealm(session, realm) + + // Mint a cookie for the session. + cookie, err := harness.SessionCookie(session) + if err != nil { + t.Fatal(err) + } + + // Create a browser runner. + browserCtx := browser.New(t) + taskCtx, done := context.WithTimeout(browserCtx, 30*time.Second) + defer done() + + var smtpAccount string + var smtpPassword string + var smtpHost string + + if err := chromedp.Run(taskCtx, + // Pre-authenticate the user. + browser.SetCookie(cookie), + + // Visit page. + chromedp.Navigate(`http://`+harness.Server.Addr()+`/realm/settings#email`), + + // Wait for render. + chromedp.WaitVisible(`div#email`, chromedp.ByQuery), + + // Fill out the form. + chromedp.SetValue(`input#smtp-account`, "myAccount", chromedp.ByQuery), + chromedp.SetValue(`input#smtp-password`, "superSecret", chromedp.ByQuery), + chromedp.SetValue(`input#smtp-host`, "1.1.1.1", chromedp.ByQuery), + + // Click submit. + chromedp.Click(`input#update-smtp`, chromedp.ByQuery), + + // Wait for the page to reload. + chromedp.WaitVisible(`div#email`, chromedp.ByQuery), + + chromedp.Value(`input#smtp-account`, &smtpAccount, chromedp.ByQuery), + chromedp.Value(`input#smtp-password`, &smtpPassword, chromedp.ByQuery), + chromedp.Value(`input#smtp-host`, &smtpHost, chromedp.ByQuery), + ); err != nil { + t.Fatal(err) + } + + // Check form + if got, want := smtpAccount, "myAccount"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + if got, want := smtpPassword, "very-nice-try-maybe-next-time"; got != want { + // very-nice-try-maybe-next-time comes from passwordSentinel + t.Errorf("expected %q to be %q", got, want) + } + if got, want := smtpHost, "1.1.1.1"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + + { + // Check database + emailConfig, err := realm.EmailConfig(harness.Database) + if err != nil { + t.Fatal(err) + } + if emailConfig == nil { + t.Fatal("expected emailConfig") + } + + if got, want := emailConfig.SMTPAccount, "myAccount"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + if got, want := emailConfig.SMTPPassword, "superSecret"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + if got, want := emailConfig.SMTPHost, "1.1.1.1"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + } + + // Update + if err := chromedp.Run(taskCtx, + // Pre-authenticate the user. + browser.SetCookie(cookie), + + // Visit page. + chromedp.Navigate(`http://`+harness.Server.Addr()+`/realm/settings#email`), + + // Wait for render. + chromedp.WaitVisible(`div#email`, chromedp.ByQuery), + + // Fill out the form. + chromedp.SetValue(`input#smtp-account`, "myAccount-new", chromedp.ByQuery), + chromedp.SetValue(`input#smtp-host`, "1.1.1.1-new", chromedp.ByQuery), + + // Click submit. + chromedp.Click(`input#update-smtp`, chromedp.ByQuery), + + // Wait for the page to reload. + chromedp.WaitVisible(`div#email`, chromedp.ByQuery), + ); err != nil { + t.Fatal(err) + } + + { + // Check database again + emailConfig, err := realm.EmailConfig(harness.Database) + if err != nil { + t.Fatal(err) + } + if emailConfig == nil { + t.Fatal("expected emailConfig") + } + + if got, want := emailConfig.SMTPAccount, "myAccount-new"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + if got, want := emailConfig.SMTPPassword, "superSecret"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + if got, want := emailConfig.SMTPHost, "1.1.1.1-new"; got != want { + t.Errorf("expected %q to be %q", got, want) + } + } + + // Delete + if err := chromedp.Run(taskCtx, + // Pre-authenticate the user. + browser.SetCookie(cookie), + + // Visit page. + chromedp.Navigate(`http://`+harness.Server.Addr()+`/realm/settings#email`), + + // Wait for render. + chromedp.WaitVisible(`div#email`, chromedp.ByQuery), + + // Fill out the form. + chromedp.SetValue(`input#smtp-account`, "", chromedp.ByQuery), + chromedp.SetValue(`input#smtp-password`, "", chromedp.ByQuery), + chromedp.SetValue(`input#smtp-host`, "", chromedp.ByQuery), + + // Click submit. + chromedp.Click(`input#update-smtp`, chromedp.ByQuery), + + // Wait for the page to reload. + chromedp.WaitVisible(`div#email`, chromedp.ByQuery), + ); err != nil { + t.Fatal(err) + } + + // Check database again + if _, err := realm.EmailConfig(harness.Database); !database.IsNotFound(err) { + t.Fatal("expected emailConfig to be deleted") + } +}