1
Fork 0

Merge pull request 'fix: use ValidateEmail as binding across web forms' (#5158) from solomonv/consolidate-email-validation into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5158
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
Gusted 2024-10-21 14:31:32 +00:00
commit f298bf125a
24 changed files with 281 additions and 221 deletions

View file

@ -15,7 +15,7 @@ type CreateUserOption struct {
FullName string `json:"full_name" binding:"MaxSize(100)"`
// required: true
// swagger:strfmt email
Email string `json:"email" binding:"Required;Email;MaxSize(254)"`
Email string `json:"email" binding:"Required;EmailForAdmin;MaxSize(254)"`
Password string `json:"password" binding:"MaxSize(255)"`
MustChangePassword *bool `json:"must_change_password"`
SendNotify bool `json:"send_notify"`

View file

@ -7,7 +7,7 @@ package structs
// Email an email address belonging to a user
type Email struct {
// swagger:strfmt email
Email string `json:"email"`
Email string `json:"email" binding:"EmailWithAllowedDomain"`
Verified bool `json:"verified"`
Primary bool `json:"primary"`
UserID int64 `json:"user_id"`

View file

@ -26,6 +26,8 @@ const (
ErrUsername = "UsernameError"
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
// ErrEmail is returned when an email address is invalid
ErrEmail = "Email"
)
// AddBindingRules adds additional binding rules
@ -38,6 +40,7 @@ func AddBindingRules() {
addGlobOrRegexPatternRule()
addUsernamePatternRule()
addValidGroupTeamMapRule()
addEmailBindingRules()
}
func addGitRefNameBindingRule() {
@ -185,6 +188,34 @@ func addValidGroupTeamMapRule() {
})
}
func addEmailBindingRules() {
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
return strings.HasPrefix(rule, "EmailWithAllowedDomain")
},
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
if err := ValidateEmail(fmt.Sprintf("%v", val)); err != nil {
errs.Add([]string{name}, ErrEmail, err.Error())
return false, errs
}
return true, errs
},
})
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
return strings.HasPrefix(rule, "EmailForAdmin")
},
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
if err := ValidateEmailForAdmin(fmt.Sprintf("%v", val)); err != nil {
errs.Add([]string{name}, ErrEmail, err.Error())
return false, errs
}
return true, errs
},
})
}
func portOnly(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {

131
modules/validation/email.go Normal file
View file

@ -0,0 +1,131 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package validation
import (
"fmt"
"net/mail"
"regexp"
"strings"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob"
)
// ErrEmailNotActivated e-mail address has not been activated error
var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated")
// ErrEmailCharIsNotSupported e-mail address contains unsupported character
type ErrEmailCharIsNotSupported struct {
Email string
}
// IsErrEmailCharIsNotSupported checks if an error is an ErrEmailCharIsNotSupported
func IsErrEmailCharIsNotSupported(err error) bool {
_, ok := err.(ErrEmailCharIsNotSupported)
return ok
}
func (err ErrEmailCharIsNotSupported) Error() string {
return fmt.Sprintf("e-mail address contains unsupported character [email: %s]", err.Email)
}
// ErrEmailInvalid represents an error where the email address does not comply with RFC 5322
// or has a leading '-' character
type ErrEmailInvalid struct {
Email string
}
// IsErrEmailInvalid checks if an error is an ErrEmailInvalid
func IsErrEmailInvalid(err error) bool {
_, ok := err.(ErrEmailInvalid)
return ok
}
func (err ErrEmailInvalid) Error() string {
return fmt.Sprintf("e-mail invalid [email: %s]", err.Email)
}
func (err ErrEmailInvalid) Unwrap() error {
return util.ErrInvalidArgument
}
var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
// check if email is a valid address with allowed domain
func ValidateEmail(email string) error {
if err := validateEmailBasic(email); err != nil {
return err
}
return validateEmailDomain(email)
}
// check if email is a valid address when admins manually add or edit users
func ValidateEmailForAdmin(email string) error {
return validateEmailBasic(email)
// In this case we do not need to check the email domain
}
// validateEmailBasic checks whether the email complies with the rules
func validateEmailBasic(email string) error {
if len(email) == 0 {
return ErrEmailInvalid{email}
}
if !emailRegexp.MatchString(email) {
return ErrEmailCharIsNotSupported{email}
}
if email[0] == '-' {
return ErrEmailInvalid{email}
}
if _, err := mail.ParseAddress(email); err != nil {
return ErrEmailInvalid{email}
}
return nil
}
func validateEmailDomain(email string) error {
if !IsEmailDomainAllowed(email) {
return ErrEmailInvalid{email}
}
return nil
}
func IsEmailDomainAllowed(email string) bool {
if len(setting.Service.EmailDomainAllowList) == 0 {
return !isEmailDomainListed(setting.Service.EmailDomainBlockList, email)
}
return isEmailDomainListed(setting.Service.EmailDomainAllowList, email)
}
// isEmailDomainListed checks whether the domain of an email address
// matches a list of domains
func isEmailDomainListed(globs []glob.Glob, email string) bool {
if len(globs) == 0 {
return false
}
n := strings.LastIndex(email, "@")
if n <= 0 {
return false
}
domain := strings.ToLower(email[n+1:])
for _, g := range globs {
if g.Match(domain) {
return true
}
}
return false
}

View file

@ -0,0 +1,67 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package validation
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEmailAddressValidate(t *testing.T) {
kases := map[string]error{
"abc@gmail.com": nil,
"132@hotmail.com": nil,
"1-3-2@test.org": nil,
"1.3.2@test.org": nil,
"a_123@test.org.cn": nil,
`first.last@iana.org`: nil,
`first!last@iana.org`: nil,
`first#last@iana.org`: nil,
`first$last@iana.org`: nil,
`first%last@iana.org`: nil,
`first&last@iana.org`: nil,
`first'last@iana.org`: nil,
`first*last@iana.org`: nil,
`first+last@iana.org`: nil,
`first/last@iana.org`: nil,
`first=last@iana.org`: nil,
`first?last@iana.org`: nil,
`first^last@iana.org`: nil,
"first`last@iana.org": nil,
`first{last@iana.org`: nil,
`first|last@iana.org`: nil,
`first}last@iana.org`: nil,
`first~last@iana.org`: nil,
`first;last@iana.org`: ErrEmailCharIsNotSupported{`first;last@iana.org`},
".233@qq.com": ErrEmailInvalid{".233@qq.com"},
"!233@qq.com": nil,
"#233@qq.com": nil,
"$233@qq.com": nil,
"%233@qq.com": nil,
"&233@qq.com": nil,
"'233@qq.com": nil,
"*233@qq.com": nil,
"+233@qq.com": nil,
"-233@qq.com": ErrEmailInvalid{"-233@qq.com"},
"/233@qq.com": nil,
"=233@qq.com": nil,
"?233@qq.com": nil,
"^233@qq.com": nil,
"_233@qq.com": nil,
"`233@qq.com": nil,
"{233@qq.com": nil,
"|233@qq.com": nil,
"}233@qq.com": nil,
"~233@qq.com": nil,
";233@qq.com": ErrEmailCharIsNotSupported{";233@qq.com"},
"Foo <foo@bar.com>": ErrEmailCharIsNotSupported{"Foo <foo@bar.com>"},
string([]byte{0xE2, 0x84, 0xAA}): ErrEmailCharIsNotSupported{string([]byte{0xE2, 0x84, 0xAA})},
}
for kase, err := range kases {
t.Run(kase, func(t *testing.T) {
assert.EqualValues(t, err, ValidateEmail(kase))
})
}
}

View file

@ -10,8 +10,6 @@ import (
"strings"
"code.gitea.io/gitea/modules/setting"
"github.com/gobwas/glob"
)
var externalTrackerRegex = regexp.MustCompile(`({?)(?:user|repo|index)+?(}?)`)
@ -50,29 +48,6 @@ func IsValidSiteURL(uri string) bool {
return false
}
// IsEmailDomainListed checks whether the domain of an email address
// matches a list of domains
func IsEmailDomainListed(globs []glob.Glob, email string) bool {
if len(globs) == 0 {
return false
}
n := strings.LastIndex(email, "@")
if n <= 0 {
return false
}
domain := strings.ToLower(email[n+1:])
for _, g := range globs {
if g.Match(domain) {
return true
}
}
return false
}
// IsAPIURL checks if URL is current Gitea instance API URL
func IsAPIURL(uri string) bool {
return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(setting.AppURL+"api"))

View file

@ -143,6 +143,8 @@ func Validate(errs binding.Errors, data map[string]any, f any, l translation.Loc
}
case validation.ErrInvalidGroupTeamMap:
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
case validation.ErrEmail:
data["ErrorMsg"] = trName + l.TrString("form.email_error")
default:
msg := errs[0].Classification
if msg != "" && errs[0].Message != "" {