Compare commits
17 commits
83e30779cf
...
1991f27424
Author | SHA1 | Date | |
---|---|---|---|
1991f27424 | |||
136b8c7e0d | |||
|
e58e7bf088 | ||
|
51988ef52b | ||
|
3759c1a7c1 | ||
|
4b23f11864 | ||
|
4c21b82e18 | ||
|
3e8c3b7c09 | ||
|
5e2d16de0e | ||
|
101cfc1f82 | ||
|
fa5c61cab7 | ||
|
ab9b1b850b | ||
|
c590235171 | ||
|
13423d6eda | ||
|
1b1f878204 | ||
|
f8bf284794 | ||
|
dc6020645b |
49 changed files with 499 additions and 216 deletions
15
.woodpecker.yml
Normal file
15
.woodpecker.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
pipeline:
|
||||||
|
nulo-container:
|
||||||
|
image: docker.io/woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo: gitea.nulo.in/nulo/forgejo
|
||||||
|
tag: v1.20.4-1
|
||||||
|
registry: https://gitea.nulo.in
|
||||||
|
username: Nulo
|
||||||
|
password:
|
||||||
|
from_secret: registry_secret
|
||||||
|
secrets: [REGISTRY_SECRET]
|
||||||
|
when:
|
||||||
|
branch: "nulo/release/v1.20"
|
||||||
|
event: "push"
|
||||||
|
|
|
@ -65,10 +65,10 @@ RUN addgroup \
|
||||||
-s /bin/bash \
|
-s /bin/bash \
|
||||||
-u 1000 \
|
-u 1000 \
|
||||||
-G git \
|
-G git \
|
||||||
git && \
|
_gitea && \
|
||||||
echo "git:*" | chpasswd -e
|
echo "_gitea:*" | chpasswd -e
|
||||||
|
|
||||||
ENV USER git
|
ENV USER _gitea
|
||||||
ENV GITEA_CUSTOM /data/gitea
|
ENV GITEA_CUSTOM /data/gitea
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -89,7 +89,7 @@ endif
|
||||||
VERSION = ${GITEA_VERSION}
|
VERSION = ${GITEA_VERSION}
|
||||||
|
|
||||||
# SemVer
|
# SemVer
|
||||||
FORGEJO_VERSION := 5.0.4+0-gitea-1.20.4
|
FORGEJO_VERSION := 5.0.5+0-gitea-1.20.5
|
||||||
|
|
||||||
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)"
|
||||||
|
|
||||||
|
|
|
@ -17,13 +17,13 @@ menu:
|
||||||
|
|
||||||
# Database Preparation
|
# Database Preparation
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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
|
## MySQL/MariaDB
|
||||||
|
|
||||||
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:
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
96
models/auth/auth_token.go
Normal file
96
models/auth/auth_token.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// 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))
|
||||||
|
}
|
|
@ -140,3 +140,16 @@
|
||||||
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
|
||||||
|
|
|
@ -136,3 +136,17 @@
|
||||||
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
|
||||||
|
|
|
@ -40,6 +40,8 @@ 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.
|
||||||
|
|
25
models/forgejo_migrations/v1_20/v3.go
Normal file
25
models/forgejo_migrations/v1_20/v3.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// 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))
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ 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
|
||||||
|
@ -422,37 +423,18 @@ 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))
|
||||||
left := len(reviewIDs)
|
if err := db.GetEngine(ctx).In("id", reviewIDs).Find(&reviews); err != nil {
|
||||||
for left > 0 {
|
return err
|
||||||
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 {
|
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.
|
||||||
|
|
|
@ -349,14 +349,21 @@ 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.In("review.type", []ReviewType{ReviewTypeRequest, ReviewTypeReject, ReviewTypeApprove}),
|
builder.Eq{"review.type": ReviewTypeRequest},
|
||||||
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))
|
||||||
|
|
|
@ -380,6 +380,11 @@ 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,16 +4,14 @@
|
||||||
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/util"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"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"
|
||||||
|
@ -46,41 +44,13 @@ func (ctx *Context) GetSiteCookie(name string) string {
|
||||||
return middleware.GetSiteCookie(ctx.Req, name)
|
return middleware.GetSiteCookie(ctx.Req, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSuperSecureCookie returns given cookie value from request header with secret string.
|
// SetLTACookie will generate a LTA token and add it as an cookie.
|
||||||
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
|
func (ctx *Context) SetLTACookie(u *user_model.User) error {
|
||||||
val := ctx.GetSiteCookie(name)
|
days := 86400 * setting.LogInRememberDays
|
||||||
return ctx.CookieDecrypt(secret, val)
|
lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days)))
|
||||||
}
|
|
||||||
|
|
||||||
// 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 "", false
|
return err
|
||||||
}
|
}
|
||||||
|
ctx.SetSiteCookie(setting.CookieRememberName, lookup+":"+validator, days)
|
||||||
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
|
return nil
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 deleted branches without existing repository
|
// find branches without existing repository
|
||||||
genericOrphanCheck("Deleted Branches without existing repository",
|
genericOrphanCheck("Branches without existing repository",
|
||||||
"deleted_branch", "repository", "deleted_branch.repo_id=repository.id"),
|
"branch", "repository", "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"),
|
||||||
|
|
|
@ -86,7 +86,8 @@ func (repo *Repository) IsEmpty() (bool, error) {
|
||||||
Stdout: &output,
|
Stdout: &output,
|
||||||
Stderr: &errbuf,
|
Stderr: &errbuf,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if err.Error() == "exit status 1" && errbuf.String() == "" {
|
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
|
||||||
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())
|
||||||
|
|
|
@ -19,7 +19,6 @@ 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
|
||||||
|
@ -104,7 +103,6 @@ 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
|
||||||
|
|
|
@ -63,6 +63,7 @@ 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"`
|
||||||
|
|
|
@ -4,10 +4,6 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
@ -40,52 +36,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/aes"
|
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -37,21 +35,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -241,7 +241,7 @@ func CreateTeam(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiTeam, err := convert.ToTeam(ctx, team)
|
apiTeam, err := convert.ToTeam(ctx, team, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.InternalServerError(err)
|
ctx.InternalServerError(err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -552,18 +552,13 @@ func SubmitInstall(ctx *context.Context) {
|
||||||
u, _ = user_model.GetUserByName(ctx, u.Name)
|
u, _ = user_model.GetUserByName(ctx, u.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
days := 86400 * setting.LogInRememberDays
|
if err := ctx.SetLTACookie(u); err != nil {
|
||||||
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)
|
ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = ctx.Session.Set("uname", u.Name); err != nil {
|
|
||||||
|
// 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)
|
ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -49,21 +51,47 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
uname := ctx.GetSiteCookie(setting.CookieUserName)
|
authCookie := ctx.GetSiteCookie(setting.CookieRememberName)
|
||||||
if len(uname) == 0 {
|
if len(authCookie) == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
isSucceed := false
|
isSucceed := false
|
||||||
defer func() {
|
defer func() {
|
||||||
if !isSucceed {
|
if !isSucceed {
|
||||||
log.Trace("auto-login cookie cleared: %s", uname)
|
log.Trace("Auto login cookie is cleared: %s", authCookie)
|
||||||
ctx.DeleteSiteCookie(setting.CookieUserName)
|
|
||||||
ctx.DeleteSiteCookie(setting.CookieRememberName)
|
ctx.DeleteSiteCookie(setting.CookieRememberName)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
u, err := user_model.GetUserByName(ctx, uname)
|
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)
|
||||||
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)
|
||||||
|
@ -71,17 +99,11 @@ 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": u.ID,
|
"uid": authToken.UID,
|
||||||
"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)
|
||||||
}
|
}
|
||||||
|
@ -290,10 +312,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 {
|
||||||
days := 86400 * setting.LogInRememberDays
|
if err := ctx.SetLTACookie(u); err != nil {
|
||||||
ctx.SetSiteCookie(setting.CookieUserName, u.Name, days)
|
ctx.ServerError("GenerateAuthToken", err)
|
||||||
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
|
return setting.AppSubURL + "/"
|
||||||
setting.CookieRememberName, u.Name, days)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := updateSession(ctx, []string{
|
if err := updateSession(ctx, []string{
|
||||||
|
@ -306,8 +328,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
|
||||||
"twofaRemember",
|
"twofaRemember",
|
||||||
"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 + "/"
|
||||||
|
@ -368,7 +389,6 @@ 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)
|
||||||
|
@ -708,8 +728,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
|
||||||
log.Trace("User activated: %s", user.Name)
|
log.Trace("User activated: %s", user.Name)
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -1120,8 +1120,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
||||||
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
|
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
|
||||||
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
|
||||||
|
|
|
@ -54,8 +54,7 @@ func Home(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check auto-login.
|
// Check auto-login.
|
||||||
uname := ctx.GetSiteCookie(setting.CookieUserName)
|
if len(ctx.GetSiteCookie(setting.CookieRememberName)) != 0 {
|
||||||
if len(uname) != 0 {
|
|
||||||
ctx.Redirect(setting.AppSubURL + "/user/login")
|
ctx.Redirect(setting.AppSubURL + "/user/login")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,15 @@ 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"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -877,9 +877,6 @@ 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() {
|
||||||
|
@ -1132,8 +1129,9 @@ 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, reqRepoReleaseReader, context.RepoRefByType(context.RepoRefTag, true))
|
repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, true))
|
||||||
m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, reqRepoReleaseReader, repo.GetAttachment)
|
m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, 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)
|
||||||
|
|
|
@ -73,10 +73,6 @@ 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.
|
||||||
|
|
|
@ -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.CookieUserName)) > 0 {
|
len(ctx.GetSiteCookie(setting.CookieRememberName)) > 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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,7 +126,9 @@ 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) {
|
||||||
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) {
|
// 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) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -181,6 +181,7 @@ 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(),
|
||||||
|
|
|
@ -1312,7 +1312,7 @@ outer:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return diff, err
|
return diff, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommentAsDiff returns c.Patch as *Diff
|
// CommentAsDiff returns c.Patch as *Diff
|
||||||
|
|
|
@ -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.URL), nil
|
return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {
|
func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {
|
||||||
|
|
|
@ -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/api/v1/repos/test/repo/releases/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
|
assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.URL, color), nil
|
return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDiscordPayload converts a discord webhook into a DiscordPayload
|
// GetDiscordPayload converts a discord webhook into a DiscordPayload
|
||||||
|
|
|
@ -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/api/v1/repos/test/repo/releases/2", pl.(*DiscordPayload).Embeds[0].URL)
|
assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", 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)
|
||||||
|
|
|
@ -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",
|
||||||
URL: "http://localhost:3000/api/v1/repos/test/repo/releases/2",
|
HTMLURL: "http://localhost:3000/test/repo/releases/tag/v1.0",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.URL, title)
|
titleLink := MatrixLinkFormatter(p.PullRequest.HTMLURL, title)
|
||||||
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
|
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
|
||||||
var text string
|
var text string
|
||||||
|
|
||||||
|
|
|
@ -290,7 +290,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
||||||
p.Sender,
|
p.Sender,
|
||||||
title,
|
title,
|
||||||
"",
|
"",
|
||||||
p.Release.URL,
|
p.Release.HTMLURL,
|
||||||
color,
|
color,
|
||||||
&MSTeamsFact{"Tag:", p.Release.TagName},
|
&MSTeamsFact{"Tag:", p.Release.TagName},
|
||||||
), nil
|
), nil
|
||||||
|
|
|
@ -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/api/v1/repos/test/repo/releases/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
|
assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.URL,
|
TitleLink: p.PullRequest.HTMLURL,
|
||||||
Text: attachmentText,
|
Text: attachmentText,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
4
templates/swagger/v1_json.tmpl
generated
4
templates/swagger/v1_json.tmpl
generated
|
@ -21258,6 +21258,10 @@
|
||||||
"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",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
1032bbf17fbc0d9c95bb5418dabe8f8c99278700
|
163
tests/integration/auth_token_test.go
Normal file
163
tests/integration/auth_token_test.go
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
// 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})
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -236,6 +237,12 @@ 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)
|
||||||
|
|
||||||
|
@ -244,6 +251,7 @@ func loginUserWithPassword(t testing.TB, userName, password string) *TestSession
|
||||||
"_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)
|
||||||
|
|
||||||
|
|
|
@ -239,3 +239,20 @@ 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)
|
||||||
|
}
|
||||||
|
|
|
@ -176,6 +176,20 @@ 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
|
||||||
|
|
1
tests/testdata/data/attachments/a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22
vendored
Normal file
1
tests/testdata/data/attachments/a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# This is a release README
|
|
@ -86,6 +86,7 @@ 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 {
|
||||||
|
|
Loading…
Reference in a new issue