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}
# 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)"

View file

@ -17,13 +17,13 @@ menu:
# 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).
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:
@ -45,7 +45,7 @@ Note: All steps below requires that the database engine of your choice is instal
```sql
SET old_passwords=0;
CREATE USER 'gitea'@'%' IDENTIFIED BY 'gitea';
CREATE USER 'gitea' IDENTIFIED BY 'gitea';
```
For remote database:

View file

@ -342,7 +342,7 @@ func (stats *ActivityStats) FillReleases(repoID int64, fromTime time.Time) error
// Published releases list
sess := releasesForActivityStatement(repoID, fromTime)
sess.OrderBy("`release`.created_unix DESC")
sess.OrderBy("release.created_unix DESC")
stats.PublishedReleases = make([]*repo_model.Release, 0)
if err = sess.Find(&stats.PublishedReleases); err != nil {
return err
@ -350,7 +350,7 @@ func (stats *ActivityStats) FillReleases(repoID int64, fromTime time.Time) error
// Published releases authors
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
}
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 {
return db.GetEngine(db.DefaultContext).Where("`release`.repo_id = ?", repoID).
And("`release`.is_draft = ?", false).
And("`release`.created_unix >= ?", fromTime.Unix())
return db.GetEngine(db.DefaultContext).Where("release.repo_id = ?", repoID).
And("release.is_draft = ?", false).
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
size: 0
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_tag: false
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),
// v2 -> v3
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.

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"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
)
// CommentList defines a list of comments
@ -423,18 +422,37 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
reviewIDs := comments.getReviewIDs()
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
}
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 {
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.
// 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").
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").
From("review").
Where(builder.And(
builder.Eq{"review.type": ReviewTypeRequest},
builder.In("review.type", []ReviewType{ReviewTypeRequest, ReviewTypeReject, ReviewTypeApprove}),
builder.Or(
builder.Eq{"review.reviewer_id": reviewRequestedID},
builder.In("review.reviewer_team_id", existInTeamQuery),
),
builder.In("review.id", maxReview),
))
return sess.Where("issue.poster_id <> ?", reviewRequestedID).
And(builder.In("issue.id", subQuery))

View file

@ -380,11 +380,6 @@ func (u *User) SetPassword(passwd string) (err error) {
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 {
return err
}

View file

@ -4,14 +4,16 @@
package context
import (
"encoding/hex"
"net/http"
"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/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"github.com/minio/sha256-simd"
"golang.org/x/crypto/pbkdf2"
)
const CookieNameFlash = "gitea_flash"
@ -44,13 +46,41 @@ func (ctx *Context) GetSiteCookie(name string) string {
return middleware.GetSiteCookie(ctx.Req, name)
}
// SetLTACookie will generate a LTA token and add it as an cookie.
func (ctx *Context) SetLTACookie(u *user_model.User) error {
days := 86400 * setting.LogInRememberDays
lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days)))
// GetSuperSecureCookie returns given cookie value from request header with secret string.
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
val := ctx.GetSiteCookie(name)
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 {
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
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
genericOrphanCheck("Orphaned PullRequests without existing issue",
"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
genericOrphanCheck("Protected Branches without existing repository",
"protected_branch", "repository", "protected_branch.repo_id=repository.id"),
// find branches without existing repository
genericOrphanCheck("Branches without existing repository",
"branch", "repository", "branch.repo_id=repository.id"),
// find deleted branches without existing repository
genericOrphanCheck("Deleted Branches without existing repository",
"deleted_branch", "repository", "deleted_branch.repo_id=repository.id"),
// find LFS locks without existing repository
genericOrphanCheck("LFS locks without existing repository",
"lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),

View file

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

View file

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

View file

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

View file

@ -4,6 +4,10 @@
package util
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"io"
"os"
)
@ -36,3 +40,52 @@ func CopyFile(src, dest string) error {
}
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
import (
"crypto/aes"
"crypto/rand"
"fmt"
"os"
"testing"
@ -35,3 +37,21 @@ func TestCopyFile(t *testing.T) {
assert.NoError(t, err)
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
}
apiTeam, err := convert.ToTeam(ctx, team, true)
apiTeam, err := convert.ToTeam(ctx, team)
if err != nil {
ctx.InternalServerError(err)
return

View file

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

View file

@ -5,8 +5,6 @@
package auth
import (
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"net/http"
@ -51,47 +49,21 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
return false, nil
}
authCookie := ctx.GetSiteCookie(setting.CookieRememberName)
if len(authCookie) == 0 {
uname := ctx.GetSiteCookie(setting.CookieUserName)
if len(uname) == 0 {
return false, nil
}
isSucceed := false
defer func() {
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)
}
}()
lookupKey, validator, found := strings.Cut(authCookie, ":")
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)
u, err := user_model.GetUserByName(ctx, uname)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
return false, fmt.Errorf("GetUserByName: %w", err)
@ -99,11 +71,17 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
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
if err := updateSession(ctx, nil, map[string]any{
// Set session IDs
"uid": authToken.UID,
"uid": u.ID,
"uname": u.Name,
}); err != nil {
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 {
if remember {
if err := ctx.SetLTACookie(u); err != nil {
ctx.ServerError("GenerateAuthToken", err)
return setting.AppSubURL + "/"
}
days := 86400 * setting.LogInRememberDays
ctx.SetSiteCookie(setting.CookieUserName, u.Name, days)
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
setting.CookieRememberName, u.Name, days)
}
if err := updateSession(ctx, []string{
@ -329,6 +307,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
"linkAccount",
}, map[string]any{
"uid": u.ID,
"uname": u.Name,
}); err != nil {
ctx.ServerError("RegenerateSession", err)
return setting.AppSubURL + "/"
@ -389,6 +368,7 @@ func getUserName(gothUser *goth.User) string {
func HandleSignOut(ctx *context.Context) {
_ = ctx.Session.Flush()
_ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
ctx.DeleteSiteCookie(setting.CookieUserName)
ctx.DeleteSiteCookie(setting.CookieRememberName)
ctx.Csrf.DeleteCookie(ctx)
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{
"uid": user.ID,
"uname": user.Name,
}); err != nil {
log.Error("Unable to regenerate session for user: %-v with email: %s: %v", user, user.Email, 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 err := updateSession(ctx, nil, map[string]any{
"uid": u.ID,
"uname": u.Name,
}); err != nil {
ctx.ServerError("updateSession", err)
return

View file

@ -54,7 +54,8 @@ func Home(ctx *context.Context) {
}
// 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")
return
}

View file

@ -78,15 +78,6 @@ func AccountPost(ctx *context.Context) {
ctx.ServerError("UpdateUser", err)
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)
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))
}, 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("/settings", func() {
m.Group("", func() {
@ -1129,9 +1132,8 @@ func registerRoutes(m *web.Route) {
m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS)
m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom)
}, ctxDataSet("EnableFeed", setting.Other.EnableFeed),
repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, true))
m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment)
m.Get("/releases/download/{vTag}/{fileName}", repo.MustBeNotEmpty, repo.RedirectDownload)
repo.MustBeNotEmpty, reqRepoReleaseReader, context.RepoRefByType(context.RepoRefTag, true))
m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, reqRepoReleaseReader, repo.GetAttachment)
m.Group("/releases", func() {
m.Get("/new", repo.NewRelease)
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 {
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
// 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.
if !options.SignOutRequired && !ctx.IsSigned &&
len(ctx.GetSiteCookie(setting.CookieRememberName)) > 0 {
len(ctx.GetSiteCookie(setting.CookieUserName)) > 0 {
if ctx.Req.URL.Path != "/user/events" {
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.
// Returns nil if verification fails.
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) &&
!gitRawReleasePathRe.MatchString(req.URL.Path) {
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) {
return nil, nil
}

View file

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

View file

@ -1312,7 +1312,7 @@ outer:
}
}
return diff, nil
return diff, err
}
// 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) {
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 {

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.Title)
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) {
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

View file

@ -270,7 +270,7 @@ func TestDiscordPayload(t *testing.T) {
assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
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, "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, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)

View file

@ -228,7 +228,7 @@ func pullReleaseTestPayload() *api.ReleasePayload {
Target: "master",
Title: "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) {
senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
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)
var text string

View file

@ -290,7 +290,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
p.Sender,
title,
"",
p.Release.HTMLURL,
p.Release.URL,
color,
&MSTeamsFact{"Tag:", p.Release.TagName},
), 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[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{
Color: fmt.Sprintf("%x", color),
Title: issueTitle,
TitleLink: p.PullRequest.HTMLURL,
TitleLink: p.PullRequest.URL,
Text: attachmentText,
})
}

View file

@ -21258,10 +21258,6 @@
"format": "date-time",
"x-go-name": "Updated"
},
"url": {
"type": "string",
"x-go-name": "URL"
},
"watchers_count": {
"type": "integer",
"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"
"os"
"path/filepath"
"strconv"
"strings"
"sync/atomic"
"testing"
@ -237,12 +236,6 @@ func loginUser(t testing.TB, userName string) *TestSession {
func loginUserWithPassword(t testing.TB, userName, password string) *TestSession {
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")
resp := MakeRequest(t, req, http.StatusOK)
@ -251,7 +244,6 @@ func loginUserWithPasswordRemember(t testing.TB, userName, password string, reme
"_csrf": doc.GetCSRF(),
"user_name": userName,
"password": password,
"remember": strconv.FormatBool(rememberMe),
})
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)
}
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())
}
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() {
t.Helper()
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: 1px solid var(--color-secondary);
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 {