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.

Merge pull request 'fix: 15 November 2024 security fixes batch' (#5974) from earl-warren/forgejo:wip-security-15-11 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5974
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>

+951 -288
+23 -3
models/auth/auth_token.go
··· 15 15 "code.gitea.io/gitea/modules/util" 16 16 ) 17 17 18 + type AuthorizationPurpose string 19 + 20 + var ( 21 + // Used to store long term authorization tokens. 22 + LongTermAuthorization AuthorizationPurpose = "long_term_authorization" 23 + 24 + // Used to activate a user account. 25 + UserActivation AuthorizationPurpose = "user_activation" 26 + 27 + // Used to reset the password. 28 + PasswordReset AuthorizationPurpose = "password_reset" 29 + ) 30 + 31 + // Used to activate the specified email address for a user. 32 + func EmailActivation(email string) AuthorizationPurpose { 33 + return AuthorizationPurpose("email_activation:" + email) 34 + } 35 + 18 36 // AuthorizationToken represents a authorization token to a user. 19 37 type AuthorizationToken struct { 20 38 ID int64 `xorm:"pk autoincr"` 21 39 UID int64 `xorm:"INDEX"` 22 40 LookupKey string `xorm:"INDEX UNIQUE"` 23 41 HashedValidator string 42 + Purpose AuthorizationPurpose `xorm:"NOT NULL"` 24 43 Expiry timeutil.TimeStamp 25 44 } 26 45 ··· 41 60 // GenerateAuthToken generates a new authentication token for the given user. 42 61 // It returns the lookup key and validator values that should be passed to the 43 62 // user via a long-term cookie. 44 - func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeStamp) (lookupKey, validator string, err error) { 63 + func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeStamp, purpose AuthorizationPurpose) (lookupKey, validator string, err error) { 45 64 // Request 64 random bytes. The first 32 bytes will be used for the lookupKey 46 65 // and the other 32 bytes will be used for the validator. 47 66 rBytes, err := util.CryptoRandomBytes(64) ··· 56 75 Expiry: expiry, 57 76 LookupKey: lookupKey, 58 77 HashedValidator: HashValidator(rBytes[32:]), 78 + Purpose: purpose, 59 79 }) 60 80 return lookupKey, validator, err 61 81 } 62 82 63 83 // FindAuthToken will find a authorization token via the lookup key. 64 - func FindAuthToken(ctx context.Context, lookupKey string) (*AuthorizationToken, error) { 84 + func FindAuthToken(ctx context.Context, lookupKey string, purpose AuthorizationPurpose) (*AuthorizationToken, error) { 65 85 var authToken AuthorizationToken 66 - has, err := db.GetEngine(ctx).Where("lookup_key = ?", lookupKey).Get(&authToken) 86 + has, err := db.GetEngine(ctx).Where("lookup_key = ? AND purpose = ?", lookupKey, purpose).Get(&authToken) 67 87 if err != nil { 68 88 return nil, err 69 89 } else if !has {
+2
models/forgejo_migrations/migrate.go
··· 84 84 NewMigration("Add `legacy` to `web_authn_credential` table", AddLegacyToWebAuthnCredential), 85 85 // v23 -> v24 86 86 NewMigration("Add `delete_branch_after_merge` to `auto_merge` table", AddDeleteBranchAfterMergeToAutoMerge), 87 + // v24 -> v25 88 + NewMigration("Add `purpose` column to `forgejo_auth_token` table", AddPurposeToForgejoAuthToken), 87 89 } 88 90 89 91 // GetCurrentDBVersion returns the current Forgejo database version.
+19
models/forgejo_migrations/v24.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package forgejo_migrations //nolint:revive 5 + 6 + import "xorm.io/xorm" 7 + 8 + func AddPurposeToForgejoAuthToken(x *xorm.Engine) error { 9 + type ForgejoAuthToken struct { 10 + ID int64 `xorm:"pk autoincr"` 11 + Purpose string `xorm:"NOT NULL"` 12 + } 13 + if err := x.Sync(new(ForgejoAuthToken)); err != nil { 14 + return err 15 + } 16 + 17 + _, err := x.Exec("UPDATE `forgejo_auth_token` SET purpose = 'long_term_authorization' WHERE purpose = ''") 18 + return err 19 + }
+30
models/repo/TestSearchRepositoryIDsByCondition/repository.yml
··· 1 + - 2 + id: 1001 3 + owner_id: 33 4 + owner_name: user33 5 + lower_name: repo1001 6 + name: repo1001 7 + default_branch: main 8 + num_watches: 0 9 + num_stars: 0 10 + num_forks: 0 11 + num_issues: 0 12 + num_closed_issues: 0 13 + num_pulls: 0 14 + num_closed_pulls: 0 15 + num_milestones: 0 16 + num_closed_milestones: 0 17 + num_projects: 0 18 + num_closed_projects: 0 19 + is_private: false 20 + is_empty: false 21 + is_archived: false 22 + is_mirror: false 23 + status: 0 24 + is_fork: false 25 + fork_id: 0 26 + is_template: false 27 + template_id: 0 28 + size: 0 29 + is_fsck_enabled: true 30 + close_issues_via_commit_in_any_branch: false
+6 -4
models/repo/fork.go
··· 7 7 "context" 8 8 9 9 "code.gitea.io/gitea/models/db" 10 + "code.gitea.io/gitea/models/unit" 10 11 user_model "code.gitea.io/gitea/models/user" 11 12 12 13 "xorm.io/builder" ··· 54 55 return &forkedRepo, nil 55 56 } 56 57 57 - // GetForks returns all the forks of the repository 58 - func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions) ([]*Repository, error) { 59 - sess := db.GetEngine(ctx) 58 + // GetForks returns all the forks of the repository that are visible to the user. 59 + func GetForks(ctx context.Context, repo *Repository, user *user_model.User, listOptions db.ListOptions) ([]*Repository, int64, error) { 60 + sess := db.GetEngine(ctx).Where(AccessibleRepositoryCondition(user, unit.TypeInvalid)) 60 61 61 62 var forks []*Repository 62 63 if listOptions.Page == 0 { ··· 66 67 sess = db.SetSessionPagination(sess, &listOptions) 67 68 } 68 69 69 - return forks, sess.Find(&forks, &Repository{ForkID: repo.ID}) 70 + count, err := sess.FindAndCount(&forks, &Repository{ForkID: repo.ID}) 71 + return forks, count, err 70 72 } 71 73 72 74 // IncrementRepoForkNum increment repository fork number
+2 -5
models/repo/repo_list.go
··· 641 641 // 1. Be able to see all non-private repositories that either: 642 642 cond = cond.Or(builder.And( 643 643 builder.Eq{"`repository`.is_private": false}, 644 - // 2. Aren't in an private organisation or limited organisation if we're not logged in 644 + // 2. Aren't in an private organisation/user or limited organisation/user if the doer is not logged in. 645 645 builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where( 646 - builder.And( 647 - builder.Eq{"type": user_model.UserTypeOrganization}, 648 - builder.In("visibility", orgVisibilityLimit)), 649 - )))) 646 + builder.In("visibility", orgVisibilityLimit))))) 650 647 } 651 648 652 649 if user != nil {
+45
models/repo/repo_list_test.go
··· 4 4 package repo_test 5 5 6 6 import ( 7 + "path/filepath" 8 + "slices" 7 9 "strings" 8 10 "testing" 9 11 10 12 "code.gitea.io/gitea/models/db" 11 13 repo_model "code.gitea.io/gitea/models/repo" 12 14 "code.gitea.io/gitea/models/unittest" 15 + "code.gitea.io/gitea/models/user" 13 16 "code.gitea.io/gitea/modules/optional" 17 + "code.gitea.io/gitea/modules/setting" 18 + "code.gitea.io/gitea/modules/structs" 14 19 15 20 "github.com/stretchr/testify/assert" 16 21 "github.com/stretchr/testify/require" ··· 403 408 }) 404 409 } 405 410 } 411 + 412 + func TestSearchRepositoryIDsByCondition(t *testing.T) { 413 + defer unittest.OverrideFixtures( 414 + unittest.FixturesOptions{ 415 + Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"), 416 + Base: setting.AppWorkPath, 417 + Dirs: []string{"models/repo/TestSearchRepositoryIDsByCondition/"}, 418 + }, 419 + )() 420 + require.NoError(t, unittest.PrepareTestDatabase()) 421 + // Sanity check of the database 422 + limitedUser := unittest.AssertExistsAndLoadBean(t, &user.User{ID: 33, Visibility: structs.VisibleTypeLimited}) 423 + unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1001, OwnerID: limitedUser.ID}) 424 + 425 + testCases := []struct { 426 + user *user.User 427 + repoIDs []int64 428 + }{ 429 + { 430 + user: nil, 431 + repoIDs: []int64{1, 4, 8, 9, 10, 11, 12, 14, 17, 18, 21, 23, 25, 27, 29, 32, 33, 34, 35, 36, 37, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 57, 58, 60, 61, 62, 1059}, 432 + }, 433 + { 434 + user: unittest.AssertExistsAndLoadBean(t, &user.User{ID: 4}), 435 + repoIDs: []int64{1, 3, 4, 8, 9, 10, 11, 12, 14, 17, 18, 21, 23, 25, 27, 29, 32, 33, 34, 35, 36, 37, 38, 40, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 57, 58, 60, 61, 62, 1001, 1059}, 436 + }, 437 + { 438 + user: unittest.AssertExistsAndLoadBean(t, &user.User{ID: 5}), 439 + repoIDs: []int64{1, 4, 8, 9, 10, 11, 12, 14, 17, 18, 21, 23, 25, 27, 29, 32, 33, 34, 35, 36, 37, 38, 40, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 57, 58, 60, 61, 62, 1001, 1059}, 440 + }, 441 + } 442 + 443 + for _, testCase := range testCases { 444 + repoIDs, err := repo_model.FindUserCodeAccessibleRepoIDs(db.DefaultContext, testCase.user) 445 + require.NoError(t, err) 446 + 447 + slices.Sort(repoIDs) 448 + assert.EqualValues(t, testCase.repoIDs, repoIDs) 449 + } 450 + }
-19
models/user/email_address.go
··· 8 8 "context" 9 9 "fmt" 10 10 "strings" 11 - "time" 12 11 13 12 "code.gitea.io/gitea/models/db" 14 - "code.gitea.io/gitea/modules/base" 15 13 "code.gitea.io/gitea/modules/log" 16 14 "code.gitea.io/gitea/modules/optional" 17 15 "code.gitea.io/gitea/modules/setting" ··· 244 242 return err 245 243 } 246 244 return UpdateUserCols(ctx, user, "rands") 247 - } 248 - 249 - // VerifyActiveEmailCode verifies active email code when active account 250 - func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress { 251 - if user := GetVerifyUser(ctx, code); user != nil { 252 - // time limit code 253 - prefix := code[:base.TimeLimitCodeLength] 254 - data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands) 255 - 256 - if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) { 257 - emailAddress := &EmailAddress{UID: user.ID, Email: email} 258 - if has, _ := db.GetEngine(ctx).Get(emailAddress); has { 259 - return emailAddress 260 - } 261 - } 262 - } 263 - return nil 264 245 } 265 246 266 247 // SearchEmailOrderBy is used to sort the results from SearchEmails()
+46 -30
models/user/user.go
··· 7 7 8 8 import ( 9 9 "context" 10 + "crypto/subtle" 10 11 "encoding/hex" 12 + "errors" 11 13 "fmt" 12 14 "net/mail" 13 15 "net/url" ··· 318 320 return setting.AppSubURL + "/org/" + url.PathEscape(u.Name) 319 321 } 320 322 321 - // GenerateEmailActivateCode generates an activate code based on user information and given e-mail. 322 - func (u *User) GenerateEmailActivateCode(email string) string { 323 - code := base.CreateTimeLimitCode( 324 - fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands), 325 - setting.Service.ActiveCodeLives, time.Now(), nil) 326 - 327 - // Add tail hex username 328 - code += hex.EncodeToString([]byte(u.LowerName)) 329 - return code 323 + // GenerateEmailAuthorizationCode generates an activation code based for the user for the specified purpose. 324 + // The standard expiry is ActiveCodeLives minutes. 325 + func (u *User) GenerateEmailAuthorizationCode(ctx context.Context, purpose auth.AuthorizationPurpose) (string, error) { 326 + lookup, validator, err := auth.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(setting.Service.ActiveCodeLives)*60), purpose) 327 + if err != nil { 328 + return "", err 329 + } 330 + return lookup + ":" + validator, nil 330 331 } 331 332 332 333 // GetUserFollowers returns range of user's followers. ··· 838 839 return count 839 840 } 840 841 841 - // GetVerifyUser get user by verify code 842 - func GetVerifyUser(ctx context.Context, code string) (user *User) { 843 - if len(code) <= base.TimeLimitCodeLength { 844 - return nil 842 + // VerifyUserActiveCode verifies that the code is valid for the given purpose for this user. 843 + // If delete is specified, the token will be deleted. 844 + func VerifyUserAuthorizationToken(ctx context.Context, code string, purpose auth.AuthorizationPurpose, delete bool) (*User, error) { 845 + lookupKey, validator, found := strings.Cut(code, ":") 846 + if !found { 847 + return nil, nil 845 848 } 846 849 847 - // use tail hex username query user 848 - hexStr := code[base.TimeLimitCodeLength:] 849 - if b, err := hex.DecodeString(hexStr); err == nil { 850 - if user, err = GetUserByName(ctx, string(b)); user != nil { 851 - return user 850 + authToken, err := auth.FindAuthToken(ctx, lookupKey, purpose) 851 + if err != nil { 852 + if errors.Is(err, util.ErrNotExist) { 853 + return nil, nil 852 854 } 853 - log.Error("user.getVerifyUser: %v", err) 855 + return nil, err 856 + } 857 + 858 + if authToken.IsExpired() { 859 + return nil, auth.DeleteAuthToken(ctx, authToken) 860 + } 861 + 862 + rawValidator, err := hex.DecodeString(validator) 863 + if err != nil { 864 + return nil, err 854 865 } 855 866 856 - return nil 857 - } 867 + if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 { 868 + return nil, errors.New("validator doesn't match") 869 + } 858 870 859 - // VerifyUserActiveCode verifies active code when active account 860 - func VerifyUserActiveCode(ctx context.Context, code string) (user *User) { 861 - if user = GetVerifyUser(ctx, code); user != nil { 862 - // time limit code 863 - prefix := code[:base.TimeLimitCodeLength] 864 - data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands) 865 - if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) { 866 - return user 871 + u, err := GetUserByID(ctx, authToken.UID) 872 + if err != nil { 873 + if IsErrUserNotExist(err) { 874 + return nil, nil 867 875 } 876 + return nil, err 868 877 } 869 - return nil 878 + 879 + if delete { 880 + if err := auth.DeleteAuthToken(ctx, authToken); err != nil { 881 + return nil, err 882 + } 883 + } 884 + 885 + return u, nil 870 886 } 871 887 872 888 // ValidateUser check if user is valid to insert / update into database
+66
models/user/user_test.go
··· 7 7 import ( 8 8 "context" 9 9 "crypto/rand" 10 + "encoding/hex" 10 11 "fmt" 11 12 "strings" 12 13 "testing" ··· 21 22 "code.gitea.io/gitea/modules/optional" 22 23 "code.gitea.io/gitea/modules/setting" 23 24 "code.gitea.io/gitea/modules/structs" 25 + "code.gitea.io/gitea/modules/test" 24 26 "code.gitea.io/gitea/modules/timeutil" 27 + "code.gitea.io/gitea/modules/util" 25 28 "code.gitea.io/gitea/modules/validation" 26 29 "code.gitea.io/gitea/tests" 27 30 ··· 700 703 assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f)) 701 704 } 702 705 } 706 + 707 + func TestGenerateEmailAuthorizationCode(t *testing.T) { 708 + defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 2)() 709 + require.NoError(t, unittest.PrepareTestDatabase()) 710 + 711 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 712 + 713 + code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation) 714 + require.NoError(t, err) 715 + 716 + lookupKey, validator, ok := strings.Cut(code, ":") 717 + assert.True(t, ok) 718 + 719 + rawValidator, err := hex.DecodeString(validator) 720 + require.NoError(t, err) 721 + 722 + authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation) 723 + require.NoError(t, err) 724 + assert.False(t, authToken.IsExpired()) 725 + assert.EqualValues(t, authToken.HashedValidator, auth.HashValidator(rawValidator)) 726 + 727 + authToken.Expiry = authToken.Expiry.Add(-int64(setting.Service.ActiveCodeLives) * 60) 728 + assert.True(t, authToken.IsExpired()) 729 + } 730 + 731 + func TestVerifyUserAuthorizationToken(t *testing.T) { 732 + defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 2)() 733 + require.NoError(t, unittest.PrepareTestDatabase()) 734 + 735 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 736 + 737 + code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation) 738 + require.NoError(t, err) 739 + 740 + lookupKey, _, ok := strings.Cut(code, ":") 741 + assert.True(t, ok) 742 + 743 + t.Run("Wrong purpose", func(t *testing.T) { 744 + u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.PasswordReset, false) 745 + require.NoError(t, err) 746 + assert.Nil(t, u) 747 + }) 748 + 749 + t.Run("No delete", func(t *testing.T) { 750 + u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, false) 751 + require.NoError(t, err) 752 + assert.EqualValues(t, user.ID, u.ID) 753 + 754 + authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation) 755 + require.NoError(t, err) 756 + assert.NotNil(t, authToken) 757 + }) 758 + 759 + t.Run("Delete", func(t *testing.T) { 760 + u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, true) 761 + require.NoError(t, err) 762 + assert.EqualValues(t, user.ID, u.ID) 763 + 764 + authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation) 765 + require.ErrorIs(t, err, util.ErrNotExist) 766 + assert.Nil(t, authToken) 767 + }) 768 + }
-66
modules/base/tool.go
··· 4 4 package base 5 5 6 6 import ( 7 - "crypto/hmac" 8 - "crypto/sha1" 9 7 "crypto/sha256" 10 - "crypto/subtle" 11 8 "encoding/base64" 12 9 "encoding/hex" 13 10 "errors" 14 11 "fmt" 15 - "hash" 16 12 "os" 17 13 "path/filepath" 18 14 "runtime" 19 15 "strconv" 20 16 "strings" 21 - "time" 22 17 "unicode/utf8" 23 18 24 19 "code.gitea.io/gitea/modules/git" 25 20 "code.gitea.io/gitea/modules/log" 26 - "code.gitea.io/gitea/modules/setting" 27 21 28 22 "github.com/dustin/go-humanize" 29 23 ) ··· 52 46 return username, password, nil 53 47 } 54 48 return "", "", errors.New("invalid basic authentication") 55 - } 56 - 57 - // VerifyTimeLimitCode verify time limit code 58 - func VerifyTimeLimitCode(now time.Time, data string, minutes int, code string) bool { 59 - if len(code) <= 18 { 60 - return false 61 - } 62 - 63 - startTimeStr := code[:12] 64 - aliveTimeStr := code[12:18] 65 - aliveTime, _ := strconv.Atoi(aliveTimeStr) // no need to check err, if anything wrong, the following code check will fail soon 66 - 67 - // check code 68 - retCode := CreateTimeLimitCode(data, aliveTime, startTimeStr, nil) 69 - if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 { 70 - retCode = CreateTimeLimitCode(data, aliveTime, startTimeStr, sha1.New()) // TODO: this is only for the support of legacy codes, remove this in/after 1.23 71 - if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 { 72 - return false 73 - } 74 - } 75 - 76 - // check time is expired or not: startTime <= now && now < startTime + minutes 77 - startTime, _ := time.ParseInLocation("200601021504", startTimeStr, time.Local) 78 - return (startTime.Before(now) || startTime.Equal(now)) && now.Before(startTime.Add(time.Minute*time.Duration(minutes))) 79 - } 80 - 81 - // TimeLimitCodeLength default value for time limit code 82 - const TimeLimitCodeLength = 12 + 6 + 40 83 - 84 - // CreateTimeLimitCode create a time-limited code. 85 - // Format: 12 length date time string + 6 minutes string (not used) + 40 hash string, some other code depends on this fixed length 86 - // If h is nil, then use the default hmac hash. 87 - func CreateTimeLimitCode[T time.Time | string](data string, minutes int, startTimeGeneric T, h hash.Hash) string { 88 - const format = "200601021504" 89 - 90 - var start time.Time 91 - var startTimeAny any = startTimeGeneric 92 - if t, ok := startTimeAny.(time.Time); ok { 93 - start = t 94 - } else { 95 - var err error 96 - start, err = time.ParseInLocation(format, startTimeAny.(string), time.Local) 97 - if err != nil { 98 - return "" // return an invalid code because the "parse" failed 99 - } 100 - } 101 - startStr := start.Format(format) 102 - end := start.Add(time.Minute * time.Duration(minutes)) 103 - 104 - if h == nil { 105 - h = hmac.New(sha1.New, setting.GetGeneralTokenSigningSecret()) 106 - } 107 - _, _ = fmt.Fprintf(h, "%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, end.Format(format), minutes) 108 - encoded := hex.EncodeToString(h.Sum(nil)) 109 - 110 - code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded) 111 - if len(code) != TimeLimitCodeLength { 112 - panic("there is a hard requirement for the length of time-limited code") // it shouldn't happen 113 - } 114 - return code 115 49 } 116 50 117 51 // FileSize calculates the file size and generate user-friendly string.
-57
modules/base/tool_test.go
··· 4 4 package base 5 5 6 6 import ( 7 - "crypto/sha1" 8 - "fmt" 9 7 "testing" 10 - "time" 11 - 12 - "code.gitea.io/gitea/modules/setting" 13 - "code.gitea.io/gitea/modules/test" 14 8 15 9 "github.com/stretchr/testify/assert" 16 10 "github.com/stretchr/testify/require" ··· 44 38 45 39 _, _, err = BasicAuthDecode("YWxpY2U=") // "alice", no colon 46 40 require.Error(t, err) 47 - } 48 - 49 - func TestVerifyTimeLimitCode(t *testing.T) { 50 - defer test.MockVariableValue(&setting.InstallLock, true)() 51 - initGeneralSecret := func(secret string) { 52 - setting.InstallLock = true 53 - setting.CfgProvider, _ = setting.NewConfigProviderFromData(fmt.Sprintf(` 54 - [oauth2] 55 - JWT_SECRET = %s 56 - `, secret)) 57 - setting.LoadCommonSettings() 58 - } 59 - 60 - initGeneralSecret("KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko") 61 - now := time.Now() 62 - 63 - t.Run("TestGenericParameter", func(t *testing.T) { 64 - time2000 := time.Date(2000, 1, 2, 3, 4, 5, 0, time.Local) 65 - assert.Equal(t, "2000010203040000026fa5221b2731b7cf80b1b506f5e39e38c115fee5", CreateTimeLimitCode("test-sha1", 2, time2000, sha1.New())) 66 - assert.Equal(t, "2000010203040000026fa5221b2731b7cf80b1b506f5e39e38c115fee5", CreateTimeLimitCode("test-sha1", 2, "200001020304", sha1.New())) 67 - assert.Equal(t, "2000010203040000024842227a2f87041ff82025199c0187410a9297bf", CreateTimeLimitCode("test-hmac", 2, time2000, nil)) 68 - assert.Equal(t, "2000010203040000024842227a2f87041ff82025199c0187410a9297bf", CreateTimeLimitCode("test-hmac", 2, "200001020304", nil)) 69 - }) 70 - 71 - t.Run("TestInvalidCode", func(t *testing.T) { 72 - assert.False(t, VerifyTimeLimitCode(now, "data", 2, "")) 73 - assert.False(t, VerifyTimeLimitCode(now, "data", 2, "invalid code")) 74 - }) 75 - 76 - t.Run("TestCreateAndVerify", func(t *testing.T) { 77 - code := CreateTimeLimitCode("data", 2, now, nil) 78 - assert.False(t, VerifyTimeLimitCode(now.Add(-time.Minute), "data", 2, code)) // not started yet 79 - assert.True(t, VerifyTimeLimitCode(now, "data", 2, code)) 80 - assert.True(t, VerifyTimeLimitCode(now.Add(time.Minute), "data", 2, code)) 81 - assert.False(t, VerifyTimeLimitCode(now.Add(time.Minute), "DATA", 2, code)) // invalid data 82 - assert.False(t, VerifyTimeLimitCode(now.Add(2*time.Minute), "data", 2, code)) // expired 83 - }) 84 - 85 - t.Run("TestDifferentSecret", func(t *testing.T) { 86 - // use another secret to ensure the code is invalid for different secret 87 - verifyDataCode := func(c string) bool { 88 - return VerifyTimeLimitCode(now, "data", 2, c) 89 - } 90 - code1 := CreateTimeLimitCode("data", 2, now, sha1.New()) 91 - code2 := CreateTimeLimitCode("data", 2, now, nil) 92 - assert.True(t, verifyDataCode(code1)) 93 - assert.True(t, verifyDataCode(code2)) 94 - initGeneralSecret("000_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko") 95 - assert.False(t, verifyDataCode(code1)) 96 - assert.False(t, verifyDataCode(code2)) 97 - }) 98 41 } 99 42 100 43 func TestFileSize(t *testing.T) {
+4 -4
modules/markup/sanitizer.go
··· 97 97 policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ref-issue( ref-external-issue)?|mention)$`)).OnElements("a") 98 98 99 99 // Allow classes for task lists 100 - policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li") 100 + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^task-list-item$`)).OnElements("li") 101 101 102 102 // Allow classes for org mode list item status. 103 103 policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li") ··· 106 106 policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i") 107 107 108 108 // Allow classes for emojis 109 - policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") 109 + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img") 110 110 111 111 // Allow icons, emojis, chroma syntax and keyword markup on span 112 112 policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") ··· 123 123 policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div") 124 124 policy.AllowAttrs("data-line-number").Matching(regexp.MustCompile("^[0-9]+$")).OnElements("span") 125 125 policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span") 126 - policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table") 126 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview$")).OnElements("table") 127 127 policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td") 128 128 policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button") 129 129 policy.AllowAttrs("title").OnElements("button") 130 130 policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span") 131 131 policy.AllowAttrs("data-tooltip-content").OnElements("span") 132 - policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a") 132 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^muted|(text black)$")).OnElements("a") 133 133 policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui warning message tw-text-left$")).OnElements("div") 134 134 135 135 // Allow generally safe attributes
+8
release-notes/5974.md
··· 1 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/1ce33aa38d1d258d14523ff2c7c2dbf339f22b74) it was possible to use a token sent via email for secondary email validation to reset the password instead. In other words, a token sent for a given action (registration, password reset or secondary email validation) could be used to perform a different action. It is no longer possible to use a token for an action that is different from its original purpose. 2 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/061abe60045212acf8c3f5c49b5cc758b4cbcde9) a fork of a public repository would show in the list of forks, even if its owner was not a public user or organization. Such a fork is now hidden from the list of forks of the public repository. 3 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/3e3ef76808100cb1c853378733d0f6a910324ac6) the members of an organization team with read access to a repository (e.g. to read issues) but no read access to the code could read the RSS or atom feeds which include the commit activity. Reading the RSS or atom feeds is now denied unless the team has read permissions on the code. 4 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/9508aa7713632ed40124a933d91d5766cf2369c2) the tokens used when [replying by email to issues or pull requests](https://forgejo.org/docs/v9.0/user/incoming/) were weaker than the [rfc2104 recommendations](https://datatracker.ietf.org/doc/html/rfc2104#section-5). The tokens are now truncated to 128 bits instead of 80 bits. It is no longer possible to reply to emails sent before the upgrade because the weaker tokens are invalid. 5 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/786dfc7fb81ee76d4292ca5fcb33e6ea7bdccc29) a registered user could modify the update frequency of any push mirror (e.g. every 4h instead of every 8h). They are now only able to do that if they have administrative permissions on the repository. 6 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/e6bbecb02d47730d3cc630d419fe27ef2fb5cb39) it was possible to use basic authorization (i.e. user:password) for requests to the API even when security keys were enrolled for a user. It is no longer possible, an application token must be used instead. 7 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/7067cc7da4f144cc8a2fd2ae6e5307e0465ace7f) some markup sanitation rules were not as strong as they could be (e.g. allowing `emoji somethingelse` as well as `emoji`). The rules are now stricter and do not allow for such cases. 8 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/b70196653f9d7d3b9d4e72d114e5cc6f472988c4) when Forgejo is configured to enable instance wide search (e.g. with [bleve](https://blevesearch.com/)), results found in the repositories of private or limited users were displayed to anonymous visitors. The results found in private or limited organizations were not displayed. The search results found in the repositories of private or limited user are no longer displayed to anonymous visitors.
+2 -2
routers/api/v1/repo/fork.go
··· 56 56 // "404": 57 57 // "$ref": "#/responses/notFound" 58 58 59 - forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx)) 59 + forks, total, err := repo_model.GetForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx)) 60 60 if err != nil { 61 61 ctx.Error(http.StatusInternalServerError, "GetForks", err) 62 62 return ··· 71 71 apiForks[i] = convert.ToRepo(ctx, fork, permission) 72 72 } 73 73 74 - ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumForks)) 74 + ctx.SetTotalCountHeader(total) 75 75 ctx.JSON(http.StatusOK, apiForks) 76 76 } 77 77
+46 -50
routers/web/auth/auth.go
··· 5 5 package auth 6 6 7 7 import ( 8 - "crypto/subtle" 9 - "encoding/hex" 10 8 "errors" 11 9 "fmt" 12 10 "net/http" ··· 63 61 return false, nil 64 62 } 65 63 66 - lookupKey, validator, found := strings.Cut(authCookie, ":") 67 - if !found { 68 - return false, nil 69 - } 70 - 71 - authToken, err := auth.FindAuthToken(ctx, lookupKey) 64 + u, err := user_model.VerifyUserAuthorizationToken(ctx, authCookie, auth.LongTermAuthorization, false) 72 65 if err != nil { 73 - if errors.Is(err, util.ErrNotExist) { 74 - return false, nil 75 - } 76 - return false, err 66 + return false, fmt.Errorf("VerifyUserAuthorizationToken: %w", err) 77 67 } 78 - 79 - if authToken.IsExpired() { 80 - err = auth.DeleteAuthToken(ctx, authToken) 81 - return false, err 82 - } 83 - 84 - rawValidator, err := hex.DecodeString(validator) 85 - if err != nil { 86 - return false, err 87 - } 88 - 89 - if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 { 90 - return false, nil 91 - } 92 - 93 - u, err := user_model.GetUserByID(ctx, authToken.UID) 94 - if err != nil { 95 - if !user_model.IsErrUserNotExist(err) { 96 - return false, fmt.Errorf("GetUserByID: %w", err) 97 - } 68 + if u == nil { 98 69 return false, nil 99 70 } 100 71 ··· 633 604 return false 634 605 } 635 606 636 - mailer.SendActivateAccountMail(ctx.Locale, u) 607 + if err := mailer.SendActivateAccountMail(ctx, u); err != nil { 608 + ctx.ServerError("SendActivateAccountMail", err) 609 + return false 610 + } 637 611 638 612 ctx.Data["IsSendRegisterMail"] = true 639 613 ctx.Data["Email"] = u.Email ··· 674 648 ctx.Data["ResendLimited"] = true 675 649 } else { 676 650 ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale) 677 - mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer) 651 + if err := mailer.SendActivateAccountMail(ctx, ctx.Doer); err != nil { 652 + ctx.ServerError("SendActivateAccountMail", err) 653 + return 654 + } 678 655 679 656 if err := ctx.Cache.Put(cacheKey+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil { 680 657 log.Error("Set cache(MailResendLimit) fail: %v", err) ··· 687 664 return 688 665 } 689 666 690 - user := user_model.VerifyUserActiveCode(ctx, code) 667 + user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, false) 668 + if err != nil { 669 + ctx.ServerError("VerifyUserAuthorizationToken", err) 670 + return 671 + } 672 + 691 673 // if code is wrong 692 674 if user == nil { 693 675 ctx.Data["IsCodeInvalid"] = true ··· 751 733 return 752 734 } 753 735 754 - user := user_model.VerifyUserActiveCode(ctx, code) 736 + user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, true) 737 + if err != nil { 738 + ctx.ServerError("VerifyUserAuthorizationToken", err) 739 + return 740 + } 741 + 755 742 // if code is wrong 756 743 if user == nil { 757 744 ctx.Data["IsCodeInvalid"] = true ··· 835 822 code := ctx.FormString("code") 836 823 emailStr := ctx.FormString("email") 837 824 838 - // Verify code. 839 - if email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); email != nil { 840 - if err := user_model.ActivateEmail(ctx, email); err != nil { 841 - ctx.ServerError("ActivateEmail", err) 842 - return 843 - } 825 + u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.EmailActivation(emailStr), true) 826 + if err != nil { 827 + ctx.ServerError("VerifyUserAuthorizationToken", err) 828 + return 829 + } 830 + if u == nil { 831 + ctx.Redirect(setting.AppSubURL + "/user/settings/account") 832 + return 833 + } 844 834 845 - log.Trace("Email activated: %s", email.Email) 846 - ctx.Flash.Success(ctx.Tr("settings.add_email_success")) 835 + email, err := user_model.GetEmailAddressOfUser(ctx, emailStr, u.ID) 836 + if err != nil { 837 + ctx.ServerError("GetEmailAddressOfUser", err) 838 + return 839 + } 847 840 848 - if u, err := user_model.GetUserByID(ctx, email.UID); err != nil { 849 - log.Warn("GetUserByID: %d", email.UID) 850 - } else { 851 - // Allow user to validate more emails 852 - _ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName) 853 - } 841 + if err := user_model.ActivateEmail(ctx, email); err != nil { 842 + ctx.ServerError("ActivateEmail", err) 843 + return 854 844 } 845 + 846 + log.Trace("Email activated: %s", email.Email) 847 + ctx.Flash.Success(ctx.Tr("settings.add_email_success")) 848 + 849 + // Allow user to validate more emails 850 + _ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName) 855 851 856 852 // FIXME: e-mail verification does not require the user to be logged in, 857 853 // so this could be redirecting to the login page.
+13 -5
routers/web/auth/password.go
··· 86 86 return 87 87 } 88 88 89 - mailer.SendResetPasswordMail(u) 89 + if err := mailer.SendResetPasswordMail(ctx, u); err != nil { 90 + ctx.ServerError("SendResetPasswordMail", err) 91 + return 92 + } 90 93 91 94 if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { 92 95 log.Error("Set cache(MailResendLimit) fail: %v", err) ··· 97 100 ctx.HTML(http.StatusOK, tplForgotPassword) 98 101 } 99 102 100 - func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFactor) { 103 + func commonResetPassword(ctx *context.Context, shouldDeleteToken bool) (*user_model.User, *auth.TwoFactor) { 101 104 code := ctx.FormString("code") 102 105 103 106 ctx.Data["Title"] = ctx.Tr("auth.reset_password") ··· 113 116 } 114 117 115 118 // Fail early, don't frustrate the user 116 - u := user_model.VerifyUserActiveCode(ctx, code) 119 + u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.PasswordReset, shouldDeleteToken) 120 + if err != nil { 121 + ctx.ServerError("VerifyUserAuthorizationToken", err) 122 + return nil, nil 123 + } 124 + 117 125 if u == nil { 118 126 ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true) 119 127 return nil, nil ··· 145 153 func ResetPasswd(ctx *context.Context) { 146 154 ctx.Data["IsResetForm"] = true 147 155 148 - commonResetPassword(ctx) 156 + commonResetPassword(ctx, false) 149 157 if ctx.Written() { 150 158 return 151 159 } ··· 155 163 156 164 // ResetPasswdPost response from account recovery request 157 165 func ResetPasswdPost(ctx *context.Context) { 158 - u, twofa := commonResetPassword(ctx) 166 + u, twofa := commonResetPassword(ctx, true) 159 167 if ctx.Written() { 160 168 return 161 169 }
+7 -9
routers/web/repo/setting/setting.go
··· 566 566 // as an error on the UI for this action 567 567 ctx.Data["Err_RepoName"] = nil 568 568 569 + m, err := selectPushMirrorByForm(ctx, form, repo) 570 + if err != nil { 571 + ctx.NotFound("", nil) 572 + return 573 + } 574 + 569 575 interval, err := time.ParseDuration(form.PushMirrorInterval) 570 576 if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { 571 577 ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{}) 572 578 return 573 579 } 574 580 575 - id, err := strconv.ParseInt(form.PushMirrorID, 10, 64) 576 - if err != nil { 577 - ctx.ServerError("UpdatePushMirrorIntervalPushMirrorID", err) 578 - return 579 - } 580 - m := &repo_model.PushMirror{ 581 - ID: id, 582 - Interval: interval, 583 - } 581 + m.Interval = interval 584 582 if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil { 585 583 ctx.ServerError("UpdatePushMirrorInterval", err) 586 584 return
+5 -5
routers/web/repo/view.go
··· 1232 1232 page = 1 1233 1233 } 1234 1234 1235 - pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.MaxForksPerPage, page, 5) 1236 - ctx.Data["Page"] = pager 1237 - 1238 - forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, db.ListOptions{ 1239 - Page: pager.Paginater.Current(), 1235 + forks, total, err := repo_model.GetForks(ctx, ctx.Repo.Repository, ctx.Doer, db.ListOptions{ 1236 + Page: page, 1240 1237 PageSize: setting.MaxForksPerPage, 1241 1238 }) 1242 1239 if err != nil { 1243 1240 ctx.ServerError("GetForks", err) 1244 1241 return 1245 1242 } 1243 + 1244 + pager := context.NewPagination(int(total), setting.MaxForksPerPage, page, 5) 1245 + ctx.Data["Page"] = pager 1246 1246 1247 1247 for _, fork := range forks { 1248 1248 if err = fork.LoadOwner(ctx); err != nil {
+12 -3
routers/web/user/setting/account.go
··· 155 155 return 156 156 } 157 157 // Only fired when the primary email is inactive (Wrong state) 158 - mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer) 158 + if err := mailer.SendActivateAccountMail(ctx, ctx.Doer); err != nil { 159 + ctx.ServerError("SendActivateAccountMail", err) 160 + return 161 + } 159 162 } else { 160 - mailer.SendActivateEmailMail(ctx.Doer, email.Email) 163 + if err := mailer.SendActivateEmailMail(ctx, ctx.Doer, email.Email); err != nil { 164 + ctx.ServerError("SendActivateEmailMail", err) 165 + return 166 + } 161 167 } 162 168 address = email.Email 163 169 ··· 218 224 219 225 // Send confirmation email 220 226 if setting.Service.RegisterEmailConfirm { 221 - mailer.SendActivateEmailMail(ctx.Doer, form.Email) 227 + if err := mailer.SendActivateEmailMail(ctx, ctx.Doer, form.Email); err != nil { 228 + ctx.ServerError("SendActivateEmailMail", err) 229 + return 230 + } 222 231 if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil { 223 232 log.Error("Set cache(MailResendLimit) fail: %v", err) 224 233 }
+4 -2
routers/web/web.go
··· 1562 1562 m.Get("/cherry-pick/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick) 1563 1563 }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader) 1564 1564 1565 - m.Get("/rss/branch/*", repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed("rss")) 1566 - m.Get("/atom/branch/*", repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed("atom")) 1565 + m.Group("", func() { 1566 + m.Get("/rss/branch/*", feed.RenderBranchFeed("rss")) 1567 + m.Get("/atom/branch/*", feed.RenderBranchFeed("atom")) 1568 + }, repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), reqRepoCodeReader, feedEnabled) 1567 1569 1568 1570 m.Group("/src", func() { 1569 1571 m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home)
+11
services/auth/basic.go
··· 5 5 package auth 6 6 7 7 import ( 8 + "errors" 8 9 "net/http" 9 10 "strings" 10 11 ··· 130 131 log.Error("UserSignIn: %v", err) 131 132 } 132 133 return nil, err 134 + } 135 + 136 + hashWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID) 137 + if err != nil { 138 + log.Error("HasWebAuthnRegistrationsByUID: %v", err) 139 + return nil, err 140 + } 141 + 142 + if hashWebAuthn { 143 + return nil, errors.New("Basic authorization is not allowed while having security keys enrolled") 133 144 } 134 145 135 146 if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
+1 -1
services/context/context_cookie.go
··· 47 47 // SetLTACookie will generate a LTA token and add it as an cookie. 48 48 func (ctx *Context) SetLTACookie(u *user_model.User) error { 49 49 days := 86400 * setting.LogInRememberDays 50 - lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days))) 50 + lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days)), auth_model.LongTermAuthorization) 51 51 if err != nil { 52 52 return err 53 53 }
+3
services/doctor/dbconsistency.go
··· 243 243 // find archive download count without existing release 244 244 genericOrphanCheck("Archive download count without existing Release", 245 245 "repo_archive_download_count", "release", "repo_archive_download_count.release_id=release.id"), 246 + // find authorization tokens without existing user 247 + genericOrphanCheck("Authorization token without existing User", 248 + "forgejo_auth_token", "user", "forgejo_auth_token.uid=user.id"), 246 249 ) 247 250 248 251 for _, c := range consistencyChecks {
+33 -14
services/mailer/mail.go
··· 70 70 } 71 71 72 72 // sendUserMail sends a mail to the user 73 - func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, subject, info string) { 73 + func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, subject, info string) error { 74 74 locale := translation.NewLocale(language) 75 75 data := map[string]any{ 76 76 "locale": locale, ··· 84 84 var content bytes.Buffer 85 85 86 86 if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil { 87 - log.Error("Template: %v", err) 88 - return 87 + return err 89 88 } 90 89 91 90 msg := NewMessage(u.EmailTo(), subject, content.String()) 92 91 msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info) 93 92 94 93 SendAsync(msg) 94 + return nil 95 95 } 96 96 97 97 // SendActivateAccountMail sends an activation mail to the user (new user registration) 98 - func SendActivateAccountMail(locale translation.Locale, u *user_model.User) { 98 + func SendActivateAccountMail(ctx context.Context, u *user_model.User) error { 99 99 if setting.MailService == nil { 100 100 // No mail service configured 101 - return 101 + return nil 102 102 } 103 - sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account") 103 + 104 + locale := translation.NewLocale(u.Language) 105 + code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.UserActivation) 106 + if err != nil { 107 + return err 108 + } 109 + 110 + return sendUserMail(locale.Language(), u, mailAuthActivate, code, locale.TrString("mail.activate_account"), "activate account") 104 111 } 105 112 106 113 // SendResetPasswordMail sends a password reset mail to the user 107 - func SendResetPasswordMail(u *user_model.User) { 114 + func SendResetPasswordMail(ctx context.Context, u *user_model.User) error { 108 115 if setting.MailService == nil { 109 116 // No mail service configured 110 - return 117 + return nil 111 118 } 119 + 112 120 locale := translation.NewLocale(u.Language) 113 - sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account") 121 + code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.PasswordReset) 122 + if err != nil { 123 + return err 124 + } 125 + 126 + return sendUserMail(u.Language, u, mailAuthResetPassword, code, locale.TrString("mail.reset_password"), "recover account") 114 127 } 115 128 116 129 // SendActivateEmailMail sends confirmation email to confirm new email address 117 - func SendActivateEmailMail(u *user_model.User, email string) { 130 + func SendActivateEmailMail(ctx context.Context, u *user_model.User, email string) error { 118 131 if setting.MailService == nil { 119 132 // No mail service configured 120 - return 133 + return nil 121 134 } 135 + 122 136 locale := translation.NewLocale(u.Language) 137 + code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.EmailActivation(email)) 138 + if err != nil { 139 + return err 140 + } 141 + 123 142 data := map[string]any{ 124 143 "locale": locale, 125 144 "DisplayName": u.DisplayName(), 126 145 "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale), 127 - "Code": u.GenerateEmailActivateCode(email), 146 + "Code": code, 128 147 "Email": email, 129 148 "Language": locale.Language(), 130 149 } ··· 132 151 var content bytes.Buffer 133 152 134 153 if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil { 135 - log.Error("Template: %v", err) 136 - return 154 + return err 137 155 } 138 156 139 157 msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String()) 140 158 msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID) 141 159 142 160 SendAsync(msg) 161 + return nil 143 162 } 144 163 145 164 // SendRegisterNotifyMail triggers a notify e-mail by admin created a account.
+13 -3
services/mailer/token/token.go
··· 22 22 // 23 23 // The payload is verifiable by the generated HMAC using the user secret. It contains: 24 24 // | Timestamp | Action/Handler Type | Action/Handler Data | 25 + // 26 + // 27 + // Version changelog 28 + // 29 + // v1 -> v2: 30 + // Use 128 instead of 80 bits of the HMAC-SHA256 output. 25 31 26 32 const ( 27 33 tokenVersion1 byte = 1 34 + tokenVersion2 byte = 2 28 35 tokenLifetimeInYears int = 1 29 36 ) 30 37 ··· 70 77 return "", err 71 78 } 72 79 73 - return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil 80 + return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion2}, packagedData...)), nil 74 81 } 75 82 76 83 // ExtractToken extracts the action/user tuple from the token and verifies the content ··· 84 91 return UnknownHandlerType, nil, nil, &ErrToken{"no data"} 85 92 } 86 93 87 - if data[0] != tokenVersion1 { 94 + if data[0] != tokenVersion2 { 88 95 return UnknownHandlerType, nil, nil, &ErrToken{fmt.Sprintf("unsupported token version: %v", data[0])} 89 96 } 90 97 ··· 124 131 mac.Write(payload) 125 132 hmac := mac.Sum(nil) 126 133 127 - return hmac[:10] // RFC2104 recommends not using less then 80 bits 134 + // RFC2104 section 5 recommends that if you do HMAC truncation, you should use 135 + // the max(80, hash_len/2) of the leftmost bits. 136 + // For SHA256 this works out to using 128 of the leftmost bits. 137 + return hmac[:16] 128 138 }
+1
services/user/delete.go
··· 96 96 &user_model.BlockedUser{BlockID: u.ID}, 97 97 &user_model.BlockedUser{UserID: u.ID}, 98 98 &actions_model.ActionRunnerToken{OwnerID: u.ID}, 99 + &auth_model.AuthorizationToken{UID: u.ID}, 99 100 ); err != nil { 100 101 return fmt.Errorf("deleteBeans: %w", err) 101 102 }
+20
tests/integration/api_feed_user_test.go
··· 109 109 }) 110 110 }) 111 111 }) 112 + 113 + t.Run("View permission", func(t *testing.T) { 114 + t.Run("Anomynous", func(t *testing.T) { 115 + defer tests.PrintCurrentTest(t)() 116 + req := NewRequest(t, "GET", "/org3/repo3/rss/branch/master") 117 + MakeRequest(t, req, http.StatusNotFound) 118 + }) 119 + t.Run("No code permission", func(t *testing.T) { 120 + defer tests.PrintCurrentTest(t)() 121 + session := loginUser(t, "user8") 122 + req := NewRequest(t, "GET", "/org3/repo3/rss/branch/master") 123 + session.MakeRequest(t, req, http.StatusNotFound) 124 + }) 125 + t.Run("With code permission", func(t *testing.T) { 126 + defer tests.PrintCurrentTest(t)() 127 + session := loginUser(t, "user9") 128 + req := NewRequest(t, "GET", "/org3/repo3/rss/branch/master") 129 + session.MakeRequest(t, req, http.StatusOK) 130 + }) 131 + }) 112 132 }
+43
tests/integration/api_fork_test.go
··· 17 17 "code.gitea.io/gitea/modules/test" 18 18 "code.gitea.io/gitea/routers" 19 19 "code.gitea.io/gitea/tests" 20 + 21 + "github.com/stretchr/testify/assert" 20 22 ) 21 23 22 24 func TestAPIForkAsAdminIgnoringLimits(t *testing.T) { ··· 106 108 session.MakeRequest(t, req, http.StatusNotFound) 107 109 }) 108 110 } 111 + 112 + func TestAPIForkListPrivateRepo(t *testing.T) { 113 + defer tests.PrepareTestEnv(t)() 114 + 115 + session := loginUser(t, "user5") 116 + token := getTokenForLoggedInUser(t, session, 117 + auth_model.AccessTokenScopeWriteRepository, 118 + auth_model.AccessTokenScopeWriteOrganization) 119 + org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23, Visibility: api.VisibleTypePrivate}) 120 + 121 + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{ 122 + Organization: &org23.Name, 123 + }).AddTokenAuth(token) 124 + MakeRequest(t, req, http.StatusAccepted) 125 + 126 + t.Run("Anomynous", func(t *testing.T) { 127 + defer tests.PrintCurrentTest(t)() 128 + 129 + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks") 130 + resp := MakeRequest(t, req, http.StatusOK) 131 + 132 + var forks []*api.Repository 133 + DecodeJSON(t, resp, &forks) 134 + 135 + assert.Empty(t, forks) 136 + assert.EqualValues(t, "0", resp.Header().Get("X-Total-Count")) 137 + }) 138 + 139 + t.Run("Logged in", func(t *testing.T) { 140 + defer tests.PrintCurrentTest(t)() 141 + 142 + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(token) 143 + resp := MakeRequest(t, req, http.StatusOK) 144 + 145 + var forks []*api.Repository 146 + DecodeJSON(t, resp, &forks) 147 + 148 + assert.Len(t, forks, 1) 149 + assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count")) 150 + }) 151 + }
+22
tests/integration/api_twofa_test.go
··· 15 15 "code.gitea.io/gitea/tests" 16 16 17 17 "github.com/pquerna/otp/totp" 18 + "github.com/stretchr/testify/assert" 18 19 "github.com/stretchr/testify/require" 19 20 ) 20 21 ··· 58 59 req.Header.Set("X-Forgejo-OTP", passcode) 59 60 MakeRequest(t, req, http.StatusOK) 60 61 } 62 + 63 + func TestAPIWebAuthn(t *testing.T) { 64 + defer tests.PrepareTestEnv(t)() 65 + 66 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 32}) 67 + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{UserID: user.ID}) 68 + 69 + req := NewRequest(t, "GET", "/api/v1/user") 70 + req.SetBasicAuth(user.Name, "notpassword") 71 + 72 + resp := MakeRequest(t, req, http.StatusUnauthorized) 73 + 74 + type userResponse struct { 75 + Message string `json:"message"` 76 + } 77 + var userParsed userResponse 78 + 79 + DecodeJSON(t, resp, &userParsed) 80 + 81 + assert.EqualValues(t, "Basic authorization is not allowed while having security keys enrolled", userParsed.Message) 82 + }
+4 -4
tests/integration/auth_token_test.go
··· 84 84 assert.True(t, found) 85 85 rawValidator, err := hex.DecodeString(validator) 86 86 require.NoError(t, err) 87 - unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{LookupKey: lookupKey, HashedValidator: auth.HashValidator(rawValidator), UID: user.ID}) 87 + unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{LookupKey: lookupKey, HashedValidator: auth.HashValidator(rawValidator), UID: user.ID, Purpose: auth.LongTermAuthorization}) 88 88 89 89 // Check if the LTA cookie it provides authentication. 90 90 // If LTA cookie provides authentication /user/login shouldn't return status 200. ··· 143 143 assert.True(t, found) 144 144 145 145 // Ensure it's not expired. 146 - lta := unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey}) 146 + lta := unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization}) 147 147 assert.False(t, lta.IsExpired()) 148 148 149 149 // Manually stub LTA's expiry. ··· 151 151 require.NoError(t, err) 152 152 153 153 // Ensure it's expired. 154 - lta = unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey}) 154 + lta = unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization}) 155 155 assert.True(t, lta.IsExpired()) 156 156 157 157 // Should return 200 OK, because LTA doesn't provide authorization anymore. ··· 160 160 session.MakeRequest(t, req, http.StatusOK) 161 161 162 162 // Ensure it's deleted. 163 - unittest.AssertNotExistsBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey}) 163 + unittest.AssertNotExistsBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization}) 164 164 }
+21
tests/integration/fixtures/TestFeed/team.yml
··· 1 + - 2 + id: 1001 3 + org_id: 3 4 + lower_name: no_code 5 + name: no_code 6 + authorize: 1 # read 7 + num_repos: 1 8 + num_members: 1 9 + includes_all_repositories: false 10 + can_create_org_repo: false 11 + 12 + - 13 + id: 1002 14 + org_id: 3 15 + lower_name: read_code 16 + name: no_code 17 + authorize: 1 # read 18 + num_repos: 1 19 + num_members: 1 20 + includes_all_repositories: false 21 + can_create_org_repo: false
+11
tests/integration/fixtures/TestFeed/team_repo.yml
··· 1 + - 2 + id: 1001 3 + org_id: 3 4 + team_id: 1001 5 + repo_id: 3 6 + 7 + - 8 + id: 1002 9 + org_id: 3 10 + team_id: 1002 11 + repo_id: 3
+83
tests/integration/fixtures/TestFeed/team_unit.yml
··· 1 + - 2 + id: 1001 3 + team_id: 1001 4 + type: 1 5 + access_mode: 0 6 + 7 + - 8 + id: 1002 9 + team_id: 1001 10 + type: 2 11 + access_mode: 1 12 + 13 + - 14 + id: 1003 15 + team_id: 1001 16 + type: 3 17 + access_mode: 1 18 + 19 + - 20 + id: 1004 21 + team_id: 1001 22 + type: 4 23 + access_mode: 1 24 + 25 + - 26 + id: 1005 27 + team_id: 1001 28 + type: 5 29 + access_mode: 1 30 + 31 + - 32 + id: 1006 33 + team_id: 1001 34 + type: 6 35 + access_mode: 1 36 + 37 + - 38 + id: 1007 39 + team_id: 1001 40 + type: 7 41 + access_mode: 1 42 + 43 + - 44 + id: 1008 45 + team_id: 1002 46 + type: 1 47 + access_mode: 1 48 + 49 + - 50 + id: 1009 51 + team_id: 1002 52 + type: 2 53 + access_mode: 1 54 + 55 + - 56 + id: 1010 57 + team_id: 1002 58 + type: 3 59 + access_mode: 1 60 + 61 + - 62 + id: 1011 63 + team_id: 1002 64 + type: 4 65 + access_mode: 1 66 + 67 + - 68 + id: 1012 69 + team_id: 1002 70 + type: 5 71 + access_mode: 1 72 + 73 + - 74 + id: 1013 75 + team_id: 1002 76 + type: 6 77 + access_mode: 1 78 + 79 + - 80 + id: 1014 81 + team_id: 1002 82 + type: 7 83 + access_mode: 1
+11
tests/integration/fixtures/TestFeed/team_user.yml
··· 1 + - 2 + id: 1001 3 + org_id: 3 4 + team_id: 1001 5 + uid: 8 6 + 7 + - 8 + id: 1002 9 + org_id: 3 10 + team_id: 1002 11 + uid: 9
+46
tests/integration/incoming_email_test.go
··· 4 4 package integration 5 5 6 6 import ( 7 + "encoding/base32" 7 8 "io" 8 9 "net" 9 10 "net/smtp" ··· 73 74 assert.Equal(t, token_service.ReplyHandlerType, ht) 74 75 assert.Equal(t, user.ID, u.ID) 75 76 assert.Equal(t, payload, p) 77 + }) 78 + 79 + tokenEncoding := base32.StdEncoding.WithPadding(base32.NoPadding) 80 + t.Run("Deprecated token version", func(t *testing.T) { 81 + defer tests.PrintCurrentTest(t)() 82 + 83 + payload := []byte{1, 2, 3, 4, 5} 84 + 85 + token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload) 86 + require.NoError(t, err) 87 + assert.NotEmpty(t, token) 88 + 89 + // Set the token to version 1. 90 + unencodedToken, err := tokenEncoding.DecodeString(token) 91 + require.NoError(t, err) 92 + unencodedToken[0] = 1 93 + token = tokenEncoding.EncodeToString(unencodedToken) 94 + 95 + ht, u, p, err := token_service.ExtractToken(db.DefaultContext, token) 96 + require.ErrorContains(t, err, "unsupported token version: 1") 97 + assert.Equal(t, token_service.UnknownHandlerType, ht) 98 + assert.Nil(t, u) 99 + assert.Nil(t, p) 100 + }) 101 + 102 + t.Run("MAC check", func(t *testing.T) { 103 + defer tests.PrintCurrentTest(t)() 104 + 105 + payload := []byte{1, 2, 3, 4, 5} 106 + 107 + token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload) 108 + require.NoError(t, err) 109 + assert.NotEmpty(t, token) 110 + 111 + // Modify the MAC. 112 + unencodedToken, err := tokenEncoding.DecodeString(token) 113 + require.NoError(t, err) 114 + unencodedToken[len(unencodedToken)-1] ^= 0x01 115 + token = tokenEncoding.EncodeToString(unencodedToken) 116 + 117 + ht, u, p, err := token_service.ExtractToken(db.DefaultContext, token) 118 + require.ErrorContains(t, err, "verification failed") 119 + assert.Equal(t, token_service.UnknownHandlerType, ht) 120 + assert.Nil(t, u) 121 + assert.Nil(t, p) 76 122 }) 77 123 78 124 t.Run("Handler", func(t *testing.T) {
+79
tests/integration/mirror_push_test.go
··· 323 323 }) 324 324 }) 325 325 } 326 + 327 + func TestPushMirrorSettings(t *testing.T) { 328 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 329 + defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() 330 + defer test.MockVariableValue(&setting.Mirror.Enabled, true)() 331 + require.NoError(t, migrations.Init()) 332 + 333 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 334 + srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) 335 + srcRepo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) 336 + assert.False(t, srcRepo.HasWiki()) 337 + sess := loginUser(t, user.Name) 338 + pushToRepo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{ 339 + Name: optional.Some("push-mirror-test"), 340 + AutoInit: optional.Some(false), 341 + EnabledUnits: optional.Some([]unit.Type{unit.TypeCode}), 342 + }) 343 + defer f() 344 + 345 + t.Run("Adding", func(t *testing.T) { 346 + defer tests.PrintCurrentTest(t)() 347 + 348 + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings", srcRepo2.FullName()), map[string]string{ 349 + "_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings", srcRepo2.FullName())), 350 + "action": "push-mirror-add", 351 + "push_mirror_address": u.String() + pushToRepo.FullName(), 352 + "push_mirror_interval": "0", 353 + }) 354 + sess.MakeRequest(t, req, http.StatusSeeOther) 355 + 356 + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings", srcRepo.FullName()), map[string]string{ 357 + "_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings", srcRepo.FullName())), 358 + "action": "push-mirror-add", 359 + "push_mirror_address": u.String() + pushToRepo.FullName(), 360 + "push_mirror_interval": "0", 361 + }) 362 + sess.MakeRequest(t, req, http.StatusSeeOther) 363 + 364 + flashCookie := sess.GetCookie(gitea_context.CookieNameFlash) 365 + assert.NotNil(t, flashCookie) 366 + assert.Contains(t, flashCookie.Value, "success") 367 + }) 368 + 369 + mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{}) 370 + require.NoError(t, err) 371 + assert.Len(t, mirrors, 1) 372 + mirrorID := mirrors[0].ID 373 + 374 + mirrors, _, err = repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo2.ID, db.ListOptions{}) 375 + require.NoError(t, err) 376 + assert.Len(t, mirrors, 1) 377 + 378 + t.Run("Interval", func(t *testing.T) { 379 + defer tests.PrintCurrentTest(t)() 380 + 381 + unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{ID: mirrorID - 1}) 382 + 383 + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings", srcRepo.FullName()), map[string]string{ 384 + "_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings", srcRepo.FullName())), 385 + "action": "push-mirror-update", 386 + "push_mirror_id": strconv.FormatInt(mirrorID-1, 10), 387 + "push_mirror_interval": "10m0s", 388 + }) 389 + sess.MakeRequest(t, req, http.StatusNotFound) 390 + 391 + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings", srcRepo.FullName()), map[string]string{ 392 + "_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings", srcRepo.FullName())), 393 + "action": "push-mirror-update", 394 + "push_mirror_id": strconv.FormatInt(mirrorID, 10), 395 + "push_mirror_interval": "10m0s", 396 + }) 397 + sess.MakeRequest(t, req, http.StatusSeeOther) 398 + 399 + flashCookie := sess.GetCookie(gitea_context.CookieNameFlash) 400 + assert.NotNil(t, flashCookie) 401 + assert.Contains(t, flashCookie.Value, "success") 402 + }) 403 + }) 404 + }
+5 -2
tests/integration/org_team_invite_test.go
··· 10 10 "strings" 11 11 "testing" 12 12 13 + "code.gitea.io/gitea/models/auth" 13 14 "code.gitea.io/gitea/models/db" 14 15 "code.gitea.io/gitea/models/organization" 15 16 "code.gitea.io/gitea/models/unittest" ··· 293 294 require.NoError(t, err) 294 295 session.jar.SetCookies(baseURL, cr.Cookies()) 295 296 296 - activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com")) 297 - req = NewRequestWithValues(t, "POST", activateURL, map[string]string{ 297 + code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation) 298 + require.NoError(t, err) 299 + 300 + req = NewRequestWithValues(t, "POST", "/user/activate?code="+url.QueryEscape(code), map[string]string{ 298 301 "password": "examplePassword!1", 299 302 }) 300 303
+32
tests/integration/repo_fork_test.go
··· 17 17 "code.gitea.io/gitea/models/unittest" 18 18 user_model "code.gitea.io/gitea/models/user" 19 19 "code.gitea.io/gitea/modules/setting" 20 + "code.gitea.io/gitea/modules/structs" 20 21 "code.gitea.io/gitea/modules/test" 21 22 "code.gitea.io/gitea/routers" 22 23 repo_service "code.gitea.io/gitea/services/repository" ··· 238 239 }) 239 240 }) 240 241 } 242 + 243 + func TestForkListPrivateRepo(t *testing.T) { 244 + forkItemSelector := ".tw-flex.tw-items-center.tw-py-2" 245 + 246 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 247 + session := loginUser(t, "user5") 248 + org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23, Visibility: structs.VisibleTypePrivate}) 249 + 250 + testRepoFork(t, session, "user2", "repo1", org23.Name, "repo1") 251 + 252 + t.Run("Anomynous", func(t *testing.T) { 253 + defer tests.PrintCurrentTest(t)() 254 + 255 + req := NewRequest(t, "GET", "/user2/repo1/forks") 256 + resp := MakeRequest(t, req, http.StatusOK) 257 + htmlDoc := NewHTMLParser(t, resp.Body) 258 + 259 + htmlDoc.AssertElement(t, forkItemSelector, false) 260 + }) 261 + 262 + t.Run("Logged in", func(t *testing.T) { 263 + defer tests.PrintCurrentTest(t)() 264 + 265 + req := NewRequest(t, "GET", "/user2/repo1/forks") 266 + resp := session.MakeRequest(t, req, http.StatusOK) 267 + htmlDoc := NewHTMLParser(t, resp.Body) 268 + 269 + htmlDoc.AssertElement(t, forkItemSelector, true) 270 + }) 271 + }) 272 + }
+172
tests/integration/user_test.go
··· 5 5 package integration 6 6 7 7 import ( 8 + "bytes" 9 + "encoding/hex" 8 10 "fmt" 9 11 "net/http" 12 + "net/url" 10 13 "strconv" 11 14 "strings" 12 15 "testing" 13 16 "time" 14 17 15 18 auth_model "code.gitea.io/gitea/models/auth" 19 + "code.gitea.io/gitea/models/db" 16 20 issues_model "code.gitea.io/gitea/models/issues" 17 21 repo_model "code.gitea.io/gitea/models/repo" 18 22 unit_model "code.gitea.io/gitea/models/unit" ··· 836 840 } 837 841 } 838 842 } 843 + 844 + func TestUserActivate(t *testing.T) { 845 + defer tests.PrepareTestEnv(t)() 846 + defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)() 847 + 848 + called := false 849 + code := "" 850 + defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) { 851 + called = true 852 + assert.Len(t, msgs, 1) 853 + assert.Equal(t, `"doesnotexist" <doesnotexist@example.com>`, msgs[0].To) 854 + assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.activate_account"), msgs[0].Subject) 855 + 856 + messageDoc := NewHTMLParser(t, bytes.NewBuffer([]byte(msgs[0].Body))) 857 + link, ok := messageDoc.Find("a").Attr("href") 858 + assert.True(t, ok) 859 + u, err := url.Parse(link) 860 + require.NoError(t, err) 861 + code = u.Query()["code"][0] 862 + })() 863 + 864 + session := emptyTestSession(t) 865 + req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ 866 + "_csrf": GetCSRF(t, session, "/user/sign_up"), 867 + "user_name": "doesnotexist", 868 + "email": "doesnotexist@example.com", 869 + "password": "examplePassword!1", 870 + "retype": "examplePassword!1", 871 + }) 872 + session.MakeRequest(t, req, http.StatusOK) 873 + assert.True(t, called) 874 + 875 + queryCode, err := url.QueryUnescape(code) 876 + require.NoError(t, err) 877 + 878 + lookupKey, validator, ok := strings.Cut(queryCode, ":") 879 + assert.True(t, ok) 880 + 881 + rawValidator, err := hex.DecodeString(validator) 882 + require.NoError(t, err) 883 + 884 + authToken, err := auth_model.FindAuthToken(db.DefaultContext, lookupKey, auth_model.UserActivation) 885 + require.NoError(t, err) 886 + assert.False(t, authToken.IsExpired()) 887 + assert.EqualValues(t, authToken.HashedValidator, auth_model.HashValidator(rawValidator)) 888 + 889 + req = NewRequest(t, "POST", "/user/activate?code="+code) 890 + session.MakeRequest(t, req, http.StatusOK) 891 + 892 + unittest.AssertNotExistsBean(t, &auth_model.AuthorizationToken{ID: authToken.ID}) 893 + unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "doesnotexist", IsActive: true}) 894 + } 895 + 896 + func TestUserPasswordReset(t *testing.T) { 897 + defer tests.PrepareTestEnv(t)() 898 + 899 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 900 + 901 + called := false 902 + code := "" 903 + defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) { 904 + if called { 905 + return 906 + } 907 + called = true 908 + 909 + assert.Len(t, msgs, 1) 910 + assert.Equal(t, user2.EmailTo(), msgs[0].To) 911 + assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.reset_password"), msgs[0].Subject) 912 + 913 + messageDoc := NewHTMLParser(t, bytes.NewBuffer([]byte(msgs[0].Body))) 914 + link, ok := messageDoc.Find("a").Attr("href") 915 + assert.True(t, ok) 916 + u, err := url.Parse(link) 917 + require.NoError(t, err) 918 + code = u.Query()["code"][0] 919 + })() 920 + 921 + session := emptyTestSession(t) 922 + req := NewRequestWithValues(t, "POST", "/user/forgot_password", map[string]string{ 923 + "_csrf": GetCSRF(t, session, "/user/forgot_password"), 924 + "email": user2.Email, 925 + }) 926 + session.MakeRequest(t, req, http.StatusOK) 927 + assert.True(t, called) 928 + 929 + queryCode, err := url.QueryUnescape(code) 930 + require.NoError(t, err) 931 + 932 + lookupKey, validator, ok := strings.Cut(queryCode, ":") 933 + assert.True(t, ok) 934 + 935 + rawValidator, err := hex.DecodeString(validator) 936 + require.NoError(t, err) 937 + 938 + authToken, err := auth_model.FindAuthToken(db.DefaultContext, lookupKey, auth_model.PasswordReset) 939 + require.NoError(t, err) 940 + assert.False(t, authToken.IsExpired()) 941 + assert.EqualValues(t, authToken.HashedValidator, auth_model.HashValidator(rawValidator)) 942 + 943 + req = NewRequestWithValues(t, "POST", "/user/recover_account", map[string]string{ 944 + "_csrf": GetCSRF(t, session, "/user/recover_account"), 945 + "code": code, 946 + "password": "new_password", 947 + }) 948 + session.MakeRequest(t, req, http.StatusSeeOther) 949 + 950 + unittest.AssertNotExistsBean(t, &auth_model.AuthorizationToken{ID: authToken.ID}) 951 + assert.True(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).ValidatePassword("new_password")) 952 + } 953 + 954 + func TestActivateEmailAddress(t *testing.T) { 955 + defer tests.PrepareTestEnv(t)() 956 + defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)() 957 + 958 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 959 + 960 + called := false 961 + code := "" 962 + defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) { 963 + if called { 964 + return 965 + } 966 + called = true 967 + 968 + assert.Len(t, msgs, 1) 969 + assert.Equal(t, "newemail@example.org", msgs[0].To) 970 + assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.activate_email"), msgs[0].Subject) 971 + 972 + messageDoc := NewHTMLParser(t, bytes.NewBuffer([]byte(msgs[0].Body))) 973 + link, ok := messageDoc.Find("a").Attr("href") 974 + assert.True(t, ok) 975 + u, err := url.Parse(link) 976 + require.NoError(t, err) 977 + code = u.Query()["code"][0] 978 + })() 979 + 980 + session := loginUser(t, user2.Name) 981 + req := NewRequestWithValues(t, "POST", "/user/settings/account/email", map[string]string{ 982 + "_csrf": GetCSRF(t, session, "/user/settings"), 983 + "email": "newemail@example.org", 984 + }) 985 + session.MakeRequest(t, req, http.StatusSeeOther) 986 + assert.True(t, called) 987 + 988 + queryCode, err := url.QueryUnescape(code) 989 + require.NoError(t, err) 990 + 991 + lookupKey, validator, ok := strings.Cut(queryCode, ":") 992 + assert.True(t, ok) 993 + 994 + rawValidator, err := hex.DecodeString(validator) 995 + require.NoError(t, err) 996 + 997 + authToken, err := auth_model.FindAuthToken(db.DefaultContext, lookupKey, auth_model.EmailActivation("newemail@example.org")) 998 + require.NoError(t, err) 999 + assert.False(t, authToken.IsExpired()) 1000 + assert.EqualValues(t, authToken.HashedValidator, auth_model.HashValidator(rawValidator)) 1001 + 1002 + req = NewRequestWithValues(t, "POST", "/user/activate_email", map[string]string{ 1003 + "code": code, 1004 + "email": "newemail@example.org", 1005 + }) 1006 + session.MakeRequest(t, req, http.StatusSeeOther) 1007 + 1008 + unittest.AssertNotExistsBean(t, &auth_model.AuthorizationToken{ID: authToken.ID}) 1009 + unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{UID: user2.ID, IsActivated: true, Email: "newemail@example.org"}) 1010 + }