Compare commits

..

2 commits

Author SHA1 Message Date
83e30779cf nulo: woodpecker CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-21 10:56:07 -03:00
ebf42af71c Dockerfile: rename user to _gitea instead of git 2023-09-21 10:55:51 -03:00
47 changed files with 215 additions and 483 deletions

View file

@ -89,7 +89,7 @@ endif
VERSION = ${GITEA_VERSION} VERSION = ${GITEA_VERSION}
# SemVer # SemVer
FORGEJO_VERSION := 5.0.5+0-gitea-1.20.5 FORGEJO_VERSION := 5.0.4+0-gitea-1.20.4
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)" LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)"

View file

@ -17,13 +17,13 @@ menu:
# Database Preparation # Database Preparation
You need a database to use Gitea. Gitea supports PostgreSQL (>=10), MySQL (>=5.7), MariaDB, SQLite, and MSSQL (>=2008R2 SP3). This page will guide into preparing database. Only PostgreSQL and MySQL will be covered here since those database engines are widely-used in production. If you plan to use SQLite, you can ignore this chapter. You need a database to use Gitea. Gitea supports PostgreSQL (>=10), MySQL (>=5.7), SQLite, and MSSQL (>=2008R2 SP3). This page will guide into preparing database. Only PostgreSQL and MySQL will be covered here since those database engines are widely-used in production. If you plan to use SQLite, you can ignore this chapter.
Database instance can be on same machine as Gitea (local database setup), or on different machine (remote database). Database instance can be on same machine as Gitea (local database setup), or on different machine (remote database).
Note: All steps below requires that the database engine of your choice is installed on your system. For remote database setup, install the server application on database instance and client program on your Gitea server. The client program is used to test connection to the database from Gitea server, while Gitea itself use database driver provided by Go to accomplish the same thing. In addition, make sure you use same engine version for both server and client for some engine features to work. For security reason, protect `root` (MySQL) or `postgres` (PostgreSQL) database superuser with secure password. The steps assumes that you run Linux for both database and Gitea servers. Note: All steps below requires that the database engine of your choice is installed on your system. For remote database setup, install the server application on database instance and client program on your Gitea server. The client program is used to test connection to the database from Gitea server, while Gitea itself use database driver provided by Go to accomplish the same thing. In addition, make sure you use same engine version for both server and client for some engine features to work. For security reason, protect `root` (MySQL) or `postgres` (PostgreSQL) database superuser with secure password. The steps assumes that you run Linux for both database and Gitea servers.
## MySQL/MariaDB ## MySQL
1. For remote database setup, you will need to make MySQL listen to your IP address. Edit `bind-address` option on `/etc/mysql/my.cnf` on database instance to: 1. For remote database setup, you will need to make MySQL listen to your IP address. Edit `bind-address` option on `/etc/mysql/my.cnf` on database instance to:
@ -45,7 +45,7 @@ Note: All steps below requires that the database engine of your choice is instal
```sql ```sql
SET old_passwords=0; SET old_passwords=0;
CREATE USER 'gitea'@'%' IDENTIFIED BY 'gitea'; CREATE USER 'gitea' IDENTIFIED BY 'gitea';
``` ```
For remote database: For remote database:

View file

@ -342,7 +342,7 @@ func (stats *ActivityStats) FillReleases(repoID int64, fromTime time.Time) error
// Published releases list // Published releases list
sess := releasesForActivityStatement(repoID, fromTime) sess := releasesForActivityStatement(repoID, fromTime)
sess.OrderBy("`release`.created_unix DESC") sess.OrderBy("release.created_unix DESC")
stats.PublishedReleases = make([]*repo_model.Release, 0) stats.PublishedReleases = make([]*repo_model.Release, 0)
if err = sess.Find(&stats.PublishedReleases); err != nil { if err = sess.Find(&stats.PublishedReleases); err != nil {
return err return err
@ -350,7 +350,7 @@ func (stats *ActivityStats) FillReleases(repoID int64, fromTime time.Time) error
// Published releases authors // Published releases authors
sess = releasesForActivityStatement(repoID, fromTime) sess = releasesForActivityStatement(repoID, fromTime)
if _, err = sess.Select("count(distinct `release`.publisher_id) as `count`").Table("release").Get(&count); err != nil { if _, err = sess.Select("count(distinct release.publisher_id) as `count`").Table("release").Get(&count); err != nil {
return err return err
} }
stats.PublishedReleaseAuthorCount = count stats.PublishedReleaseAuthorCount = count
@ -359,7 +359,7 @@ func (stats *ActivityStats) FillReleases(repoID int64, fromTime time.Time) error
} }
func releasesForActivityStatement(repoID int64, fromTime time.Time) *xorm.Session { func releasesForActivityStatement(repoID int64, fromTime time.Time) *xorm.Session {
return db.GetEngine(db.DefaultContext).Where("`release`.repo_id = ?", repoID). return db.GetEngine(db.DefaultContext).Where("release.repo_id = ?", repoID).
And("`release`.is_draft = ?", false). And("release.is_draft = ?", false).
And("`release`.created_unix >= ?", fromTime.Unix()) And("release.created_unix >= ?", fromTime.Unix())
} }

View file

@ -1,96 +0,0 @@
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
)
// AuthorizationToken represents a authorization token to a user.
type AuthorizationToken struct {
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"INDEX"`
LookupKey string `xorm:"INDEX UNIQUE"`
HashedValidator string
Expiry timeutil.TimeStamp
}
// TableName provides the real table name.
func (AuthorizationToken) TableName() string {
return "forgejo_auth_token"
}
func init() {
db.RegisterModel(new(AuthorizationToken))
}
// IsExpired returns if the authorization token is expired.
func (authToken *AuthorizationToken) IsExpired() bool {
return authToken.Expiry.AsLocalTime().Before(time.Now())
}
// GenerateAuthToken generates a new authentication token for the given user.
// It returns the lookup key and validator values that should be passed to the
// user via a long-term cookie.
func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeStamp) (lookupKey, validator string, err error) {
// Request 64 random bytes. The first 32 bytes will be used for the lookupKey
// and the other 32 bytes will be used for the validator.
rBytes, err := util.CryptoRandomBytes(64)
if err != nil {
return "", "", err
}
hexEncoded := hex.EncodeToString(rBytes)
validator, lookupKey = hexEncoded[64:], hexEncoded[:64]
_, err = db.GetEngine(ctx).Insert(&AuthorizationToken{
UID: userID,
Expiry: expiry,
LookupKey: lookupKey,
HashedValidator: HashValidator(rBytes[32:]),
})
return lookupKey, validator, err
}
// FindAuthToken will find a authorization token via the lookup key.
func FindAuthToken(ctx context.Context, lookupKey string) (*AuthorizationToken, error) {
var authToken AuthorizationToken
has, err := db.GetEngine(ctx).Where("lookup_key = ?", lookupKey).Get(&authToken)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("lookup key %q: %w", lookupKey, util.ErrNotExist)
}
return &authToken, nil
}
// DeleteAuthToken will delete the authorization token.
func DeleteAuthToken(ctx context.Context, authToken *AuthorizationToken) error {
_, err := db.DeleteByBean(ctx, authToken)
return err
}
// DeleteAuthTokenByUser will delete all authorization tokens for the user.
func DeleteAuthTokenByUser(ctx context.Context, userID int64) error {
if userID == 0 {
return nil
}
_, err := db.DeleteByBean(ctx, &AuthorizationToken{UID: userID})
return err
}
// HashValidator will return a hexified hashed version of the validator.
func HashValidator(validator []byte) string {
h := sha256.New()
h.Write(validator)
return hex.EncodeToString(h.Sum(nil))
}

View file

@ -140,16 +140,3 @@
download_count: 0 download_count: 0
size: 0 size: 0
created_unix: 946684800 created_unix: 946684800
-
id: 12
uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22
repo_id: 2
issue_id: 0
release_id: 11
uploader_id: 2
comment_id: 0
name: README.md
download_count: 0
size: 0
created_unix: 946684800

View file

@ -136,17 +136,3 @@
is_prerelease: false is_prerelease: false
is_tag: false is_tag: false
created_unix: 946684803 created_unix: 946684803
- id: 11
repo_id: 2
publisher_id: 2
tag_name: "v1.1"
lower_tag_name: "v1.1"
target: ""
title: "v1.1"
sha1: "205ac761f3326a7ebe416e8673760016450b5cec"
num_commits: 2
is_draft: false
is_prerelease: false
is_tag: false
created_unix: 946684803

View file

@ -40,8 +40,6 @@ var migrations = []*Migration{
NewMigration("Add Forgejo Blocked Users table", forgejo_v1_20.AddForgejoBlockedUser), NewMigration("Add Forgejo Blocked Users table", forgejo_v1_20.AddForgejoBlockedUser),
// v2 -> v3 // v2 -> v3
NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable), NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable),
// v2 -> v3
NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable),
} }
// GetCurrentDBVersion returns the current Forgejo database version. // GetCurrentDBVersion returns the current Forgejo database version.

View file

@ -1,25 +0,0 @@
// SPDX-License-Identifier: MIT
package forgejo_v1_20 //nolint:revive
import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
type AuthorizationToken struct {
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"INDEX"`
LookupKey string `xorm:"INDEX UNIQUE"`
HashedValidator string
Expiry timeutil.TimeStamp
}
func (AuthorizationToken) TableName() string {
return "forgejo_auth_token"
}
func CreateAuthorizationTokenTable(x *xorm.Engine) error {
return x.Sync(new(AuthorizationToken))
}

View file

@ -10,7 +10,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
) )
// CommentList defines a list of comments // CommentList defines a list of comments
@ -423,18 +422,37 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
reviewIDs := comments.getReviewIDs() reviewIDs := comments.getReviewIDs()
reviews := make(map[int64]*Review, len(reviewIDs)) reviews := make(map[int64]*Review, len(reviewIDs))
if err := db.GetEngine(ctx).In("id", reviewIDs).Find(&reviews); err != nil { left := len(reviewIDs)
for left > 0 {
limit := db.DefaultMaxInSize
if left < limit {
limit = left
}
rows, err := db.GetEngine(ctx).
In("id", reviewIDs[:limit]).
Rows(new(Review))
if err != nil {
return err return err
} }
for rows.Next() {
var review Review
err = rows.Scan(&review)
if err != nil {
_ = rows.Close()
return err
}
reviews[review.ID] = &review
}
_ = rows.Close()
left -= limit
reviewIDs = reviewIDs[limit:]
}
for _, comment := range comments { for _, comment := range comments {
comment.Review = reviews[comment.ReviewID] comment.Review = reviews[comment.ReviewID]
if comment.Review == nil {
if comment.ReviewID > 0 {
log.Error("comment with review id [%d] but has no review record", comment.ReviewID)
}
continue
}
// If the comment dismisses a review, we need to load the reviewer to show whose review has been dismissed. // If the comment dismisses a review, we need to load the reviewer to show whose review has been dismissed.
// Otherwise, the reviewer is the poster of the comment, so we don't need to load it. // Otherwise, the reviewer is the poster of the comment, so we don't need to load it.

View file

@ -349,21 +349,14 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64)
From("team_user"). From("team_user").
Where(builder.Eq{"team_user.uid": reviewRequestedID}) Where(builder.Eq{"team_user.uid": reviewRequestedID})
// if the review is approved or rejected, it should not be shown in the review requested list
maxReview := builder.Select("MAX(r.id)").
From("review as r").
Where(builder.In("r.type", []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest})).
GroupBy("r.issue_id, r.reviewer_id, r.reviewer_team_id")
subQuery := builder.Select("review.issue_id"). subQuery := builder.Select("review.issue_id").
From("review"). From("review").
Where(builder.And( Where(builder.And(
builder.Eq{"review.type": ReviewTypeRequest}, builder.In("review.type", []ReviewType{ReviewTypeRequest, ReviewTypeReject, ReviewTypeApprove}),
builder.Or( builder.Or(
builder.Eq{"review.reviewer_id": reviewRequestedID}, builder.Eq{"review.reviewer_id": reviewRequestedID},
builder.In("review.reviewer_team_id", existInTeamQuery), builder.In("review.reviewer_team_id", existInTeamQuery),
), ),
builder.In("review.id", maxReview),
)) ))
return sess.Where("issue.poster_id <> ?", reviewRequestedID). return sess.Where("issue.poster_id <> ?", reviewRequestedID).
And(builder.In("issue.id", subQuery)) And(builder.In("issue.id", subQuery))

View file

@ -380,11 +380,6 @@ func (u *User) SetPassword(passwd string) (err error) {
return nil return nil
} }
// Invalidate all authentication tokens for this user.
if err := auth.DeleteAuthTokenByUser(db.DefaultContext, u.ID); err != nil {
return err
}
if u.Salt, err = GetUserSalt(); err != nil { if u.Salt, err = GetUserSalt(); err != nil {
return err return err
} }

View file

@ -4,14 +4,16 @@
package context package context
import ( import (
"encoding/hex"
"net/http" "net/http"
"strings" "strings"
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"github.com/minio/sha256-simd"
"golang.org/x/crypto/pbkdf2"
) )
const CookieNameFlash = "gitea_flash" const CookieNameFlash = "gitea_flash"
@ -44,13 +46,41 @@ func (ctx *Context) GetSiteCookie(name string) string {
return middleware.GetSiteCookie(ctx.Req, name) return middleware.GetSiteCookie(ctx.Req, name)
} }
// SetLTACookie will generate a LTA token and add it as an cookie. // GetSuperSecureCookie returns given cookie value from request header with secret string.
func (ctx *Context) SetLTACookie(u *user_model.User) error { func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
days := 86400 * setting.LogInRememberDays val := ctx.GetSiteCookie(name)
lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days))) return ctx.CookieDecrypt(secret, val)
}
// CookieDecrypt returns given value from with secret string.
func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) {
if val == "" {
return "", false
}
text, err := hex.DecodeString(val)
if err != nil { if err != nil {
return err return "", false
} }
ctx.SetSiteCookie(setting.CookieRememberName, lookup+":"+validator, days)
return nil key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
text, err = util.AESGCMDecrypt(key, text)
return string(text), err == nil
}
// SetSuperSecureCookie sets given cookie value to response header with secret string.
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, maxAge int) {
text := ctx.CookieEncrypt(secret, value)
ctx.SetSiteCookie(name, text, maxAge)
}
// CookieEncrypt encrypts a given value using the provided secret
func (ctx *Context) CookieEncrypt(secret, value string) string {
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
text, err := util.AESGCMEncrypt(key, []byte(value))
if err != nil {
panic("error encrypting cookie: " + err.Error())
}
return hex.EncodeToString(text)
} }

View file

@ -101,7 +101,7 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
}, },
// find releases without existing repository // find releases without existing repository
genericOrphanCheck("Orphaned Releases without existing repository", genericOrphanCheck("Orphaned Releases without existing repository",
"release", "repository", "`release`.repo_id=repository.id"), "release", "repository", "release.repo_id=repository.id"),
// find pulls without existing issues // find pulls without existing issues
genericOrphanCheck("Orphaned PullRequests without existing issue", genericOrphanCheck("Orphaned PullRequests without existing issue",
"pull_request", "issue", "pull_request.issue_id=issue.id"), "pull_request", "issue", "pull_request.issue_id=issue.id"),
@ -168,9 +168,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
// find protected branches without existing repository // find protected branches without existing repository
genericOrphanCheck("Protected Branches without existing repository", genericOrphanCheck("Protected Branches without existing repository",
"protected_branch", "repository", "protected_branch.repo_id=repository.id"), "protected_branch", "repository", "protected_branch.repo_id=repository.id"),
// find branches without existing repository // find deleted branches without existing repository
genericOrphanCheck("Branches without existing repository", genericOrphanCheck("Deleted Branches without existing repository",
"branch", "repository", "branch.repo_id=repository.id"), "deleted_branch", "repository", "deleted_branch.repo_id=repository.id"),
// find LFS locks without existing repository // find LFS locks without existing repository
genericOrphanCheck("LFS locks without existing repository", genericOrphanCheck("LFS locks without existing repository",
"lfs_lock", "repository", "lfs_lock.repo_id=repository.id"), "lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),

View file

@ -86,8 +86,7 @@ func (repo *Repository) IsEmpty() (bool, error) {
Stdout: &output, Stdout: &output,
Stderr: &errbuf, Stderr: &errbuf,
}); err != nil { }); err != nil {
if (err.Error() == "exit status 1" && strings.TrimSpace(errbuf.String()) == "") || err.Error() == "exit status 129" { if err.Error() == "exit status 1" && errbuf.String() == "" {
// git 2.11 exits with 129 if the repo is empty
return true, nil return true, nil
} }
return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String()) return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String())

View file

@ -19,6 +19,7 @@ var (
SecretKey string SecretKey string
InternalToken string // internal access token InternalToken string // internal access token
LogInRememberDays int LogInRememberDays int
CookieUserName string
CookieRememberName string CookieRememberName string
ReverseProxyAuthUser string ReverseProxyAuthUser string
ReverseProxyAuthEmail string ReverseProxyAuthEmail string
@ -103,6 +104,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("security") sec := rootCfg.Section("security")
InstallLock = HasInstallLock(rootCfg) InstallLock = HasInstallLock(rootCfg)
LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7) LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome")
SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY") SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
if SecretKey == "" { if SecretKey == "" {
// FIXME: https://github.com/go-gitea/gitea/issues/16832 // FIXME: https://github.com/go-gitea/gitea/issues/16832

View file

@ -63,7 +63,6 @@ type Repository struct {
Language string `json:"language"` Language string `json:"language"`
LanguagesURL string `json:"languages_url"` LanguagesURL string `json:"languages_url"`
HTMLURL string `json:"html_url"` HTMLURL string `json:"html_url"`
URL string `json:"url"`
Link string `json:"link"` Link string `json:"link"`
SSHURL string `json:"ssh_url"` SSHURL string `json:"ssh_url"`
CloneURL string `json:"clone_url"` CloneURL string `json:"clone_url"`

View file

@ -4,6 +4,10 @@
package util package util
import ( import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"io" "io"
"os" "os"
) )
@ -36,3 +40,52 @@ func CopyFile(src, dest string) error {
} }
return os.Chmod(dest, si.Mode()) return os.Chmod(dest, si.Mode())
} }
// AESGCMEncrypt (from legacy package): encrypts plaintext with the given key using AES in GCM mode. should be replaced.
func AESGCMEncrypt(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
return append(nonce, ciphertext...), nil
}
// AESGCMDecrypt (from legacy package): decrypts ciphertext with the given key using AES in GCM mode. should be replaced.
func AESGCMDecrypt(key, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
size := gcm.NonceSize()
if len(ciphertext)-size <= 0 {
return nil, errors.New("ciphertext is empty")
}
nonce := ciphertext[:size]
ciphertext = ciphertext[size:]
plainText, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plainText, nil
}

View file

@ -4,6 +4,8 @@
package util package util
import ( import (
"crypto/aes"
"crypto/rand"
"fmt" "fmt"
"os" "os"
"testing" "testing"
@ -35,3 +37,21 @@ func TestCopyFile(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, testContent, dstContent) assert.Equal(t, testContent, dstContent)
} }
func TestAESGCM(t *testing.T) {
t.Parallel()
key := make([]byte, aes.BlockSize)
_, err := rand.Read(key)
assert.NoError(t, err)
plaintext := []byte("this will be encrypted")
ciphertext, err := AESGCMEncrypt(key, plaintext)
assert.NoError(t, err)
decrypted, err := AESGCMDecrypt(key, ciphertext)
assert.NoError(t, err)
assert.Equal(t, plaintext, decrypted)
}

View file

@ -241,7 +241,7 @@ func CreateTeam(ctx *context.APIContext) {
return return
} }
apiTeam, err := convert.ToTeam(ctx, team, true) apiTeam, err := convert.ToTeam(ctx, team)
if err != nil { if err != nil {
ctx.InternalServerError(err) ctx.InternalServerError(err)
return return

View file

@ -552,16 +552,21 @@ func SubmitInstall(ctx *context.Context) {
u, _ = user_model.GetUserByName(ctx, u.Name) u, _ = user_model.GetUserByName(ctx, u.Name)
} }
if err := ctx.SetLTACookie(u); err != nil { days := 86400 * setting.LogInRememberDays
ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) ctx.SetSiteCookie(setting.CookieUserName, u.Name, days)
return
} ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
setting.CookieRememberName, u.Name, days)
// Auto-login for admin // Auto-login for admin
if err = ctx.Session.Set("uid", u.ID); err != nil { if err = ctx.Session.Set("uid", u.ID); err != nil {
ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
return return
} }
if err = ctx.Session.Set("uname", u.Name); err != nil {
ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
return
}
if err = ctx.Session.Release(); err != nil { if err = ctx.Session.Release(); err != nil {
ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)

View file

@ -5,8 +5,6 @@
package auth package auth
import ( import (
"crypto/subtle"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -51,47 +49,21 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
return false, nil return false, nil
} }
authCookie := ctx.GetSiteCookie(setting.CookieRememberName) uname := ctx.GetSiteCookie(setting.CookieUserName)
if len(authCookie) == 0 { if len(uname) == 0 {
return false, nil return false, nil
} }
isSucceed := false isSucceed := false
defer func() { defer func() {
if !isSucceed { if !isSucceed {
log.Trace("Auto login cookie is cleared: %s", authCookie) log.Trace("auto-login cookie cleared: %s", uname)
ctx.DeleteSiteCookie(setting.CookieUserName)
ctx.DeleteSiteCookie(setting.CookieRememberName) ctx.DeleteSiteCookie(setting.CookieRememberName)
} }
}() }()
lookupKey, validator, found := strings.Cut(authCookie, ":") u, err := user_model.GetUserByName(ctx, uname)
if !found {
return false, nil
}
authToken, err := auth.FindAuthToken(ctx, lookupKey)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
return false, nil
}
return false, err
}
if authToken.IsExpired() {
err = auth.DeleteAuthToken(ctx, authToken)
return false, err
}
rawValidator, err := hex.DecodeString(validator)
if err != nil {
return false, err
}
if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 {
return false, nil
}
u, err := user_model.GetUserByID(ctx, authToken.UID)
if err != nil { if err != nil {
if !user_model.IsErrUserNotExist(err) { if !user_model.IsErrUserNotExist(err) {
return false, fmt.Errorf("GetUserByName: %w", err) return false, fmt.Errorf("GetUserByName: %w", err)
@ -99,11 +71,17 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
return false, nil return false, nil
} }
if val, ok := ctx.GetSuperSecureCookie(
base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); !ok || val != u.Name {
return false, nil
}
isSucceed = true isSucceed = true
if err := updateSession(ctx, nil, map[string]any{ if err := updateSession(ctx, nil, map[string]any{
// Set session IDs // Set session IDs
"uid": authToken.UID, "uid": u.ID,
"uname": u.Name,
}); err != nil { }); err != nil {
return false, fmt.Errorf("unable to updateSession: %w", err) return false, fmt.Errorf("unable to updateSession: %w", err)
} }
@ -312,10 +290,10 @@ func handleSignIn(ctx *context.Context, u *user_model.User, remember bool) {
func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRedirect bool) string { func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRedirect bool) string {
if remember { if remember {
if err := ctx.SetLTACookie(u); err != nil { days := 86400 * setting.LogInRememberDays
ctx.ServerError("GenerateAuthToken", err) ctx.SetSiteCookie(setting.CookieUserName, u.Name, days)
return setting.AppSubURL + "/" ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
} setting.CookieRememberName, u.Name, days)
} }
if err := updateSession(ctx, []string{ if err := updateSession(ctx, []string{
@ -329,6 +307,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
"linkAccount", "linkAccount",
}, map[string]any{ }, map[string]any{
"uid": u.ID, "uid": u.ID,
"uname": u.Name,
}); err != nil { }); err != nil {
ctx.ServerError("RegenerateSession", err) ctx.ServerError("RegenerateSession", err)
return setting.AppSubURL + "/" return setting.AppSubURL + "/"
@ -389,6 +368,7 @@ func getUserName(gothUser *goth.User) string {
func HandleSignOut(ctx *context.Context) { func HandleSignOut(ctx *context.Context) {
_ = ctx.Session.Flush() _ = ctx.Session.Flush()
_ = ctx.Session.Destroy(ctx.Resp, ctx.Req) _ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
ctx.DeleteSiteCookie(setting.CookieUserName)
ctx.DeleteSiteCookie(setting.CookieRememberName) ctx.DeleteSiteCookie(setting.CookieRememberName)
ctx.Csrf.DeleteCookie(ctx) ctx.Csrf.DeleteCookie(ctx)
middleware.DeleteRedirectToCookie(ctx.Resp) middleware.DeleteRedirectToCookie(ctx.Resp)
@ -729,6 +709,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
if err := updateSession(ctx, nil, map[string]any{ if err := updateSession(ctx, nil, map[string]any{
"uid": user.ID, "uid": user.ID,
"uname": user.Name,
}); err != nil { }); err != nil {
log.Error("Unable to regenerate session for user: %-v with email: %s: %v", user, user.Email, err) log.Error("Unable to regenerate session for user: %-v with email: %s: %v", user, user.Email, err)
ctx.ServerError("ActivateUserEmail", err) ctx.ServerError("ActivateUserEmail", err)

View file

@ -1121,6 +1121,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
if !needs2FA { if !needs2FA {
if err := updateSession(ctx, nil, map[string]any{ if err := updateSession(ctx, nil, map[string]any{
"uid": u.ID, "uid": u.ID,
"uname": u.Name,
}); err != nil { }); err != nil {
ctx.ServerError("updateSession", err) ctx.ServerError("updateSession", err)
return return

View file

@ -54,7 +54,8 @@ func Home(ctx *context.Context) {
} }
// Check auto-login. // Check auto-login.
if len(ctx.GetSiteCookie(setting.CookieRememberName)) != 0 { uname := ctx.GetSiteCookie(setting.CookieUserName)
if len(uname) != 0 {
ctx.Redirect(setting.AppSubURL + "/user/login") ctx.Redirect(setting.AppSubURL + "/user/login")
return return
} }

View file

@ -78,15 +78,6 @@ func AccountPost(ctx *context.Context) {
ctx.ServerError("UpdateUser", err) ctx.ServerError("UpdateUser", err)
return return
} }
// Re-generate LTA cookie.
if len(ctx.GetSiteCookie(setting.CookieRememberName)) != 0 {
if err := ctx.SetLTACookie(ctx.Doer); err != nil {
ctx.ServerError("SetLTACookie", err)
return
}
}
log.Trace("User password updated: %s", ctx.Doer.Name) log.Trace("User password updated: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.change_password_success")) ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
} }

View file

@ -877,6 +877,9 @@ func registerRoutes(m *web.Route) {
}, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false)) }, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false))
}, ignSignIn, context_service.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code) }, ignSignIn, context_service.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code)
// ***** Release Attachment Download without Signin
m.Get("/{username}/{reponame}/releases/download/{vTag}/{fileName}", ignSignIn, context.RepoAssignment, repo.MustBeNotEmpty, repo.RedirectDownload)
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
m.Group("/settings", func() { m.Group("/settings", func() {
m.Group("", func() { m.Group("", func() {
@ -1129,9 +1132,8 @@ func registerRoutes(m *web.Route) {
m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS) m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS)
m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom) m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom)
}, ctxDataSet("EnableFeed", setting.Other.EnableFeed), }, ctxDataSet("EnableFeed", setting.Other.EnableFeed),
repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, true)) repo.MustBeNotEmpty, reqRepoReleaseReader, context.RepoRefByType(context.RepoRefTag, true))
m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment) m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, reqRepoReleaseReader, repo.GetAttachment)
m.Get("/releases/download/{vTag}/{fileName}", repo.MustBeNotEmpty, repo.RedirectDownload)
m.Group("/releases", func() { m.Group("/releases", func() {
m.Get("/new", repo.NewRelease) m.Get("/new", repo.NewRelease)
m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost) m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)

View file

@ -73,6 +73,10 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
if err != nil { if err != nil {
log.Error(fmt.Sprintf("Error setting session: %v", err)) log.Error(fmt.Sprintf("Error setting session: %v", err))
} }
err = sess.Set("uname", user.Name)
if err != nil {
log.Error(fmt.Sprintf("Error setting session: %v", err))
}
// Language setting of the user overwrites the one previously set // Language setting of the user overwrites the one previously set
// If the user does not have a locale set, we save the current one. // If the user does not have a locale set, we save the current one.

View file

@ -149,7 +149,7 @@ func VerifyAuthWithOptions(options *VerifyOptions) func(ctx *context.Context) {
// Redirect to log in page if auto-signin info is provided and has not signed in. // Redirect to log in page if auto-signin info is provided and has not signed in.
if !options.SignOutRequired && !ctx.IsSigned && if !options.SignOutRequired && !ctx.IsSigned &&
len(ctx.GetSiteCookie(setting.CookieRememberName)) > 0 { len(ctx.GetSiteCookie(setting.CookieUserName)) > 0 {
if ctx.Req.URL.Path != "/user/events" { if ctx.Req.URL.Path != "/user/events" {
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI()) middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
} }

View file

@ -126,9 +126,7 @@ func (o *OAuth2) userIDFromToken(tokenSHA string, store DataStore) int64 {
// If verification is successful returns an existing user object. // If verification is successful returns an existing user object.
// Returns nil if verification fails. // Returns nil if verification fails.
func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) {
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) &&
!gitRawReleasePathRe.MatchString(req.URL.Path) {
return nil, nil return nil, nil
} }

View file

@ -181,7 +181,6 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
Parent: parent, Parent: parent,
Mirror: repo.IsMirror, Mirror: repo.IsMirror,
HTMLURL: repo.HTMLURL(), HTMLURL: repo.HTMLURL(),
URL: repoAPIURL,
SSHURL: cloneLink.SSH, SSHURL: cloneLink.SSH,
CloneURL: cloneLink.HTTPS, CloneURL: cloneLink.HTTPS,
OriginalURL: repo.SanitizedOriginalURL(), OriginalURL: repo.SanitizedOriginalURL(),

View file

@ -1312,7 +1312,7 @@ outer:
} }
} }
return diff, nil return diff, err
} }
// CommentAsDiff returns c.Patch as *Diff // CommentAsDiff returns c.Patch as *Diff

View file

@ -170,7 +170,7 @@ func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e
func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true) text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil return createDingtalkPayload(text, text, "view release", p.Release.URL), nil
} }
func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload { func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {

View file

@ -238,7 +238,7 @@ func TestDingTalkPayload(t *testing.T) {
assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Text) assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Text)
assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Title) assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Title)
assert.Equal(t, "view release", pl.(*DingtalkPayload).ActionCard.SingleTitle) assert.Equal(t, "view release", pl.(*DingtalkPayload).ActionCard.SingleTitle)
assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) assert.Equal(t, "http://localhost:3000/api/v1/repos/test/repo/releases/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
}) })
} }

View file

@ -253,7 +253,7 @@ func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, color := getReleasePayloadInfo(p, noneLinkFormatter, false) text, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil return d.createPayload(p.Sender, text, p.Release.Note, p.Release.URL, color), nil
} }
// GetDiscordPayload converts a discord webhook into a DiscordPayload // GetDiscordPayload converts a discord webhook into a DiscordPayload

View file

@ -270,7 +270,7 @@ func TestDiscordPayload(t *testing.T) {
assert.Len(t, pl.(*DiscordPayload).Embeds, 1) assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*DiscordPayload).Embeds[0].Title) assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*DiscordPayload).Embeds[0].Title)
assert.Equal(t, "Note of first stable release", pl.(*DiscordPayload).Embeds[0].Description) assert.Equal(t, "Note of first stable release", pl.(*DiscordPayload).Embeds[0].Description)
assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*DiscordPayload).Embeds[0].URL) assert.Equal(t, "http://localhost:3000/api/v1/repos/test/repo/releases/2", pl.(*DiscordPayload).Embeds[0].URL)
assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)

View file

@ -228,7 +228,7 @@ func pullReleaseTestPayload() *api.ReleasePayload {
Target: "master", Target: "master",
Title: "First stable release", Title: "First stable release",
Note: "Note of first stable release", Note: "Note of first stable release",
HTMLURL: "http://localhost:3000/test/repo/releases/tag/v1.0", URL: "http://localhost:3000/api/v1/repos/test/repo/releases/2",
}, },
} }
} }

View file

@ -177,7 +177,7 @@ func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, e
func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
titleLink := MatrixLinkFormatter(p.PullRequest.HTMLURL, title) titleLink := MatrixLinkFormatter(p.PullRequest.URL, title)
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string var text string

View file

@ -290,7 +290,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
p.Sender, p.Sender,
title, title,
"", "",
p.Release.HTMLURL, p.Release.URL,
color, color,
&MSTeamsFact{"Tag:", p.Release.TagName}, &MSTeamsFact{"Tag:", p.Release.TagName},
), nil ), nil

View file

@ -429,7 +429,7 @@ func TestMSTeamsPayload(t *testing.T) {
} }
assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) assert.Equal(t, "http://localhost:3000/api/v1/repos/test/repo/releases/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
}) })
} }

View file

@ -223,7 +223,7 @@ func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, er
attachments = append(attachments, SlackAttachment{ attachments = append(attachments, SlackAttachment{
Color: fmt.Sprintf("%x", color), Color: fmt.Sprintf("%x", color),
Title: issueTitle, Title: issueTitle,
TitleLink: p.PullRequest.HTMLURL, TitleLink: p.PullRequest.URL,
Text: attachmentText, Text: attachmentText,
}) })
} }

View file

@ -21258,10 +21258,6 @@
"format": "date-time", "format": "date-time",
"x-go-name": "Updated" "x-go-name": "Updated"
}, },
"url": {
"type": "string",
"x-go-name": "URL"
},
"watchers_count": { "watchers_count": {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",

View file

@ -1 +0,0 @@
1032bbf17fbc0d9c95bb5418dabe8f8c99278700

View file

@ -1,163 +0,0 @@
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/hex"
"net/http"
"net/url"
"strings"
"testing"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
// GetSessionForLTACookie returns a new session with only the LTA cookie being set.
func GetSessionForLTACookie(t *testing.T, ltaCookie *http.Cookie) *TestSession {
t.Helper()
ch := http.Header{}
ch.Add("Cookie", ltaCookie.String())
cr := http.Request{Header: ch}
session := emptyTestSession(t)
baseURL, err := url.Parse(setting.AppURL)
assert.NoError(t, err)
session.jar.SetCookies(baseURL, cr.Cookies())
return session
}
// GetLTACookieValue returns the value of the LTA cookie.
func GetLTACookieValue(t *testing.T, sess *TestSession) string {
t.Helper()
rememberCookie := sess.GetCookie(setting.CookieRememberName)
assert.NotNil(t, rememberCookie)
cookieValue, err := url.QueryUnescape(rememberCookie.Value)
assert.NoError(t, err)
return cookieValue
}
// TestSessionCookie checks if the session cookie provides authentication.
func TestSessionCookie(t *testing.T) {
defer tests.PrepareTestEnv(t)()
sess := loginUser(t, "user1")
assert.NotNil(t, sess.GetCookie(setting.SessionConfig.CookieName))
req := NewRequest(t, "GET", "/user/settings")
sess.MakeRequest(t, req, http.StatusOK)
}
// TestLTACookie checks if the LTA cookie that's returned is valid, exists in the database
// and provides authentication of no session cookie is present.
func TestLTACookie(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
sess := emptyTestSession(t)
req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
"_csrf": GetCSRF(t, sess, "/user/login"),
"user_name": user.Name,
"password": userPassword,
"remember": "true",
})
sess.MakeRequest(t, req, http.StatusSeeOther)
// Checks if the database entry exist for the user.
ltaCookieValue := GetLTACookieValue(t, sess)
lookupKey, validator, found := strings.Cut(ltaCookieValue, ":")
assert.True(t, found)
rawValidator, err := hex.DecodeString(validator)
assert.NoError(t, err)
unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{LookupKey: lookupKey, HashedValidator: auth.HashValidator(rawValidator), UID: user.ID})
// Check if the LTA cookie it provides authentication.
// If LTA cookie provides authentication /user/login shouldn't return status 200.
session := GetSessionForLTACookie(t, sess.GetCookie(setting.CookieRememberName))
req = NewRequest(t, "GET", "/user/login")
session.MakeRequest(t, req, http.StatusSeeOther)
}
// TestLTAPasswordChange checks that LTA doesn't provide authentication when a
// password change has happened and that the new LTA does provide authentication.
func TestLTAPasswordChange(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
sess := loginUserWithPasswordRemember(t, user.Name, userPassword, true)
oldRememberCookie := sess.GetCookie(setting.CookieRememberName)
assert.NotNil(t, oldRememberCookie)
// Make a simple password change.
req := NewRequestWithValues(t, "POST", "/user/settings/account", map[string]string{
"_csrf": GetCSRF(t, sess, "/user/settings/account"),
"old_password": userPassword,
"password": "password2",
"retype": "password2",
})
sess.MakeRequest(t, req, http.StatusSeeOther)
rememberCookie := sess.GetCookie(setting.CookieRememberName)
assert.NotNil(t, rememberCookie)
// Check if the password really changed.
assert.NotEqualValues(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).Passwd, user.Passwd)
// /user/settings/account should provide with a new LTA cookie, so check for that.
// If LTA cookie provides authentication /user/login shouldn't return status 200.
session := GetSessionForLTACookie(t, rememberCookie)
req = NewRequest(t, "GET", "/user/login")
session.MakeRequest(t, req, http.StatusSeeOther)
// Check if the old LTA token is invalidated.
session = GetSessionForLTACookie(t, oldRememberCookie)
req = NewRequest(t, "GET", "/user/login")
session.MakeRequest(t, req, http.StatusOK)
}
// TestLTAExpiry tests that the LTA expiry works.
func TestLTAExpiry(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
sess := loginUserWithPasswordRemember(t, user.Name, userPassword, true)
ltaCookieValie := GetLTACookieValue(t, sess)
lookupKey, _, found := strings.Cut(ltaCookieValie, ":")
assert.True(t, found)
// Ensure it's not expired.
lta := unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
assert.False(t, lta.IsExpired())
// Manually stub LTA's expiry.
_, err := db.GetEngine(db.DefaultContext).ID(lta.ID).Table("forgejo_auth_token").Cols("expiry").Update(&auth.AuthorizationToken{Expiry: timeutil.TimeStampNow()})
assert.NoError(t, err)
// Ensure it's expired.
lta = unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
assert.True(t, lta.IsExpired())
// Should return 200 OK, because LTA doesn't provide authorization anymore.
session := GetSessionForLTACookie(t, sess.GetCookie(setting.CookieRememberName))
req := NewRequest(t, "GET", "/user/login")
session.MakeRequest(t, req, http.StatusOK)
// Ensure it's deleted.
unittest.AssertNotExistsBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
}

View file

@ -17,7 +17,6 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
@ -237,12 +236,6 @@ func loginUser(t testing.TB, userName string) *TestSession {
func loginUserWithPassword(t testing.TB, userName, password string) *TestSession { func loginUserWithPassword(t testing.TB, userName, password string) *TestSession {
t.Helper() t.Helper()
return loginUserWithPasswordRemember(t, userName, password, false)
}
func loginUserWithPasswordRemember(t testing.TB, userName, password string, rememberMe bool) *TestSession {
t.Helper()
req := NewRequest(t, "GET", "/user/login") req := NewRequest(t, "GET", "/user/login")
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
@ -251,7 +244,6 @@ func loginUserWithPasswordRemember(t testing.TB, userName, password string, reme
"_csrf": doc.GetCSRF(), "_csrf": doc.GetCSRF(),
"user_name": userName, "user_name": userName,
"password": password, "password": password,
"remember": strconv.FormatBool(rememberMe),
}) })
resp = MakeRequest(t, req, http.StatusSeeOther) resp = MakeRequest(t, req, http.StatusSeeOther)

View file

@ -239,20 +239,3 @@ func TestViewTagsList(t *testing.T) {
assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames) assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames)
} }
func TestDownloadReleaseAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)()
tests.PrepareAttachmentsStorage(t)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
url := repo.Link() + "/releases/download/v1.1/README.md"
req := NewRequest(t, "GET", url)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", url)
session := loginUser(t, "user2")
session.MakeRequest(t, req, http.StatusOK)
}

View file

@ -176,20 +176,6 @@ func InitTest(requireGitea bool) {
routers.InitWebInstalled(graceful.GetManager().HammerContext()) routers.InitWebInstalled(graceful.GetManager().HammerContext())
} }
func PrepareAttachmentsStorage(t testing.TB) {
// prepare attachments directory and files
assert.NoError(t, storage.Clean(storage.Attachments))
s, err := storage.NewStorage(setting.LocalStorageType, &setting.Storage{
Path: filepath.Join(filepath.Dir(setting.AppPath), "tests", "testdata", "data", "attachments"),
})
assert.NoError(t, err)
assert.NoError(t, s.IterateObjects("", func(p string, obj storage.Object) error {
_, err = storage.Copy(storage.Attachments, p, s, p)
return err
}))
}
func PrepareTestEnv(t testing.TB, skip ...int) func() { func PrepareTestEnv(t testing.TB, skip ...int) func() {
t.Helper() t.Helper()
ourSkip := 2 ourSkip := 2

View file

@ -1 +0,0 @@
# This is a release README

View file

@ -86,7 +86,6 @@ text-expander .suggestions {
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
box-shadow: 0 .5rem 1rem var(--color-shadow); box-shadow: 0 .5rem 1rem var(--color-shadow);
z-index: 100; /* needs to be > 20 to be on top of dropzone's .dz-details */
} }
text-expander .suggestions li { text-expander .suggestions li {