mirror of
https://github.com/go-gitea/gitea.git
synced 2025-11-08 05:02:38 +09:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99053ce4fa | ||
|
|
e818de179e | ||
|
|
0a87bf9016 | ||
|
|
86d99e2f38 | ||
|
|
7bfb7567b2 | ||
|
|
7619808137 | ||
|
|
b854930a96 | ||
|
|
935f5e0ad5 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -4,6 +4,18 @@ This changelog goes through the changes that have been made in each release
|
|||||||
without substantial changes to our git log; to see the highlights of what has
|
without substantial changes to our git log; to see the highlights of what has
|
||||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||||
|
|
||||||
|
## [1.24.7](https://github.com/go-gitea/gitea/releases/tag/1.24.7) - 2025-10-24
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Refactor legacy code (#35708) (#35713)
|
||||||
|
* Fixing issue #35530: Password Leak in Log Messages (#35584) (#35665)
|
||||||
|
* Fix a bug missed return (#35655) (#35671)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix inputing review comment will remove reviewer (#35591) (#35664)
|
||||||
|
* TESTING
|
||||||
|
* Mock external service in hcaptcha TestCaptcha (#35604) (#35663)
|
||||||
|
* Fix build (#35669)
|
||||||
|
|
||||||
## [1.24.6](https://github.com/go-gitea/gitea/releases/tag/1.24.6) - 2025-09-10
|
## [1.24.6](https://github.com/go-gitea/gitea/releases/tag/1.24.6) - 2025-09-10
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
|
|||||||
27
cmd/serv.go
27
cmd/serv.go
@@ -13,7 +13,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||||
@@ -31,7 +30,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/services/lfs"
|
"code.gitea.io/gitea/services/lfs"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
"github.com/kballard/go-shellquote"
|
"github.com/kballard/go-shellquote"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
@@ -131,27 +129,6 @@ func getAccessMode(verb, lfsVerb string) perm.AccessMode {
|
|||||||
return perm.AccessModeNone
|
return perm.AccessModeNone
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServCommandResults) (string, error) {
|
|
||||||
now := time.Now()
|
|
||||||
claims := lfs.Claims{
|
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
|
||||||
ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)),
|
|
||||||
NotBefore: jwt.NewNumericDate(now),
|
|
||||||
},
|
|
||||||
RepoID: results.RepoID,
|
|
||||||
Op: lfsVerb,
|
|
||||||
UserID: results.UserID,
|
|
||||||
}
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
||||||
|
|
||||||
// Sign and get the complete encoded token as a string using the secret
|
|
||||||
tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err)
|
|
||||||
}
|
|
||||||
return "Bearer " + tokenString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runServ(c *cli.Context) error {
|
func runServ(c *cli.Context) error {
|
||||||
ctx, cancel := installSignals()
|
ctx, cancel := installSignals()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -284,7 +261,7 @@ func runServ(c *cli.Context) error {
|
|||||||
|
|
||||||
// LFS SSH protocol
|
// LFS SSH protocol
|
||||||
if verb == git.CmdVerbLfsTransfer {
|
if verb == git.CmdVerbLfsTransfer {
|
||||||
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -295,7 +272,7 @@ func runServ(c *cli.Context) error {
|
|||||||
if verb == git.CmdVerbLfsAuthenticate {
|
if verb == git.CmdVerbLfsAuthenticate {
|
||||||
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
|
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
|
||||||
|
|
||||||
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,13 +67,6 @@ func (key *PublicKey) OmitEmail() string {
|
|||||||
return strings.Join(strings.Split(key.Content, " ")[:2], " ")
|
return strings.Join(strings.Split(key.Content, " ")[:2], " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedString returns formatted public key string for authorized_keys file.
|
|
||||||
//
|
|
||||||
// TODO: Consider dropping this function
|
|
||||||
func (key *PublicKey) AuthorizedString() string {
|
|
||||||
return AuthorizedStringForKey(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addKey(ctx context.Context, key *PublicKey) (err error) {
|
func addKey(ctx context.Context, key *PublicKey) (err error) {
|
||||||
if len(key.Fingerprint) == 0 {
|
if len(key.Fingerprint) == 0 {
|
||||||
key.Fingerprint, err = CalcFingerprint(key.Content)
|
key.Fingerprint, err = CalcFingerprint(key.Content)
|
||||||
|
|||||||
@@ -17,29 +17,13 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// _____ __ .__ .__ .___
|
// AuthorizedStringCommentPrefix is a magic tag
|
||||||
// / _ \ __ ___/ |_| |__ ___________|__|_______ ____ __| _/
|
// some functions like RegeneratePublicKeys needs this tag to skip the keys generated by Gitea, while keep other keys
|
||||||
// / /_\ \| | \ __\ | \ / _ \_ __ \ \___ // __ \ / __ |
|
const AuthorizedStringCommentPrefix = `# gitea public key`
|
||||||
// / | \ | /| | | Y ( <_> ) | \/ |/ /\ ___// /_/ |
|
|
||||||
// \____|__ /____/ |__| |___| /\____/|__| |__/_____ \\___ >____ |
|
|
||||||
// \/ \/ \/ \/ \/
|
|
||||||
// ____ __.
|
|
||||||
// | |/ _|____ ___.__. ______
|
|
||||||
// | <_/ __ < | |/ ___/
|
|
||||||
// | | \ ___/\___ |\___ \
|
|
||||||
// |____|__ \___ > ____/____ >
|
|
||||||
// \/ \/\/ \/
|
|
||||||
//
|
|
||||||
// This file contains functions for creating authorized_keys files
|
|
||||||
//
|
|
||||||
// There is a dependence on the database within RegeneratePublicKeys however most of these functions probably belong in a module
|
|
||||||
|
|
||||||
const (
|
|
||||||
tplCommentPrefix = `# gitea public key`
|
|
||||||
tplPublicKey = tplCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s` + "\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
var sshOpLocker sync.Mutex
|
var sshOpLocker sync.Mutex
|
||||||
|
|
||||||
@@ -50,17 +34,45 @@ func WithSSHOpLocker(f func() error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
|
// AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
|
||||||
func AuthorizedStringForKey(key *PublicKey) string {
|
func AuthorizedStringForKey(key *PublicKey) (string, error) {
|
||||||
sb := &strings.Builder{}
|
sb := &strings.Builder{}
|
||||||
_ = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sb, map[string]any{
|
_, err := writeAuthorizedStringForKey(key, sb)
|
||||||
|
return sb.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAuthorizedStringForValidKey writes the authorized key for the provided key. If the key is invalid, it does nothing.
|
||||||
|
func WriteAuthorizedStringForValidKey(key *PublicKey, w io.Writer) error {
|
||||||
|
validKey, err := writeAuthorizedStringForKey(key, w)
|
||||||
|
if !validKey {
|
||||||
|
log.Debug("WriteAuthorizedStringForValidKey: key %s is not valid: %v", key, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, err error) {
|
||||||
|
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s %s` + "\n"
|
||||||
|
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
// now the key is valid, the code below could only return template/IO related errors
|
||||||
|
sbCmd := &strings.Builder{}
|
||||||
|
err = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sbCmd, map[string]any{
|
||||||
"AppPath": util.ShellEscape(setting.AppPath),
|
"AppPath": util.ShellEscape(setting.AppPath),
|
||||||
"AppWorkPath": util.ShellEscape(setting.AppWorkPath),
|
"AppWorkPath": util.ShellEscape(setting.AppWorkPath),
|
||||||
"CustomConf": util.ShellEscape(setting.CustomConf),
|
"CustomConf": util.ShellEscape(setting.CustomConf),
|
||||||
"CustomPath": util.ShellEscape(setting.CustomPath),
|
"CustomPath": util.ShellEscape(setting.CustomPath),
|
||||||
"Key": key,
|
"Key": key,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
return fmt.Sprintf(tplPublicKey, util.ShellEscape(sb.String()), key.Content)
|
return true, err
|
||||||
|
}
|
||||||
|
sshCommandEscaped := util.ShellEscape(sbCmd.String())
|
||||||
|
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
|
||||||
|
sshKeyComment := fmt.Sprintf("user-%d", key.OwnerID)
|
||||||
|
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKeyMarshalled, sshKeyComment)
|
||||||
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
|
// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
|
||||||
@@ -112,7 +124,7 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
|||||||
if key.Type == KeyTypePrincipal {
|
if key.Type == KeyTypePrincipal {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err = f.WriteString(key.AuthorizedString()); err != nil {
|
if err = WriteAuthorizedStringForValidKey(key, f); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,10 +132,9 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RegeneratePublicKeys regenerates the authorized_keys file
|
// RegeneratePublicKeys regenerates the authorized_keys file
|
||||||
func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
|
func RegeneratePublicKeys(ctx context.Context, t io.Writer) error {
|
||||||
if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
|
if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
|
||||||
_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
|
return WriteAuthorizedStringForValidKey(bean.(*PublicKey), t)
|
||||||
return err
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -144,11 +155,11 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
|
|||||||
scanner := bufio.NewScanner(f)
|
scanner := bufio.NewScanner(f)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
if strings.HasPrefix(line, AuthorizedStringCommentPrefix) {
|
||||||
scanner.Scan()
|
scanner.Scan()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err = t.WriteString(line + "\n")
|
_, err = io.WriteString(t, line+"\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, mig
|
|||||||
reviewersMap := make(map[int64][]*Review) // key is reviewer id
|
reviewersMap := make(map[int64][]*Review) // key is reviewer id
|
||||||
originalReviewersMap := make(map[int64][]*Review) // key is original author id
|
originalReviewersMap := make(map[int64][]*Review) // key is original author id
|
||||||
reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id
|
reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id
|
||||||
countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest}
|
countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, ReviewTypeComment}
|
||||||
for _, review := range reviews {
|
for _, review := range reviews {
|
||||||
if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed {
|
if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed {
|
||||||
if review.OriginalAuthorID != 0 {
|
if review.OriginalAuthorID != 0 {
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ func TestGetReviewersByIssueID(t *testing.T) {
|
|||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
||||||
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
@@ -130,6 +131,12 @@ func TestGetReviewersByIssueID(t *testing.T) {
|
|||||||
|
|
||||||
expectedReviews := []*issues_model.Review{}
|
expectedReviews := []*issues_model.Review{}
|
||||||
expectedReviews = append(expectedReviews,
|
expectedReviews = append(expectedReviews,
|
||||||
|
&issues_model.Review{
|
||||||
|
ID: 5,
|
||||||
|
Reviewer: user1,
|
||||||
|
Type: issues_model.ReviewTypeComment,
|
||||||
|
UpdatedUnix: 946684810,
|
||||||
|
},
|
||||||
&issues_model.Review{
|
&issues_model.Review{
|
||||||
ID: 7,
|
ID: 7,
|
||||||
Reviewer: org3,
|
Reviewer: org3,
|
||||||
@@ -168,8 +175,9 @@ func TestGetReviewersByIssueID(t *testing.T) {
|
|||||||
for _, review := range allReviews {
|
for _, review := range allReviews {
|
||||||
assert.NoError(t, review.LoadReviewer(db.DefaultContext))
|
assert.NoError(t, review.LoadReviewer(db.DefaultContext))
|
||||||
}
|
}
|
||||||
if assert.Len(t, allReviews, 5) {
|
if assert.Len(t, allReviews, 6) {
|
||||||
for i, review := range allReviews {
|
for i, review := range allReviews {
|
||||||
|
assert.Equal(t, expectedReviews[i].ID, review.ID)
|
||||||
assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer)
|
assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer)
|
||||||
assert.Equal(t, expectedReviews[i].Type, review.Type)
|
assert.Equal(t, expectedReviews[i].Type, review.Type)
|
||||||
assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix)
|
assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix)
|
||||||
|
|||||||
@@ -348,10 +348,8 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
|||||||
|
|
||||||
for _, u := range repo.Units {
|
for _, u := range repo.Units {
|
||||||
for _, team := range teams {
|
for _, team := range teams {
|
||||||
unitAccessMode := minAccessMode
|
teamMode, _ := team.UnitAccessModeEx(ctx, u.Type)
|
||||||
if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist {
|
unitAccessMode := max(perm.unitsMode[u.Type], minAccessMode, teamMode)
|
||||||
unitAccessMode = max(perm.unitsMode[u.Type], unitAccessMode, teamMode)
|
|
||||||
}
|
|
||||||
perm.unitsMode[u.Type] = unitAccessMode
|
perm.unitsMode[u.Type] = unitAccessMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,4 +197,37 @@ func TestGetUserRepoPermission(t *testing.T) {
|
|||||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // org private repo, same org as repo 32
|
||||||
|
require.NoError(t, repo3.LoadOwner(ctx))
|
||||||
|
require.True(t, repo3.Owner.IsOrganization())
|
||||||
|
require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}, &Access{})) // The user has access set of that repo, remove it, it is useless for our test
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo3.ID}))
|
||||||
|
t.Run("DoerWithNoopTeamOnPrivateRepo", func(t *testing.T) {
|
||||||
|
perm, err := GetUserRepoPermission(ctx, repo3, user)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||||
|
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
||||||
|
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeIssues])
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone}))
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeIssues, AccessMode: perm_model.AccessModeRead}))
|
||||||
|
t.Run("DoerWithReadIssueTeamOnPrivateRepo", func(t *testing.T) {
|
||||||
|
perm, err := GetUserRepoPermission(ctx, repo3, user)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||||
|
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
||||||
|
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
||||||
|
require.NoError(t, db.Insert(ctx, Access{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
||||||
|
t.Run("DoerWithReadIssueTeamAndWriteCollaboratorOnPrivateRepo", func(t *testing.T) {
|
||||||
|
perm, err := GetUserRepoPermission(ctx, repo3, user)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode)
|
||||||
|
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||||
|
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeIssues])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,16 +137,9 @@ func DeleteUploads(ctx context.Context, uploads ...*Upload) (err error) {
|
|||||||
|
|
||||||
for _, upload := range uploads {
|
for _, upload := range uploads {
|
||||||
localPath := upload.LocalPath()
|
localPath := upload.LocalPath()
|
||||||
isFile, err := util.IsFile(localPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to check if %s is a file. Error: %v", localPath, err)
|
|
||||||
}
|
|
||||||
if !isFile {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := util.Remove(localPath); err != nil {
|
if err := util.Remove(localPath); err != nil {
|
||||||
return fmt.Errorf("remove upload: %w", err)
|
// just continue, don't fail the whole operation if a file is missing (removed by others)
|
||||||
|
log.Error("unable to remove upload file %s: %v", localPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,30 +51,16 @@ func GetHook(repoPath, name string) (*Hook, error) {
|
|||||||
name: name,
|
name: name,
|
||||||
path: filepath.Join(repoPath, "hooks", name+".d", name),
|
path: filepath.Join(repoPath, "hooks", name+".d", name),
|
||||||
}
|
}
|
||||||
isFile, err := util.IsFile(h.path)
|
if data, err := os.ReadFile(h.path); err == nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isFile {
|
|
||||||
data, err := os.ReadFile(h.path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h.IsActive = true
|
h.IsActive = true
|
||||||
h.Content = string(data)
|
h.Content = string(data)
|
||||||
return h, nil
|
return h, nil
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
samplePath := filepath.Join(repoPath, "hooks", name+".sample")
|
samplePath := filepath.Join(repoPath, "hooks", name+".sample")
|
||||||
isFile, err = util.IsFile(samplePath)
|
if data, err := os.ReadFile(samplePath); err == nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isFile {
|
|
||||||
data, err := os.ReadFile(samplePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h.Sample = string(data)
|
h.Sample = string(data)
|
||||||
}
|
}
|
||||||
return h, nil
|
return h, nil
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ func TestParseGitURLs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kase: "git@[fe80:14fc:cec5:c174:d88%2510]:go-gitea/gitea.git",
|
kase: "git@[fe80::14fc:cec5:c174:d88%2510]:go-gitea/gitea.git",
|
||||||
expected: &GitURL{
|
expected: &GitURL{
|
||||||
URL: &url.URL{
|
URL: &url.URL{
|
||||||
Scheme: "ssh",
|
Scheme: "ssh",
|
||||||
User: url.User("git"),
|
User: url.User("git"),
|
||||||
Host: "[fe80:14fc:cec5:c174:d88%10]",
|
Host: "[fe80::14fc:cec5:c174:d88%10]",
|
||||||
Path: "go-gitea/gitea.git",
|
Path: "go-gitea/gitea.git",
|
||||||
},
|
},
|
||||||
extraMark: 1,
|
extraMark: 1,
|
||||||
@@ -137,11 +137,11 @@ func TestParseGitURLs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kase: "https://[fe80:14fc:cec5:c174:d88%2510]:20/go-gitea/gitea.git",
|
kase: "https://[fe80::14fc:cec5:c174:d88%2510]:20/go-gitea/gitea.git",
|
||||||
expected: &GitURL{
|
expected: &GitURL{
|
||||||
URL: &url.URL{
|
URL: &url.URL{
|
||||||
Scheme: "https",
|
Scheme: "https",
|
||||||
Host: "[fe80:14fc:cec5:c174:d88%10]:20",
|
Host: "[fe80::14fc:cec5:c174:d88%10]:20",
|
||||||
Path: "/go-gitea/gitea.git",
|
Path: "/go-gitea/gitea.git",
|
||||||
},
|
},
|
||||||
extraMark: 0,
|
extraMark: 0,
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
package hcaptcha
|
package hcaptcha
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -21,6 +24,33 @@ func TestMain(m *testing.M) {
|
|||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockTransport struct{}
|
||||||
|
|
||||||
|
func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
if req.URL.String() != verifyURL {
|
||||||
|
return nil, errors.New("unsupported url")
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyValues, err := url.ParseQuery(string(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseText string
|
||||||
|
if bodyValues.Get("response") == dummyToken {
|
||||||
|
responseText = `{"success":true,"credit":false,"hostname":"dummy-key-pass","challenge_ts":"2025-10-08T16:02:56.136Z"}`
|
||||||
|
} else {
|
||||||
|
responseText = `{"success":false,"error-codes":["invalid-input-response"]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Response{Request: req, Body: io.NopCloser(strings.NewReader(responseText))}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestCaptcha(t *testing.T) {
|
func TestCaptcha(t *testing.T) {
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -54,7 +84,8 @@ func TestCaptcha(t *testing.T) {
|
|||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
client, err := New(tc.Secret, WithHTTP(&http.Client{
|
client, err := New(tc.Secret, WithHTTP(&http.Client{
|
||||||
Timeout: time.Second * 5,
|
Timeout: time.Second * 5,
|
||||||
|
Transport: mockTransport{},
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// The only error that can be returned from creating a client
|
// The only error that can be returned from creating a client
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/process"
|
"code.gitea.io/gitea/modules/process"
|
||||||
"code.gitea.io/gitea/modules/queue"
|
"code.gitea.io/gitea/modules/queue"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -166,12 +167,12 @@ func Init() {
|
|||||||
log.Fatal("PID: %d Unable to initialize the bleve Repository Indexer at path: %s Error: %v", os.Getpid(), setting.Indexer.RepoPath, err)
|
log.Fatal("PID: %d Unable to initialize the bleve Repository Indexer at path: %s Error: %v", os.Getpid(), setting.Indexer.RepoPath, err)
|
||||||
}
|
}
|
||||||
case "elasticsearch":
|
case "elasticsearch":
|
||||||
log.Info("PID: %d Initializing Repository Indexer at: %s", os.Getpid(), setting.Indexer.RepoConnStr)
|
log.Info("PID: %d Initializing Repository Indexer at: %s", os.Getpid(), util.SanitizeCredentialURLs(setting.Indexer.RepoConnStr))
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
log.Error("PANIC whilst initializing repository indexer: %v\nStacktrace: %s", err, log.Stack(2))
|
log.Error("PANIC whilst initializing repository indexer: %v\nStacktrace: %s", err, log.Stack(2))
|
||||||
log.Error("The indexer files are likely corrupted and may need to be deleted")
|
log.Error("The indexer files are likely corrupted and may need to be deleted")
|
||||||
log.Error("You can completely remove the \"%s\" index to make Gitea recreate the indexes", setting.Indexer.RepoConnStr)
|
log.Error("You can completely remove the \"%s\" index to make Gitea recreate the indexes", util.SanitizeCredentialURLs(setting.Indexer.RepoConnStr))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -181,7 +182,7 @@ func Init() {
|
|||||||
cancel()
|
cancel()
|
||||||
(*globalIndexer.Load()).Close()
|
(*globalIndexer.Load()).Close()
|
||||||
close(waitChannel)
|
close(waitChannel)
|
||||||
log.Fatal("PID: %d Unable to initialize the elasticsearch Repository Indexer connstr: %s Error: %v", os.Getpid(), setting.Indexer.RepoConnStr, err)
|
log.Fatal("PID: %d Unable to initialize the elasticsearch Repository Indexer connstr: %s Error: %v", os.Getpid(), util.SanitizeCredentialURLs(setting.Indexer.RepoConnStr), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/process"
|
"code.gitea.io/gitea/modules/process"
|
||||||
"code.gitea.io/gitea/modules/queue"
|
"code.gitea.io/gitea/modules/queue"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IndexerMetadata is used to send data to the queue, so it contains only the ids.
|
// IndexerMetadata is used to send data to the queue, so it contains only the ids.
|
||||||
@@ -100,7 +101,7 @@ func InitIssueIndexer(syncReindex bool) {
|
|||||||
issueIndexer = elasticsearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueIndexerName)
|
issueIndexer = elasticsearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueIndexerName)
|
||||||
existed, err = issueIndexer.Init(ctx)
|
existed, err = issueIndexer.Init(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err)
|
log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", util.SanitizeCredentialURLs(setting.Indexer.IssueConnStr), err)
|
||||||
}
|
}
|
||||||
case "db":
|
case "db":
|
||||||
issueIndexer = db.GetIndexer()
|
issueIndexer = db.GetIndexer()
|
||||||
@@ -108,7 +109,7 @@ func InitIssueIndexer(syncReindex bool) {
|
|||||||
issueIndexer = meilisearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueConnAuth, setting.Indexer.IssueIndexerName)
|
issueIndexer = meilisearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueConnAuth, setting.Indexer.IssueIndexerName)
|
||||||
existed, err = issueIndexer.Init(ctx)
|
existed, err = issueIndexer.Init(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err)
|
log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", util.SanitizeCredentialURLs(setting.Indexer.IssueConnStr), err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType)
|
log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType)
|
||||||
|
|||||||
@@ -202,11 +202,11 @@ func NewConfigProviderFromFile(file string) (ConfigProvider, error) {
|
|||||||
loadedFromEmpty := true
|
loadedFromEmpty := true
|
||||||
|
|
||||||
if file != "" {
|
if file != "" {
|
||||||
isFile, err := util.IsFile(file)
|
isExist, err := util.IsExist(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to check if %q is a file. Error: %v", file, err)
|
return nil, fmt.Errorf("unable to check if %q exists: %v", file, err)
|
||||||
}
|
}
|
||||||
if isFile {
|
if isExist {
|
||||||
if err = cfg.Append(file); err != nil {
|
if err = cfg.Append(file); err != nil {
|
||||||
return nil, fmt.Errorf("failed to load config file %q: %v", file, err)
|
return nil, fmt.Errorf("failed to load config file %q: %v", file, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,15 +115,10 @@ func IsDir(dir string) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFile returns true if given path is a file,
|
func IsRegularFile(filePath string) (bool, error) {
|
||||||
// or returns false when it's a directory or does not exist.
|
f, err := os.Lstat(filePath)
|
||||||
func IsFile(filePath string) (bool, error) {
|
|
||||||
f, err := os.Stat(filePath)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return !f.IsDir(), nil
|
return f.Mode().IsRegular(), nil
|
||||||
}
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func UpdatePublicKeyInRepo(ctx *context.PrivateContext) {
|
|||||||
ctx.PlainText(http.StatusOK, "success")
|
ctx.PlainText(http.StatusOK, "success")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedPublicKeyByContent searches content as prefix (leak e-mail part)
|
// AuthorizedPublicKeyByContent searches content as prefix (without comment part)
|
||||||
// and returns public key found.
|
// and returns public key found.
|
||||||
func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) {
|
func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) {
|
||||||
content := ctx.FormString("content")
|
content := ctx.FormString("content")
|
||||||
@@ -57,5 +57,14 @@ func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.PlainText(http.StatusOK, publicKey.AuthorizedString())
|
|
||||||
|
authorizedString, err := asymkey_model.AuthorizedStringForKey(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||||
|
Err: err.Error(),
|
||||||
|
UserMsg: "invalid public key",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.PlainText(http.StatusOK, authorizedString)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -639,6 +639,7 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
|
|||||||
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
|
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
|
||||||
ErrorDescription: "cannot proceed your request",
|
ErrorDescription: "cannot proceed your request",
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
resp, tokenErr := oauth2_provider.NewAccessTokenResponse(ctx, authorizationCode.Grant, serverKey, clientKey)
|
resp, tokenErr := oauth2_provider.NewAccessTokenResponse(ctx, authorizationCode.Grant, serverKey, clientKey)
|
||||||
if tokenErr != nil {
|
if tokenErr != nil {
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ import (
|
|||||||
// There is a dependence on the database within RewriteAllPrincipalKeys & RegeneratePrincipalKeys
|
// There is a dependence on the database within RewriteAllPrincipalKeys & RegeneratePrincipalKeys
|
||||||
// The sshOpLocker is used from ssh_key_authorized_keys.go
|
// The sshOpLocker is used from ssh_key_authorized_keys.go
|
||||||
|
|
||||||
const (
|
const authorizedPrincipalsFile = "authorized_principals"
|
||||||
authorizedPrincipalsFile = "authorized_principals"
|
|
||||||
tplCommentPrefix = `# gitea public key`
|
|
||||||
)
|
|
||||||
|
|
||||||
// RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
|
// RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
|
||||||
// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
|
// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
|
||||||
@@ -90,10 +87,9 @@ func rewriteAllPrincipalKeys(ctx context.Context) error {
|
|||||||
return util.Rename(tmpPath, fPath)
|
return util.Rename(tmpPath, fPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
|
func regeneratePrincipalKeys(ctx context.Context, t io.Writer) error {
|
||||||
if err := db.GetEngine(ctx).Where("type = ?", asymkey_model.KeyTypePrincipal).Iterate(new(asymkey_model.PublicKey), func(idx int, bean any) (err error) {
|
if err := db.GetEngine(ctx).Where("type = ?", asymkey_model.KeyTypePrincipal).Iterate(new(asymkey_model.PublicKey), func(idx int, bean any) (err error) {
|
||||||
_, err = t.WriteString((bean.(*asymkey_model.PublicKey)).AuthorizedString())
|
return asymkey_model.WriteAuthorizedStringForValidKey(bean.(*asymkey_model.PublicKey), t)
|
||||||
return err
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -114,11 +110,11 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
|
|||||||
scanner := bufio.NewScanner(f)
|
scanner := bufio.NewScanner(f)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
if strings.HasPrefix(line, asymkey_model.AuthorizedStringCommentPrefix) {
|
||||||
scanner.Scan()
|
scanner.Scan()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err = t.WriteString(line + "\n")
|
_, err = io.WriteString(t, line+"\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ import (
|
|||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tplCommentPrefix = `# gitea public key`
|
|
||||||
|
|
||||||
func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) error {
|
func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||||
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
|
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
|
||||||
return nil
|
return nil
|
||||||
@@ -47,7 +45,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
|
|||||||
scanner := bufio.NewScanner(f)
|
scanner := bufio.NewScanner(f)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
if strings.HasPrefix(line, asymkey_model.AuthorizedStringCommentPrefix) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
linesInAuthorizedKeys.Add(line)
|
linesInAuthorizedKeys.Add(line)
|
||||||
@@ -67,7 +65,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
|
|||||||
scanner = bufio.NewScanner(regenerated)
|
scanner = bufio.NewScanner(regenerated)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
if strings.HasPrefix(line, asymkey_model.AuthorizedStringCommentPrefix) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if linesInAuthorizedKeys.Contains(line) {
|
if linesInAuthorizedKeys.Contains(line) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
@@ -51,6 +52,33 @@ type Claims struct {
|
|||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthTokenOptions struct {
|
||||||
|
Op string
|
||||||
|
UserID int64
|
||||||
|
RepoID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLFSAuthTokenWithBearer(opts AuthTokenOptions) (string, error) {
|
||||||
|
now := time.Now()
|
||||||
|
claims := Claims{
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)),
|
||||||
|
NotBefore: jwt.NewNumericDate(now),
|
||||||
|
},
|
||||||
|
RepoID: opts.RepoID,
|
||||||
|
Op: opts.Op,
|
||||||
|
UserID: opts.UserID,
|
||||||
|
}
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
|
// Sign and get the complete encoded token as a string using the secret
|
||||||
|
tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to sign LFS JWT token: %w", err)
|
||||||
|
}
|
||||||
|
return "Bearer " + tokenString, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DownloadLink builds a URL to download the object.
|
// DownloadLink builds a URL to download the object.
|
||||||
func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {
|
func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {
|
||||||
return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid))
|
return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid))
|
||||||
@@ -557,9 +585,6 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm_model.AccessMode) (*user_model.User, error) {
|
func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm_model.AccessMode) (*user_model.User, error) {
|
||||||
if !strings.Contains(tokenSHA, ".") {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
token, err := jwt.ParseWithClaims(tokenSHA, &Claims{}, func(t *jwt.Token) (any, error) {
|
token, err := jwt.ParseWithClaims(tokenSHA, &Claims{}, func(t *jwt.Token) (any, error) {
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
@@ -567,7 +592,7 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo
|
|||||||
return setting.LFS.JWTSecretBytes, nil
|
return setting.LFS.JWTSecretBytes, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, errors.New("invalid token")
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, claimsOk := token.Claims.(*Claims)
|
claims, claimsOk := token.Claims.(*Claims)
|
||||||
|
|||||||
51
services/lfs/server_test.go
Normal file
51
services/lfs/server_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
perm_model "code.gitea.io/gitea/models/perm"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
unittest.MainTest(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthenticate(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
|
||||||
|
token2, _ := GetLFSAuthTokenWithBearer(AuthTokenOptions{Op: "download", UserID: 2, RepoID: 1})
|
||||||
|
_, token2, _ = strings.Cut(token2, " ")
|
||||||
|
ctx, _ := contexttest.MockContext(t, "/")
|
||||||
|
|
||||||
|
t.Run("handleLFSToken", func(t *testing.T) {
|
||||||
|
u, err := handleLFSToken(ctx, "", repo1, perm_model.AccessModeRead)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, u)
|
||||||
|
|
||||||
|
u, err = handleLFSToken(ctx, "invalid", repo1, perm_model.AccessModeRead)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, u)
|
||||||
|
|
||||||
|
u, err = handleLFSToken(ctx, token2, repo1, perm_model.AccessModeRead)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 2, u.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("authenticate", func(t *testing.T) {
|
||||||
|
const prefixBearer = "Bearer "
|
||||||
|
assert.False(t, authenticate(ctx, repo1, "", true, false))
|
||||||
|
assert.False(t, authenticate(ctx, repo1, prefixBearer+"invalid", true, false))
|
||||||
|
assert.True(t, authenticate(ctx, repo1, prefixBearer+token2, true, false))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
@@ -39,29 +40,41 @@ type expansion struct {
|
|||||||
Transformers []transformer
|
Transformers []transformer
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultTransformers = []transformer{
|
var globalVars = sync.OnceValue(func() (ret struct {
|
||||||
{Name: "SNAKE", Transform: xstrings.ToSnakeCase},
|
defaultTransformers []transformer
|
||||||
{Name: "KEBAB", Transform: xstrings.ToKebabCase},
|
fileNameSanitizeRegexp *regexp.Regexp
|
||||||
{Name: "CAMEL", Transform: xstrings.ToCamelCase},
|
},
|
||||||
{Name: "PASCAL", Transform: xstrings.ToPascalCase},
|
) {
|
||||||
{Name: "LOWER", Transform: strings.ToLower},
|
ret.defaultTransformers = []transformer{
|
||||||
{Name: "UPPER", Transform: strings.ToUpper},
|
{Name: "SNAKE", Transform: xstrings.ToSnakeCase},
|
||||||
{Name: "TITLE", Transform: util.ToTitleCase},
|
{Name: "KEBAB", Transform: xstrings.ToKebabCase},
|
||||||
}
|
{Name: "CAMEL", Transform: xstrings.ToCamelCase},
|
||||||
|
{Name: "PASCAL", Transform: xstrings.ToPascalCase},
|
||||||
|
{Name: "LOWER", Transform: strings.ToLower},
|
||||||
|
{Name: "UPPER", Transform: strings.ToUpper},
|
||||||
|
{Name: "TITLE", Transform: util.ToTitleCase},
|
||||||
|
}
|
||||||
|
|
||||||
func generateExpansion(ctx context.Context, src string, templateRepo, generateRepo *repo_model.Repository, sanitizeFileName bool) string {
|
// invalid filename contents, based on https://github.com/sindresorhus/filename-reserved-regex
|
||||||
|
// "COM10" needs to be opened with UNC "\\.\COM10" on Windows, so itself is valid
|
||||||
|
ret.fileNameSanitizeRegexp = regexp.MustCompile(`(?i)[<>:"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`)
|
||||||
|
return ret
|
||||||
|
})
|
||||||
|
|
||||||
|
func generateExpansion(ctx context.Context, src string, templateRepo, generateRepo *repo_model.Repository) string {
|
||||||
|
transformers := globalVars().defaultTransformers
|
||||||
year, month, day := time.Now().Date()
|
year, month, day := time.Now().Date()
|
||||||
expansions := []expansion{
|
expansions := []expansion{
|
||||||
{Name: "YEAR", Value: strconv.Itoa(year), Transformers: nil},
|
{Name: "YEAR", Value: strconv.Itoa(year), Transformers: nil},
|
||||||
{Name: "MONTH", Value: fmt.Sprintf("%02d", int(month)), Transformers: nil},
|
{Name: "MONTH", Value: fmt.Sprintf("%02d", int(month)), Transformers: nil},
|
||||||
{Name: "MONTH_ENGLISH", Value: month.String(), Transformers: defaultTransformers},
|
{Name: "MONTH_ENGLISH", Value: month.String(), Transformers: transformers},
|
||||||
{Name: "DAY", Value: fmt.Sprintf("%02d", day), Transformers: nil},
|
{Name: "DAY", Value: fmt.Sprintf("%02d", day), Transformers: nil},
|
||||||
{Name: "REPO_NAME", Value: generateRepo.Name, Transformers: defaultTransformers},
|
{Name: "REPO_NAME", Value: generateRepo.Name, Transformers: transformers},
|
||||||
{Name: "TEMPLATE_NAME", Value: templateRepo.Name, Transformers: defaultTransformers},
|
{Name: "TEMPLATE_NAME", Value: templateRepo.Name, Transformers: transformers},
|
||||||
{Name: "REPO_DESCRIPTION", Value: generateRepo.Description, Transformers: nil},
|
{Name: "REPO_DESCRIPTION", Value: generateRepo.Description, Transformers: nil},
|
||||||
{Name: "TEMPLATE_DESCRIPTION", Value: templateRepo.Description, Transformers: nil},
|
{Name: "TEMPLATE_DESCRIPTION", Value: templateRepo.Description, Transformers: nil},
|
||||||
{Name: "REPO_OWNER", Value: generateRepo.OwnerName, Transformers: defaultTransformers},
|
{Name: "REPO_OWNER", Value: generateRepo.OwnerName, Transformers: transformers},
|
||||||
{Name: "TEMPLATE_OWNER", Value: templateRepo.OwnerName, Transformers: defaultTransformers},
|
{Name: "TEMPLATE_OWNER", Value: templateRepo.OwnerName, Transformers: transformers},
|
||||||
{Name: "REPO_LINK", Value: generateRepo.Link(), Transformers: nil},
|
{Name: "REPO_LINK", Value: generateRepo.Link(), Transformers: nil},
|
||||||
{Name: "TEMPLATE_LINK", Value: templateRepo.Link(), Transformers: nil},
|
{Name: "TEMPLATE_LINK", Value: templateRepo.Link(), Transformers: nil},
|
||||||
{Name: "REPO_HTTPS_URL", Value: generateRepo.CloneLinkGeneral(ctx).HTTPS, Transformers: nil},
|
{Name: "REPO_HTTPS_URL", Value: generateRepo.CloneLinkGeneral(ctx).HTTPS, Transformers: nil},
|
||||||
@@ -79,32 +92,23 @@ func generateExpansion(ctx context.Context, src string, templateRepo, generateRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
return os.Expand(src, func(key string) string {
|
return os.Expand(src, func(key string) string {
|
||||||
if expansion, ok := expansionMap[key]; ok {
|
if val, ok := expansionMap[key]; ok {
|
||||||
if sanitizeFileName {
|
return val
|
||||||
return fileNameSanitize(expansion)
|
|
||||||
}
|
|
||||||
return expansion
|
|
||||||
}
|
}
|
||||||
return key
|
return key
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GiteaTemplate holds information about a .gitea/template file
|
// giteaTemplateFileMatcher holds information about a .gitea/template file
|
||||||
type GiteaTemplate struct {
|
type giteaTemplateFileMatcher struct {
|
||||||
Path string
|
LocalFullPath string
|
||||||
Content []byte
|
globs []glob.Glob
|
||||||
|
|
||||||
globs []glob.Glob
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Globs parses the .gitea/template globs or returns them if they were already parsed
|
func newGiteaTemplateFileMatcher(fullPath string, content []byte) *giteaTemplateFileMatcher {
|
||||||
func (gt *GiteaTemplate) Globs() []glob.Glob {
|
gt := &giteaTemplateFileMatcher{LocalFullPath: fullPath}
|
||||||
if gt.globs != nil {
|
|
||||||
return gt.globs
|
|
||||||
}
|
|
||||||
|
|
||||||
gt.globs = make([]glob.Glob, 0)
|
gt.globs = make([]glob.Glob, 0)
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(gt.Content))
|
scanner := bufio.NewScanner(bytes.NewReader(content))
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := strings.TrimSpace(scanner.Text())
|
line := strings.TrimSpace(scanner.Text())
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
@@ -112,73 +116,91 @@ func (gt *GiteaTemplate) Globs() []glob.Glob {
|
|||||||
}
|
}
|
||||||
g, err := glob.Compile(line, '/')
|
g, err := glob.Compile(line, '/')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info("Invalid glob expression '%s' (skipped): %v", line, err)
|
log.Debug("Invalid glob expression '%s' (skipped): %v", line, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
gt.globs = append(gt.globs, g)
|
gt.globs = append(gt.globs, g)
|
||||||
}
|
}
|
||||||
return gt.globs
|
return gt
|
||||||
}
|
}
|
||||||
|
|
||||||
func readGiteaTemplateFile(tmpDir string) (*GiteaTemplate, error) {
|
func (gt *giteaTemplateFileMatcher) HasRules() bool {
|
||||||
gtPath := filepath.Join(tmpDir, ".gitea", "template")
|
return len(gt.globs) != 0
|
||||||
if _, err := os.Stat(gtPath); os.IsNotExist(err) {
|
}
|
||||||
|
|
||||||
|
func (gt *giteaTemplateFileMatcher) Match(s string) bool {
|
||||||
|
for _, g := range gt.globs {
|
||||||
|
if g.Match(s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func readGiteaTemplateFile(tmpDir string) (*giteaTemplateFileMatcher, error) {
|
||||||
|
localPath := filepath.Join(tmpDir, ".gitea", "template")
|
||||||
|
if _, err := os.Stat(localPath); os.IsNotExist(err) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := os.ReadFile(gtPath)
|
content, err := os.ReadFile(localPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &GiteaTemplate{Path: gtPath, Content: content}, nil
|
return newGiteaTemplateFileMatcher(localPath, content), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, generateRepo *repo_model.Repository, giteaTemplateFile *GiteaTemplate) error {
|
func substGiteaTemplateFile(ctx context.Context, tmpDir, tmpDirSubPath string, templateRepo, generateRepo *repo_model.Repository) error {
|
||||||
if err := util.Remove(giteaTemplateFile.Path); err != nil {
|
tmpFullPath := filepath.Join(tmpDir, tmpDirSubPath)
|
||||||
return fmt.Errorf("remove .giteatemplate: %w", err)
|
if ok, err := util.IsRegularFile(tmpFullPath); !ok {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if len(giteaTemplateFile.Globs()) == 0 {
|
|
||||||
|
content, err := os.ReadFile(tmpFullPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := util.Remove(tmpFullPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
generatedContent := generateExpansion(ctx, string(content), templateRepo, generateRepo)
|
||||||
|
substSubPath := filepath.Clean(filePathSanitize(generateExpansion(ctx, tmpDirSubPath, templateRepo, generateRepo)))
|
||||||
|
newLocalPath := filepath.Join(tmpDir, substSubPath)
|
||||||
|
regular, err := util.IsRegularFile(newLocalPath)
|
||||||
|
if canWrite := regular || os.IsNotExist(err); !canWrite {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(newLocalPath), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(newLocalPath, []byte(generatedContent), 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, generateRepo *repo_model.Repository, fileMatcher *giteaTemplateFileMatcher) error {
|
||||||
|
if err := util.Remove(fileMatcher.LocalFullPath); err != nil {
|
||||||
|
return fmt.Errorf("unable to remove .gitea/template: %w", err)
|
||||||
|
}
|
||||||
|
if !fileMatcher.HasRules() {
|
||||||
return nil // Avoid walking tree if there are no globs
|
return nil // Avoid walking tree if there are no globs
|
||||||
}
|
}
|
||||||
tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
|
|
||||||
return filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
|
return filepath.WalkDir(tmpDir, func(fullPath string, d os.DirEntry, walkErr error) error {
|
||||||
if walkErr != nil {
|
if walkErr != nil {
|
||||||
return walkErr
|
return walkErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.IsDir() {
|
if d.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
tmpDirSubPath, err := filepath.Rel(tmpDir, fullPath)
|
||||||
base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
|
if err != nil {
|
||||||
for _, g := range giteaTemplateFile.Globs() {
|
return err
|
||||||
if g.Match(base) {
|
}
|
||||||
content, err := os.ReadFile(path)
|
if fileMatcher.Match(filepath.ToSlash(tmpDirSubPath)) {
|
||||||
if err != nil {
|
return substGiteaTemplateFile(ctx, tmpDir, tmpDirSubPath, templateRepo, generateRepo)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
generatedContent := []byte(generateExpansion(ctx, string(content), templateRepo, generateRepo, false))
|
|
||||||
if err := os.WriteFile(path, generatedContent, 0o644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, generateExpansion(ctx, base, templateRepo, generateRepo, true)))
|
|
||||||
|
|
||||||
// Create parent subdirectories if needed or continue silently if it exists
|
|
||||||
if err = os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Substitute filename variables
|
|
||||||
if err = os.Rename(path, substPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}) // end: WalkDir
|
}) // end: WalkDir
|
||||||
@@ -218,13 +240,13 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Variable expansion
|
// Variable expansion
|
||||||
giteaTemplateFile, err := readGiteaTemplateFile(tmpDir)
|
fileMatcher, err := readGiteaTemplateFile(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("readGiteaTemplateFile: %w", err)
|
return fmt.Errorf("readGiteaTemplateFile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if giteaTemplateFile != nil {
|
if fileMatcher != nil {
|
||||||
err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, giteaTemplateFile)
|
err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, fileMatcher)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -321,12 +343,17 @@ func (gro GenerateRepoOptions) IsValid() bool {
|
|||||||
gro.IssueLabels || gro.ProtectedBranch // or other items as they are added
|
gro.IssueLabels || gro.ProtectedBranch // or other items as they are added
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileNameSanitizeRegexp = regexp.MustCompile(`(?i)\.\.|[<>:\"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`)
|
func filePathSanitize(s string) string {
|
||||||
|
fields := strings.Split(filepath.ToSlash(s), "/")
|
||||||
// Sanitize user input to valid OS filenames
|
for i, field := range fields {
|
||||||
//
|
field = strings.TrimSpace(strings.TrimSpace(globalVars().fileNameSanitizeRegexp.ReplaceAllString(field, "_")))
|
||||||
// Based on https://github.com/sindresorhus/filename-reserved-regex
|
if strings.HasPrefix(field, "..") {
|
||||||
// Adds ".." to prevent directory traversal
|
field = "__" + field[2:]
|
||||||
func fileNameSanitize(s string) string {
|
}
|
||||||
return strings.TrimSpace(fileNameSanitizeRegexp.ReplaceAllString(s, "_"))
|
if strings.EqualFold(field, ".git") {
|
||||||
|
field = "_" + field[1:]
|
||||||
|
}
|
||||||
|
fields[i] = field
|
||||||
|
}
|
||||||
|
return filepath.FromSlash(strings.Join(fields, "/"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,18 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
var giteaTemplate = []byte(`
|
func TestGiteaTemplate(t *testing.T) {
|
||||||
|
giteaTemplate := []byte(`
|
||||||
# Header
|
# Header
|
||||||
|
|
||||||
# All .go files
|
# All .go files
|
||||||
@@ -23,48 +28,153 @@ text/*.txt
|
|||||||
**/modules/*
|
**/modules/*
|
||||||
`)
|
`)
|
||||||
|
|
||||||
func TestGiteaTemplate(t *testing.T) {
|
gt := newGiteaTemplateFileMatcher("", giteaTemplate)
|
||||||
gt := GiteaTemplate{Content: giteaTemplate}
|
assert.Len(t, gt.globs, 3)
|
||||||
assert.Len(t, gt.Globs(), 3)
|
|
||||||
|
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
Path string
|
Path string
|
||||||
Match bool
|
Match bool
|
||||||
}{
|
}{
|
||||||
{Path: "main.go", Match: true},
|
{Path: "main.go", Match: true},
|
||||||
{Path: "a/b/c/d/e.go", Match: true},
|
{Path: "sub/sub/foo.go", Match: true},
|
||||||
{Path: "main.txt", Match: false},
|
|
||||||
{Path: "a/b.txt", Match: false},
|
{Path: "a.txt", Match: false},
|
||||||
{Path: "text/a.txt", Match: true},
|
{Path: "text/a.txt", Match: true},
|
||||||
{Path: "text/b.txt", Match: true},
|
{Path: "sub/text/a.txt", Match: false},
|
||||||
{Path: "text/c.json", Match: false},
|
{Path: "text/a.json", Match: false},
|
||||||
|
|
||||||
{Path: "a/b/c/modules/README.md", Match: true},
|
{Path: "a/b/c/modules/README.md", Match: true},
|
||||||
{Path: "a/b/c/modules/d/README.md", Match: false},
|
{Path: "a/b/c/modules/d/README.md", Match: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.Path, func(t *testing.T) {
|
assert.Equal(t, tc.Match, gt.Match(tc.Path), "path: %s", tc.Path)
|
||||||
match := false
|
|
||||||
for _, g := range gt.Globs() {
|
|
||||||
if g.Match(tc.Path) {
|
|
||||||
match = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.Equal(t, tc.Match, match)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFileNameSanitize(t *testing.T) {
|
func TestFilePathSanitize(t *testing.T) {
|
||||||
assert.Equal(t, "test_CON", fileNameSanitize("test_CON"))
|
assert.Equal(t, "test_CON", filePathSanitize("test_CON"))
|
||||||
assert.Equal(t, "test CON", fileNameSanitize("test CON "))
|
assert.Equal(t, "test CON", filePathSanitize("test CON "))
|
||||||
assert.Equal(t, "__traverse__", fileNameSanitize("../traverse/.."))
|
assert.Equal(t, "__/traverse/__", filePathSanitize(".. /traverse/ .."))
|
||||||
assert.Equal(t, "http___localhost_3003_user_test.git", fileNameSanitize("http://localhost:3003/user/test.git"))
|
assert.Equal(t, "./__/a/_git/b_", filePathSanitize("./../a/.git/ b: "))
|
||||||
assert.Equal(t, "_", fileNameSanitize("CON"))
|
assert.Equal(t, "_", filePathSanitize("CoN"))
|
||||||
assert.Equal(t, "_", fileNameSanitize("con"))
|
assert.Equal(t, "_", filePathSanitize("LpT1"))
|
||||||
assert.Equal(t, "_", fileNameSanitize("\u0000"))
|
assert.Equal(t, "_", filePathSanitize("CoM1"))
|
||||||
assert.Equal(t, "目标", fileNameSanitize("目标"))
|
assert.Equal(t, "_", filePathSanitize("\u0000"))
|
||||||
|
assert.Equal(t, "目标", filePathSanitize("目标"))
|
||||||
|
// unlike filepath.Clean, it only sanitizes, doesn't change the separator layout
|
||||||
|
assert.Equal(t, "", filePathSanitize("")) //nolint:testifylint // for easy reading
|
||||||
|
assert.Equal(t, ".", filePathSanitize("."))
|
||||||
|
assert.Equal(t, "/", filePathSanitize("/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessGiteaTemplateFile(t *testing.T) {
|
||||||
|
tmpDir := filepath.Join(t.TempDir(), "gitea-template-test")
|
||||||
|
|
||||||
|
assertFileContent := func(path, expected string) {
|
||||||
|
data, err := os.ReadFile(filepath.Join(tmpDir, path))
|
||||||
|
if expected == "" {
|
||||||
|
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, string(data), "file content mismatch for %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSymLink := func(path, expected string) {
|
||||||
|
link, err := os.Readlink(filepath.Join(tmpDir, path))
|
||||||
|
if expected == "" {
|
||||||
|
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, link, "symlink target mismatch for %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, os.MkdirAll(tmpDir+"/.gitea", 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/.gitea/template", []byte("*\ninclude/**"), 0o644))
|
||||||
|
require.NoError(t, os.MkdirAll(tmpDir+"/sub", 0o755))
|
||||||
|
require.NoError(t, os.MkdirAll(tmpDir+"/include/foo/bar", 0o755))
|
||||||
|
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/sub/link-target", []byte("link target content from ${TEMPLATE_NAME}"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/include/foo/bar/test.txt", []byte("include subdir ${TEMPLATE_NAME}"), 0o644))
|
||||||
|
|
||||||
|
// case-1
|
||||||
|
{
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/normal", []byte("normal content"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/template", []byte("template from ${TEMPLATE_NAME}"), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-2
|
||||||
|
{
|
||||||
|
require.NoError(t, os.Symlink(tmpDir+"/sub/link-target", tmpDir+"/link"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-3
|
||||||
|
{
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/subst-${REPO_NAME}", []byte("dummy subst repo name"), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-4
|
||||||
|
assertSubstTemplateName := func(normalContent, toLinkContent, fromLinkContent string) {
|
||||||
|
assertFileContent("subst-${TEMPLATE_NAME}-normal", normalContent)
|
||||||
|
assertFileContent("subst-${TEMPLATE_NAME}-to-link", toLinkContent)
|
||||||
|
assertFileContent("subst-${TEMPLATE_NAME}-from-link", fromLinkContent)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// will succeed
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/subst-${TEMPLATE_NAME}-normal", []byte("dummy subst template name normal"), 0o644))
|
||||||
|
// will skil if the path subst result is a link
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/subst-${TEMPLATE_NAME}-to-link", []byte("dummy subst template name to link"), 0o644))
|
||||||
|
require.NoError(t, os.Symlink(tmpDir+"/sub/link-target", tmpDir+"/subst-TemplateRepoName-to-link"))
|
||||||
|
// will be skipped since the source is a symlink
|
||||||
|
require.NoError(t, os.Symlink(tmpDir+"/sub/link-target", tmpDir+"/subst-${TEMPLATE_NAME}-from-link"))
|
||||||
|
// pre-check
|
||||||
|
assertSubstTemplateName("dummy subst template name normal", "dummy subst template name to link", "link target content from ${TEMPLATE_NAME}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// process the template files
|
||||||
|
{
|
||||||
|
templateRepo := &repo_model.Repository{Name: "TemplateRepoName"}
|
||||||
|
generatedRepo := &repo_model.Repository{Name: "/../.gIt/name"}
|
||||||
|
fileMatcher, _ := readGiteaTemplateFile(tmpDir)
|
||||||
|
err := processGiteaTemplateFile(t.Context(), tmpDir, templateRepo, generatedRepo, fileMatcher)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assertFileContent("include/foo/bar/test.txt", "include subdir TemplateRepoName")
|
||||||
|
}
|
||||||
|
|
||||||
|
// the lin target should never be modified, and since it is in a subdirectory, it is not affected by the template either
|
||||||
|
assertFileContent("sub/link-target", "link target content from ${TEMPLATE_NAME}")
|
||||||
|
|
||||||
|
// case-1
|
||||||
|
{
|
||||||
|
assertFileContent("no-such", "")
|
||||||
|
assertFileContent("normal", "normal content")
|
||||||
|
assertFileContent("template", "template from TemplateRepoName")
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-2
|
||||||
|
{
|
||||||
|
// symlink with templates should be preserved (not read or write)
|
||||||
|
assertSymLink("link", tmpDir+"/sub/link-target")
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-3
|
||||||
|
{
|
||||||
|
assertFileContent("subst-${REPO_NAME}", "")
|
||||||
|
assertFileContent("subst-/__/_gIt/name", "dummy subst repo name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-4
|
||||||
|
{
|
||||||
|
// the paths with templates should have been removed, subst to a regular file, succeed, the link is preserved
|
||||||
|
assertSubstTemplateName("", "", "link target content from ${TEMPLATE_NAME}")
|
||||||
|
assertFileContent("subst-TemplateRepoName-normal", "dummy subst template name normal")
|
||||||
|
// subst to a link, skip, and the target is unchanged
|
||||||
|
assertSymLink("subst-TemplateRepoName-to-link", tmpDir+"/sub/link-target")
|
||||||
|
// subst from a link, skip, and the target is unchanged
|
||||||
|
assertSymLink("subst-${TEMPLATE_NAME}-from-link", tmpDir+"/sub/link-target")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransformers(t *testing.T) {
|
func TestTransformers(t *testing.T) {
|
||||||
@@ -82,9 +192,9 @@ func TestTransformers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input := "Abc_Def-XYZ"
|
input := "Abc_Def-XYZ"
|
||||||
assert.Len(t, defaultTransformers, len(cases))
|
assert.Len(t, globalVars().defaultTransformers, len(cases))
|
||||||
for i, c := range cases {
|
for i, c := range cases {
|
||||||
tf := defaultTransformers[i]
|
tf := globalVars().defaultTransformers[i]
|
||||||
require.Equal(t, c.name, tf.Name)
|
require.Equal(t, c.name, tf.Name)
|
||||||
assert.Equal(t, c.expected, tf.Transform(input), "case %s", c.name)
|
assert.Equal(t, c.expected, tf.Transform(input), "case %s", c.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func Test_CmdKeys(t *testing.T) {
|
|||||||
"with_key",
|
"with_key",
|
||||||
[]string{"keys", "-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="},
|
[]string{"keys", "-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="},
|
||||||
false,
|
false,
|
||||||
"# gitea public key\ncommand=\"" + setting.AppPath + " --config=" + util.ShellEscape(setting.CustomConf) + " serv key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM= user2@localhost\n",
|
"# gitea public key\ncommand=\"" + setting.AppPath + " --config=" + util.ShellEscape(setting.CustomConf) + " serv key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM= user-2\n",
|
||||||
},
|
},
|
||||||
{"invalid", []string{"keys", "--not-a-flag=git"}, true, "Incorrect Usage: flag provided but not defined: -not-a-flag\n\n"},
|
{"invalid", []string{"keys", "--not-a-flag=git"}, true, "Incorrect Usage: flag provided but not defined: -not-a-flag\n\n"},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user