diff --git a/models/user/email_address.go b/models/user/email_address.go index ac8fa643c4..54667986ac 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -7,66 +7,17 @@ package user import ( "context" "fmt" - "net/mail" - "net/url" - "regexp" "strings" - "time" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/validation" - "github.com/gobwas/glob" "xorm.io/builder" ) -// 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) -} - -func (err ErrEmailCharIsNotSupported) Unwrap() error { - return util.ErrInvalidArgument -} - -// 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 -} - // ErrEmailAlreadyUsed represents a "EmailAlreadyUsed" kind of error. type ErrEmailAlreadyUsed struct { Email string @@ -158,22 +109,6 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error { return err } -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])?)*$") - -// ValidateEmail check if email is a valid & allowed address -func ValidateEmail(email string) error { - if err := validateEmailBasic(email); err != nil { - return err - } - return validateEmailDomain(email) -} - -// ValidateEmailForAdmin 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 -} - func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) { ea := &EmailAddress{} if has, err := db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(ea); err != nil { @@ -309,23 +244,6 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e return UpdateUserCols(ctx, user, "rands") } -// VerifyActiveEmailCode verifies active email code when active account -func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress { - if user := GetVerifyUser(ctx, code); user != nil { - // time limit code - prefix := code[:base.TimeLimitCodeLength] - data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands) - - if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) { - emailAddress := &EmailAddress{UID: user.ID, Email: email} - if has, _ := db.GetEngine(ctx).Get(emailAddress); has { - return emailAddress - } - } - } - return nil -} - // SearchEmailOrderBy is used to sort the results from SearchEmails() type SearchEmailOrderBy string @@ -464,70 +382,3 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate return committer.Commit() } - -// 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 -} - -// validateEmailDomain checks whether the email domain is allowed or blocked -func validateEmailDomain(email string) error { - if !IsEmailDomainAllowed(email) { - return ErrEmailInvalid{email} - } - - return nil -} - -func IsEmailDomainAllowed(email string) bool { - return IsEmailDomainAllowedInternal( - email, - setting.Service.EmailDomainAllowList, - setting.Service.EmailDomainBlockList, - setting.Federation.Enabled, - setting.AppURL) -} - -func IsEmailDomainAllowedInternal( - email string, - emailDomainAllowList []glob.Glob, - emailDomainBlockList []glob.Glob, - isFederation bool, - fqdn string, -) bool { - var result bool - - if len(emailDomainAllowList) == 0 { - result = !validation.IsEmailDomainListed(emailDomainBlockList, email) - } else if isFederation { - localFqdn, err := url.ParseRequestURI(fqdn) - if err != nil { - return false - } - globber, err := glob.Compile(localFqdn.Hostname(), ',') - if err != nil { - return false - } - emailDomainAllowList = append(emailDomainAllowList, globber) - result = validation.IsEmailDomainListed(emailDomainAllowList, email) - } else { - result = validation.IsEmailDomainListed(emailDomainAllowList, email) - } - return result -} diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index 96fae98ebb..7cd82a95fa 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -12,54 +12,10 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/optional" - "github.com/gobwas/glob" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestEmailDomainAllowList(t *testing.T) { - res := user_model.IsEmailDomainAllowed("someuser@localhost.localdomain") - assert.True(t, res) -} - -func TestEmailDomainAllowListInternal(t *testing.T) { - domain, _ := glob.Compile("domain.de", ',') - emailDomainAllowList := []glob.Glob{domain} - emailDomainBlockList := []glob.Glob{} - - res := user_model.IsEmailDomainAllowedInternal( - "user@repo.domain.de", - emailDomainAllowList, - emailDomainBlockList, - false, - "https://repo.domain.de") - assert.False(t, res) - - res = user_model.IsEmailDomainAllowedInternal( - "user@repo.domain.de", - emailDomainAllowList, - emailDomainBlockList, - true, - "xttps://repo") - assert.False(t, res) - - res = user_model.IsEmailDomainAllowedInternal( - "user@repo.Domain.de", - emailDomainAllowList, - emailDomainBlockList, - true, - "https://repo.domain.de") - assert.True(t, res) - - res = user_model.IsEmailDomainAllowedInternal( - "user@repo.domain.de", - emailDomainAllowList, - emailDomainBlockList, - true, - "https://repo.domain.de") - assert.True(t, res) -} - func TestGetEmailAddresses(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) @@ -173,64 +129,6 @@ func TestListEmails(t *testing.T) { assert.Len(t, emails, 5) assert.Greater(t, count, int64(len(emails))) } - -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`: user_model.ErrEmailCharIsNotSupported{`first;last@iana.org`}, - ".233@qq.com": user_model.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": user_model.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": user_model.ErrEmailCharIsNotSupported{";233@qq.com"}, - "Foo ": user_model.ErrEmailCharIsNotSupported{"Foo "}, - string([]byte{0xE2, 0x84, 0xAA}): user_model.ErrEmailCharIsNotSupported{string([]byte{0xE2, 0x84, 0xAA})}, - } - for kase, err := range kases { - t.Run(kase, func(t *testing.T) { - assert.EqualValues(t, err, user_model.ValidateEmail(kase)) - }) - } -} - func TestGetActivatedEmailAddresses(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/user/user_test.go b/models/user/user_test.go index 6a9e9db758..a705b87bd3 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -7,6 +7,7 @@ package user_test import ( "context" "crypto/rand" + "encoding/hex" "fmt" "strings" "testing" @@ -21,7 +22,10 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -321,7 +325,7 @@ func TestCreateUserInvalidEmail(t *testing.T) { err := user_model.CreateUser(db.DefaultContext, user) require.Error(t, err) - assert.True(t, user_model.IsErrEmailCharIsNotSupported(err)) + assert.True(t, validation.IsErrEmailCharIsNotSupported(err)) } func TestCreateUserEmailAlreadyUsed(t *testing.T) { @@ -603,6 +607,69 @@ func Test_ValidateUser(t *testing.T) { } } +func TestGenerateEmailAuthorizationCode(t *testing.T) { + defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 2)() + require.NoError(t, unittest.PrepareTestDatabase()) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation) + require.NoError(t, err) + + lookupKey, validator, ok := strings.Cut(code, ":") + assert.True(t, ok) + + rawValidator, err := hex.DecodeString(validator) + require.NoError(t, err) + + authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation) + require.NoError(t, err) + assert.False(t, authToken.IsExpired()) + assert.EqualValues(t, authToken.HashedValidator, auth.HashValidator(rawValidator)) + + authToken.Expiry = authToken.Expiry.Add(-int64(setting.Service.ActiveCodeLives) * 60) + assert.True(t, authToken.IsExpired()) +} + +func TestVerifyUserAuthorizationToken(t *testing.T) { + defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 2)() + require.NoError(t, unittest.PrepareTestDatabase()) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation) + require.NoError(t, err) + + lookupKey, _, ok := strings.Cut(code, ":") + assert.True(t, ok) + + t.Run("Wrong purpose", func(t *testing.T) { + u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.PasswordReset, false) + require.NoError(t, err) + assert.Nil(t, u) + }) + + t.Run("No delete", func(t *testing.T) { + u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, false) + require.NoError(t, err) + assert.EqualValues(t, user.ID, u.ID) + + authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation) + require.NoError(t, err) + assert.NotNil(t, authToken) + }) + + t.Run("Delete", func(t *testing.T) { + u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, true) + require.NoError(t, err) + assert.EqualValues(t, user.ID, u.ID) + + authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation) + require.ErrorIs(t, err, util.ErrNotExist) + assert.Nil(t, authToken) + }) +} + func Test_NormalizeUserFromEmail(t *testing.T) { oldSetting := setting.Service.AllowDotsInUsernames defer func() {