loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[SEC] Notify owner about TOTP enrollment

- In the spirit of #4635
- Notify the owner when their account is getting enrolled into TOTP. The
message is changed according if they have security keys or not.
- Integration test added.

Gusted a7e96aae 2285526e

+137
+7
models/unittest/unit_tests.go
··· 9 9 "code.gitea.io/gitea/models/db" 10 10 11 11 "github.com/stretchr/testify/assert" 12 + "github.com/stretchr/testify/require" 12 13 "xorm.io/builder" 13 14 ) 14 15 ··· 128 129 func AssertSuccessfulInsert(t assert.TestingT, beans ...any) { 129 130 err := db.Insert(db.DefaultContext, beans...) 130 131 assert.NoError(t, err) 132 + } 133 + 134 + // AssertSuccessfulDelete assert that beans is successfully deleted 135 + func AssertSuccessfulDelete(t require.TestingT, beans ...any) { 136 + err := db.DeleteBeans(db.DefaultContext, beans...) 137 + require.NoError(t, err) 131 138 } 132 139 133 140 // AssertCount assert the count of a bean
+4
options/locale/locale_en-US.ini
··· 517 517 account_security_caution.text_1 = If this was you, then you can safely ignore this mail. 518 518 account_security_caution.text_2 = If this wasn't you, your account is compromised. Please contact the admins of this site. 519 519 520 + totp_enrolled.subject = You have activated TOTP as 2FA method 521 + totp_enrolled.text_1.no_webauthn = You have just enabled TOTP for your account. This means that for all future logins to your account, you must use TOTP as a 2FA method. 522 + totp_enrolled.text_1.has_webauthn = You have just enabled TOTP for your account. This means that for all future logins to your account, you could use TOTP as a 2FA method or use any of your security keys. 523 + 520 524 register_success = Registration successful 521 525 522 526 issue_assigned.pull = @%[1]s assigned you to pull request %[2]s in repository %[3]s.
+5
routers/web/user/setting/security/2fa.go
··· 243 243 log.Error("Unable to save changes to the session: %v", err) 244 244 } 245 245 246 + if err := mailer.SendTOTPEnrolled(ctx, ctx.Doer); err != nil { 247 + ctx.ServerError("SendTOTPEnrolled", err) 248 + return 249 + } 250 + 246 251 if err = auth.NewTwoFactor(ctx, t); err != nil { 247 252 // FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us. 248 253 // If there is a unique constraint fail we should just tolerate the error
+34
services/mailer/mail.go
··· 44 44 mailAuthPrimaryMailChange base.TplName = "auth/primary_mail_change" 45 45 mailAuth2faDisabled base.TplName = "auth/2fa_disabled" 46 46 mailAuthRemovedSecurityKey base.TplName = "auth/removed_security_key" 47 + mailAuthTOTPEnrolled base.TplName = "auth/totp_enrolled" 47 48 48 49 mailNotifyCollaborator base.TplName = "notify/collaborator" 49 50 ··· 696 697 SendAsync(msg) 697 698 return nil 698 699 } 700 + 701 + // SendTOTPEnrolled informs the user that they've been enrolled into TOTP. 702 + func SendTOTPEnrolled(ctx context.Context, u *user_model.User) error { 703 + if setting.MailService == nil { 704 + return nil 705 + } 706 + locale := translation.NewLocale(u.Language) 707 + 708 + hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(ctx, u.ID) 709 + if err != nil { 710 + return err 711 + } 712 + 713 + data := map[string]any{ 714 + "locale": locale, 715 + "HasWebAuthn": hasWebAuthn, 716 + "DisplayName": u.DisplayName(), 717 + "Username": u.Name, 718 + "Language": locale.Language(), 719 + } 720 + 721 + var content bytes.Buffer 722 + 723 + if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthTOTPEnrolled), data); err != nil { 724 + return err 725 + } 726 + 727 + msg := NewMessage(u.EmailTo(), locale.TrString("mail.totp_enrolled.subject"), content.String()) 728 + msg.Info = fmt.Sprintf("UID: %d, enrolled into TOTP notification", u.ID) 729 + 730 + SendAsync(msg) 731 + return nil 732 + }
+15
templates/mail/auth/totp_enrolled.tmpl
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 5 + <meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no"> 6 + </head> 7 + 8 + <body> 9 + <p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br> 10 + {{if .HasWebAuthn}}<p>{{.locale.Tr "mail.totp_enrolled.text_1.has_webauthn"}}</p>{{else}}<p>{{.locale.Tr "mail.totp_enrolled.text_1.no_webauthn"}}</p>{{end}}<br> 11 + <p>{{.locale.Tr "mail.account_security_caution.text_1"}}</p><br> 12 + <p>{{.locale.Tr "mail.account_security_caution.text_2"}}</p><br> 13 + {{template "common/footer_simple" .}} 14 + </body> 15 + </html>
+72
tests/integration/user_test.go
··· 10 10 "strconv" 11 11 "strings" 12 12 "testing" 13 + "time" 13 14 14 15 auth_model "code.gitea.io/gitea/models/auth" 15 16 issues_model "code.gitea.io/gitea/models/issues" ··· 21 22 api "code.gitea.io/gitea/modules/structs" 22 23 "code.gitea.io/gitea/modules/test" 23 24 "code.gitea.io/gitea/modules/translation" 25 + gitea_context "code.gitea.io/gitea/services/context" 24 26 "code.gitea.io/gitea/services/mailer" 25 27 "code.gitea.io/gitea/tests" 26 28 29 + "github.com/pquerna/otp/totp" 27 30 "github.com/stretchr/testify/assert" 31 + "github.com/stretchr/testify/require" 28 32 ) 29 33 30 34 func TestViewUser(t *testing.T) { ··· 747 751 unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{UserID: user.ID, Name: "Little Bobby Tables's evil key"}) 748 752 }) 749 753 } 754 + 755 + func TestUserTOTPEnrolled(t *testing.T) { 756 + defer tests.PrepareTestEnv(t)() 757 + 758 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 759 + session := loginUser(t, user.Name) 760 + 761 + enrollTOTP := func(t *testing.T) { 762 + t.Helper() 763 + 764 + req := NewRequest(t, "GET", "/user/settings/security/two_factor/enroll") 765 + resp := session.MakeRequest(t, req, http.StatusOK) 766 + 767 + htmlDoc := NewHTMLParser(t, resp.Body) 768 + totpSecretKey, has := htmlDoc.Find(".twofa img[src^='data:image/png;base64']").Attr("alt") 769 + assert.True(t, has) 770 + 771 + currentTOTP, err := totp.GenerateCode(totpSecretKey, time.Now()) 772 + require.NoError(t, err) 773 + 774 + req = NewRequestWithValues(t, "POST", "/user/settings/security/two_factor/enroll", map[string]string{ 775 + "_csrf": htmlDoc.GetCSRF(), 776 + "passcode": currentTOTP, 777 + }) 778 + session.MakeRequest(t, req, http.StatusSeeOther) 779 + 780 + flashCookie := session.GetCookie(gitea_context.CookieNameFlash) 781 + assert.NotNil(t, flashCookie) 782 + assert.Contains(t, flashCookie.Value, "success%3DYour%2Baccount%2Bhas%2Bbeen%2Bsuccessfully%2Benrolled.") 783 + 784 + unittest.AssertSuccessfulDelete(t, &auth_model.TwoFactor{UID: user.ID}) 785 + } 786 + 787 + t.Run("No WebAuthn enabled", func(t *testing.T) { 788 + defer tests.PrintCurrentTest(t)() 789 + 790 + called := false 791 + defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) { 792 + assert.Len(t, msgs, 1) 793 + assert.Equal(t, user.EmailTo(), msgs[0].To) 794 + assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.totp_enrolled.subject"), msgs[0].Subject) 795 + assert.Contains(t, msgs[0].Body, translation.NewLocale("en-US").Tr("mail.totp_enrolled.text_1.no_webauthn")) 796 + called = true 797 + })() 798 + 799 + enrollTOTP(t) 800 + 801 + assert.True(t, called) 802 + }) 803 + 804 + t.Run("With WebAuthn enabled", func(t *testing.T) { 805 + defer tests.PrintCurrentTest(t)() 806 + 807 + called := false 808 + defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) { 809 + assert.Len(t, msgs, 1) 810 + assert.Equal(t, user.EmailTo(), msgs[0].To) 811 + assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.totp_enrolled.subject"), msgs[0].Subject) 812 + assert.Contains(t, msgs[0].Body, translation.NewLocale("en-US").Tr("mail.totp_enrolled.text_1.has_webauthn")) 813 + called = true 814 + })() 815 + 816 + unittest.AssertSuccessfulInsert(t, &auth_model.WebAuthnCredential{UserID: user.ID, Name: "Cueball's primary key"}) 817 + enrollTOTP(t) 818 + 819 + assert.True(t, called) 820 + }) 821 + }