0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-01-15 21:47:10 +01:00

Merge branch 'main' into webhook-bark

This commit is contained in:
ZeroDeng 2025-10-23 13:12:38 +08:00 committed by GitHub
commit d1bc442ebc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 339 additions and 223 deletions

View File

@ -2541,6 +2541,12 @@ LEVEL = Info
;; * no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code.
;; * iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page.
;RENDER_CONTENT_MODE=sanitized
;;
;; Whether post-process the rendered HTML content, including:
;; resolve relative links and image sources, recognizing issue/commit references, escaping invisible characters,
;; mentioning users, rendering permlink code blocks, replacing emoji shorthands, etc.
;; By default, this is true when RENDER_CONTENT_MODE is `sanitized`, otherwise false.
;NEED_POST_PROCESS=false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -8,7 +8,6 @@ import (
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@ -42,30 +41,6 @@ func (err ErrLFSLockNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrLFSUnauthorizedAction represents a "LFSUnauthorizedAction" kind of error.
type ErrLFSUnauthorizedAction struct {
RepoID int64
UserName string
Mode perm.AccessMode
}
// IsErrLFSUnauthorizedAction checks if an error is a ErrLFSUnauthorizedAction.
func IsErrLFSUnauthorizedAction(err error) bool {
_, ok := err.(ErrLFSUnauthorizedAction)
return ok
}
func (err ErrLFSUnauthorizedAction) Error() string {
if err.Mode == perm.AccessModeWrite {
return fmt.Sprintf("User %s doesn't have write access for lfs lock [rid: %d]", err.UserName, err.RepoID)
}
return fmt.Sprintf("User %s doesn't have read access for lfs lock [rid: %d]", err.UserName, err.RepoID)
}
func (err ErrLFSUnauthorizedAction) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error.
type ErrLFSLockAlreadyExist struct {
RepoID int64
@ -93,12 +68,6 @@ type ErrLFSFileLocked struct {
UserName string
}
// IsErrLFSFileLocked checks if an error is a ErrLFSFileLocked.
func IsErrLFSFileLocked(err error) bool {
_, ok := err.(ErrLFSFileLocked)
return ok
}
func (err ErrLFSFileLocked) Error() string {
return fmt.Sprintf("File is lfs locked [repo: %d, locked by: %s, path: %s]", err.RepoID, err.UserName, err.Path)
}

View File

@ -11,10 +11,7 @@ import (
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@ -71,10 +68,6 @@ func (l *LFSLock) LoadOwner(ctx context.Context) error {
// CreateLFSLock creates a new lock.
func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) {
return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) {
if err := CheckLFSAccessForRepo(ctx, lock.OwnerID, repo, perm.AccessModeWrite); err != nil {
return nil, err
}
lock.Path = util.PathJoinRel(lock.Path)
lock.RepoID = repo.ID
@ -165,10 +158,6 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor
return nil, err
}
if err := CheckLFSAccessForRepo(ctx, u.ID, repo, perm.AccessModeWrite); err != nil {
return nil, err
}
if !force && u.ID != lock.OwnerID {
return nil, errors.New("user doesn't own lock and force flag is not set")
}
@ -180,22 +169,3 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor
return lock, nil
})
}
// CheckLFSAccessForRepo check needed access mode base on action
func CheckLFSAccessForRepo(ctx context.Context, ownerID int64, repo *repo_model.Repository, mode perm.AccessMode) error {
if ownerID == 0 {
return ErrLFSUnauthorizedAction{repo.ID, "undefined", mode}
}
u, err := user_model.GetUserByID(ctx, ownerID)
if err != nil {
return err
}
perm, err := access_model.GetUserRepoPermission(ctx, repo, u)
if err != nil {
return err
}
if !perm.CanAccess(mode, unit.TypeCode) {
return ErrLFSUnauthorizedAction{repo.ID, u.DisplayName(), mode}
}
return nil
}

View File

@ -5,9 +5,11 @@ package access
import (
"context"
"errors"
"fmt"
"slices"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
perm_model "code.gitea.io/gitea/models/perm"
@ -253,6 +255,34 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
}
}
// GetActionsUserRepoPermission returns the actions user permissions to the repository
func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Repository, actionsUser *user_model.User, taskID int64) (perm Permission, err error) {
if actionsUser.ID != user_model.ActionsUserID {
return perm, errors.New("api GetActionsUserRepoPermission can only be called by the actions user")
}
task, err := actions_model.GetTaskByID(ctx, taskID)
if err != nil {
return perm, err
}
if task.RepoID != repo.ID {
// FIXME allow public repo read access if tokenless pull is enabled
return perm, nil
}
var accessMode perm_model.AccessMode
if task.IsForkPullRequest {
accessMode = perm_model.AccessModeRead
} else {
accessMode = perm_model.AccessModeWrite
}
if err := repo.LoadUnits(ctx); err != nil {
return perm, err
}
perm.SetUnitsWithDefaultAccessMode(repo.Units, accessMode)
return perm, nil
}
// GetUserRepoPermission returns the user permissions to the repository
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
defer func() {

View File

@ -249,8 +249,13 @@ func (u *User) MaxCreationLimit() int {
}
// CanCreateRepoIn checks whether the doer(u) can create a repository in the owner
// NOTE: functions calling this assume a failure due to repository count limit; it ONLY checks the repo number LIMIT, if new checks are added, those functions should be revised
// NOTE: functions calling this assume a failure due to repository count limit, or the owner is not a real user.
// It ONLY checks the repo number LIMIT or whether owner user is real. If new checks are added, those functions should be revised.
// TODO: the callers can only return ErrReachLimitOfRepo, need to fine tune to support other error types in the future.
func (u *User) CanCreateRepoIn(owner *User) bool {
if u.ID <= 0 || owner.ID <= 0 {
return false // fake user like Ghost or Actions user
}
if u.IsAdmin {
return true
}

View File

@ -48,17 +48,16 @@ func IsGiteaActionsUserName(name string) bool {
// NewActionsUser creates and returns a fake user for running the actions.
func NewActionsUser() *User {
return &User{
ID: ActionsUserID,
Name: ActionsUserName,
LowerName: ActionsUserName,
IsActive: true,
FullName: "Gitea Actions",
Email: ActionsUserEmail,
KeepEmailPrivate: true,
LoginName: ActionsUserName,
Type: UserTypeBot,
AllowCreateOrganization: true,
Visibility: structs.VisibleTypePublic,
ID: ActionsUserID,
Name: ActionsUserName,
LowerName: ActionsUserName,
IsActive: true,
FullName: "Gitea Actions",
Email: ActionsUserEmail,
KeepEmailPrivate: true,
LoginName: ActionsUserName,
Type: UserTypeBot,
Visibility: structs.VisibleTypePublic,
}
}

View File

@ -648,33 +648,36 @@ func TestGetInactiveUsers(t *testing.T) {
func TestCanCreateRepo(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)()
const noLimit = -1
doerNormal := &user_model.User{}
doerAdmin := &user_model.User{IsAdmin: true}
doerActions := user_model.NewActionsUser()
doerNormal := &user_model.User{ID: 2}
doerAdmin := &user_model.User{ID: 1, IsAdmin: true}
t.Run("NoGlobalLimit", func(t *testing.T) {
setting.Repository.MaxCreationLimit = noLimit
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerActions.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerAdmin.CanCreateRepoIn(doerActions))
})
t.Run("GlobalLimit50", func(t *testing.T) {
setting.Repository.MaxCreationLimit = 50
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100}))
})
}

View File

@ -15,6 +15,8 @@ import (
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"github.com/kballard/go-shellquote"
)
// RegisterRenderers registers all supported third part renderers according settings
@ -81,7 +83,10 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
envMark("GITEA_PREFIX_SRC"), baseLinkSrc,
envMark("GITEA_PREFIX_RAW"), baseLinkRaw,
).Replace(p.Command)
commands := strings.Fields(command)
commands, err := shellquote.Split(command)
if err != nil || len(commands) == 0 {
return fmt.Errorf("%s invalid command %q: %w", p.Name(), p.Command, err)
}
args := commands[1:]
if p.IsInputFile {

View File

@ -120,31 +120,38 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext {
return ctx
}
// Render renders markup file to HTML with all specific handling stuff.
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
// FindRendererByContext finds renderer by RenderContext
// TODO: it should be merged with other similar functions like GetRendererByFileName, DetectMarkupTypeByFileName, etc
func FindRendererByContext(ctx *RenderContext) (Renderer, error) {
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
if ctx.RenderOptions.MarkupType == "" {
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
return nil, util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
}
}
renderer := renderers[ctx.RenderOptions.MarkupType]
if renderer == nil {
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
return nil, util.NewNotExistErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
}
if ctx.RenderOptions.RelativePath != "" {
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
if !ctx.RenderOptions.InStandalonePage {
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
// otherwise, a <iframe> should be outputted to embed the external rendered page
return renderIFrame(ctx, output)
}
}
}
return renderer, nil
}
return render(ctx, renderer, input, output)
func RendererNeedPostProcess(renderer Renderer) bool {
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
return true
}
return false
}
// Render renders markup file to HTML with all specific handling stuff.
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
renderer, err := FindRendererByContext(ctx)
if err != nil {
return err
}
return RenderWithRenderer(ctx, renderer, input, output)
}
// RenderString renders Markup string to HTML with all specific handling stuff and return string
@ -185,7 +192,16 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) {
}
}
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
if !ctx.RenderOptions.InStandalonePage {
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
// otherwise, a <iframe> should be outputted to embed the external rendered page
return renderIFrame(ctx, output)
}
// else: this is a standalone page, fallthrough to the real rendering
}
ctx.usedByRender = true
if ctx.RenderHelper != nil {
defer ctx.RenderHelper.CleanUp()
@ -214,7 +230,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
}
eg.Go(func() (err error) {
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
if RendererNeedPostProcess(renderer) {
err = PostProcessDefault(ctx, pr1, pw2)
} else {
_, err = io.Copy(pw2, pr1)

View File

@ -259,7 +259,9 @@ func newMarkupRenderer(name string, sec ConfigSection) {
FileExtensions: exts,
Command: command,
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
RenderContentMode: renderContentMode,
// if no sanitizer is needed, no post process is needed
NeedPostProcess: sec.Key("NEED_POST_PROCESS").MustBool(renderContentMode == RenderContentModeSanitized),
})
}

View File

@ -70,7 +70,6 @@ import (
"net/http"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
@ -190,27 +189,11 @@ func repoAssignment() func(ctx *context.APIContext) {
if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID {
taskID := ctx.Data["ActionsTaskID"].(int64)
task, err := actions_model.GetTaskByID(ctx, taskID)
ctx.Repo.Permission, err = access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if task.RepoID != repo.ID {
ctx.APIErrorNotFound()
return
}
if task.IsForkPullRequest {
ctx.Repo.Permission.AccessMode = perm.AccessModeRead
} else {
ctx.Repo.Permission.AccessMode = perm.AccessModeWrite
}
if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode)
} else {
needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer)
if err != nil {

View File

@ -28,6 +28,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
@ -270,6 +271,8 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
db.IsErrNamePatternNotAllowed(err) ||
label.IsErrTemplateLoad(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.APIError(http.StatusForbidden, err)
} else {
ctx.APIErrorInternal(err)
}

View File

@ -18,7 +18,6 @@ import (
"sync"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
@ -190,29 +189,17 @@ func httpBase(ctx *context.Context) *serviceHandler {
if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64)
task, err := actions_model.GetTaskByID(ctx, taskID)
p, err := access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil {
ctx.ServerError("GetTaskByID", err)
return nil
}
if task.RepoID != repo.ID {
ctx.PlainText(http.StatusForbidden, "User permission denied")
ctx.ServerError("GetUserRepoPermission", err)
return nil
}
if task.IsForkPullRequest {
if accessMode > perm.AccessModeRead {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil
}
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeRead))
} else {
if accessMode > perm.AccessModeWrite {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil
}
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite))
if !p.CanAccess(accessMode, unitType) {
ctx.PlainText(http.StatusNotFound, "Repository not found")
return nil
}
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, p.UnitAccessMode(unitType)))
} else {
p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {

View File

@ -151,17 +151,28 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
}
func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
renderer, err := markup.FindRendererByContext(renderCtx)
if err != nil {
return nil, "", err
}
markupRd, markupWr := io.Pipe()
defer markupWr.Close()
done := make(chan struct{})
go func() {
sb := &strings.Builder{}
// We allow NBSP here this is rendered
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP)
if markup.RendererNeedPostProcess(renderer) {
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) // We allow NBSP here this is rendered
} else {
escaped = &charset.EscapeStatus{}
_, _ = io.Copy(sb, markupRd)
}
output = template.HTML(sb.String())
close(done)
}()
err = markup.Render(renderCtx, input, markupWr)
err = markup.RenderWithRenderer(renderCtx, renderer, input, markupWr)
_ = markupWr.CloseWithError(err)
<-done
return escaped, output, err

View File

@ -771,7 +771,7 @@ func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application {
// ToLFSLock convert a LFSLock to api.LFSLock
func ToLFSLock(ctx context.Context, l *git_model.LFSLock) *api.LFSLock {
u, err := user_model.GetUserByID(ctx, l.OwnerID)
u, err := user_model.GetPossibleUserByID(ctx, l.OwnerID)
if err != nil {
return nil
}

View File

@ -187,13 +187,6 @@ func PostLockHandler(ctx *context.Context) {
})
return
}
if git_model.IsErrLFSUnauthorizedAction(err) {
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
Message: "You must have push access to create locks : " + err.Error(),
})
return
}
log.Error("Unable to CreateLFSLock in repository %-v at %s for user %-v: Error: %v", repository, req.Path, ctx.Doer, err)
ctx.JSON(http.StatusInternalServerError, api.LFSLockError{
Message: "internal server error : Internal Server Error",
@ -317,13 +310,6 @@ func UnLockHandler(ctx *context.Context) {
lock, err := git_model.DeleteLFSLockByID(ctx, ctx.PathParamInt64("lid"), repository, ctx.Doer, req.Force)
if err != nil {
if git_model.IsErrLFSUnauthorizedAction(err) {
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
Message: "You must have push access to delete locks : " + err.Error(),
})
return
}
log.Error("Unable to DeleteLFSLockByID[%d] by user %-v with force %t: Error: %v", ctx.PathParamInt64("lid"), ctx.Doer, req.Force, err)
ctx.JSON(http.StatusInternalServerError, api.LFSLockError{
Message: "unable to delete lock : Internal Server Error",

View File

@ -20,7 +20,6 @@ import (
"strings"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
perm_model "code.gitea.io/gitea/models/perm"
@ -549,33 +548,31 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64)
task, err := actions_model.GetTaskByID(ctx, taskID)
perm, err := access_model.GetActionsUserRepoPermission(ctx, repository, ctx.Doer, taskID)
if err != nil {
log.Error("Unable to GetTaskByID for task[%d] Error: %v", taskID, err)
log.Error("Unable to GetActionsUserRepoPermission for task[%d] Error: %v", taskID, err)
return false
}
if task.RepoID != repository.ID {
return false
}
if task.IsForkPullRequest {
return accessMode <= perm_model.AccessModeRead
}
return accessMode <= perm_model.AccessModeWrite
return perm.CanAccess(accessMode, unit.TypeCode)
}
// ctx.IsSigned is unnecessary here, this will be checked in perm.CanAccess
// it works for both anonymous request and signed-in user, then perm.CanAccess will do the permission check
perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
if err != nil {
log.Error("Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v", ctx.Doer, repository, err)
return false
}
canRead := perm.CanAccess(accessMode, unit.TypeCode)
if canRead && (!requireSigned || ctx.IsSigned) {
canAccess := perm.CanAccess(accessMode, unit.TypeCode)
// if it doesn't require sign-in and anonymous user has access, return true
// if the user is already signed in (for example: by session auth method), and the doer can access, return true
if canAccess && (!requireSigned || ctx.IsSigned) {
return true
}
// now, either sign-in is required or the ctx.Doer cannot access, check the LFS token
// however, "ctx.Doer exists but cannot access then check LFS token" should not really happen:
// * why a request can be sent with both valid user session and valid LFS token then use LFS token to access?
user, err := parseToken(ctx, authorization, repository, accessMode)
if err != nil {
// Most of these are Warn level - the true internal server errors are logged in parseToken already

View File

@ -0,0 +1,117 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"net/http"
"net/url"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestActionsJobTokenAccess(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
t.Run("Write Access", testActionsJobTokenAccess(u, false))
t.Run("Read Access", testActionsJobTokenAccess(u, true))
})
}
func testActionsJobTokenAccess(u *url.URL, isFork bool) func(t *testing.T) {
return func(t *testing.T) {
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47})
require.NoError(t, task.GenerateToken())
task.Status = actions_model.StatusRunning
task.IsForkPullRequest = isFork
err := actions_model.UpdateTask(t.Context(), task, "token_hash", "token_salt", "token_last_eight", "status", "is_fork_pull_request")
require.NoError(t, err)
session := emptyTestSession(t)
context := APITestContext{
Session: session,
Token: task.Token,
Username: "user5",
Reponame: "repo4",
}
dstPath := t.TempDir()
u.Path = context.GitPath()
u.User = url.UserPassword("gitea-actions", task.Token)
t.Run("Git Clone", doGitClone(dstPath, u))
t.Run("API Get Repository", doAPIGetRepository(context, func(t *testing.T, r structs.Repository) {
require.Equal(t, "repo4", r.Name)
require.Equal(t, "user5", r.Owner.UserName)
}))
context.ExpectedCode = util.Iif(isFork, http.StatusForbidden, http.StatusCreated)
t.Run("API Create File", doAPICreateFile(context, "test.txt", &structs.CreateFileOptions{
FileOptions: structs.FileOptions{
NewBranchName: "new-branch",
Message: "Create File",
},
ContentBase64: base64.StdEncoding.EncodeToString([]byte(`This is a test file created using job token.`)),
}))
context.ExpectedCode = http.StatusForbidden
t.Run("Fail to Create Repository", doAPICreateRepository(context, true))
context.ExpectedCode = http.StatusForbidden
t.Run("Fail to Delete Repository", doAPIDeleteRepository(context))
t.Run("Fail to Create Organization", doAPICreateOrganization(context, &structs.CreateOrgOption{
UserName: "actions",
FullName: "Gitea Actions",
}))
}
}
func TestActionsJobTokenAccessLFS(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
t.Run("Create Repository", doAPICreateRepository(httpContext, false, func(t *testing.T, repository structs.Repository) {
task := &actions_model.ActionTask{}
require.NoError(t, task.GenerateToken())
task.Status = actions_model.StatusRunning
task.IsForkPullRequest = false
task.RepoID = repository.ID
err := db.Insert(t.Context(), task)
require.NoError(t, err)
session := emptyTestSession(t)
httpContext := APITestContext{
Session: session,
Token: task.Token,
Username: "user2",
Reponame: "repo-lfs-test",
}
u.Path = httpContext.GitPath()
dstPath := t.TempDir()
u.Path = httpContext.GitPath()
u.User = url.UserPassword("gitea-actions", task.Token)
t.Run("Clone", doGitClone(dstPath, u))
dstPath2 := t.TempDir()
t.Run("Partial Clone", doPartialGitClone(dstPath2, u))
lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0]
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo-lfs-test/media/"+lfs).AddTokenAuth(task.Token)
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
assert.Equal(t, testFileSizeSmall, respLFS.Length)
}))
})
}

View File

@ -9,7 +9,6 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@ -25,8 +24,9 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
// Test with LFS
onGiteaRun(t, func(t *testing.T, u *url.URL) {
createLFSTestRepository(t, "repo-lfs-test")
httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteRepository)
doAPICreateRepository(httpContext, false, func(t *testing.T, repository api.Repository) {
t.Run("repo-lfs-test", func(t *testing.T) {
u.Path = httpContext.GitPath()
dstPath := t.TempDir()
@ -41,7 +41,7 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0]
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs)
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo-lfs-test/media/"+lfs).AddTokenAuth(httpContext.Token)
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
assert.Equal(t, testFileSizeSmall, respLFS.Length)
})

View File

@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPILFSNotStarted(t *testing.T) {
@ -59,12 +60,12 @@ func TestAPILFSMediaType(t *testing.T) {
MakeRequest(t, req, http.StatusUnsupportedMediaType)
}
func createLFSTestRepository(t *testing.T, name string) *repo_model.Repository {
ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
func createLFSTestRepository(t *testing.T, repoName string) *repo_model.Repository {
ctx := NewAPITestContext(t, "user2", repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
t.Run("CreateRepo", doAPICreateRepository(ctx, false))
repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", "lfs-"+name+"-repo")
assert.NoError(t, err)
repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", repoName)
require.NoError(t, err)
return repo
}
@ -74,7 +75,7 @@ func TestAPILFSBatch(t *testing.T) {
setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "batch")
repo := createLFSTestRepository(t, "lfs-batch-repo")
content := []byte("dummy1")
oid := storeObjectInRepo(t, repo.ID, &content)
@ -253,7 +254,7 @@ func TestAPILFSBatch(t *testing.T) {
assert.NoError(t, err)
assert.True(t, exist)
repo2 := createLFSTestRepository(t, "batch2")
repo2 := createLFSTestRepository(t, "lfs-batch2-repo")
content := []byte("dummy0")
storeObjectInRepo(t, repo2.ID, &content)
@ -329,7 +330,7 @@ func TestAPILFSUpload(t *testing.T) {
setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "upload")
repo := createLFSTestRepository(t, "lfs-upload-repo")
content := []byte("dummy3")
oid := storeObjectInRepo(t, repo.ID, &content)
@ -433,7 +434,7 @@ func TestAPILFSVerify(t *testing.T) {
setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "verify")
repo := createLFSTestRepository(t, "lfs-verify-repo")
content := []byte("dummy3")
oid := storeObjectInRepo(t, repo.ID, &content)

View File

@ -4,18 +4,23 @@
package integration
import (
"bytes"
"io"
"net/http"
"net/url"
"strings"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/external"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExternalMarkupRenderer(t *testing.T) {
@ -25,36 +30,52 @@ func TestExternalMarkupRenderer(t *testing.T) {
return
}
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
t.Run("RenderNoSanitizer", func(t *testing.T) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
_, err := createFile(user2, repo1, "file.no-sanitizer", "master", `any content`)
require.NoError(t, err)
bs, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/file.no-sanitizer")
resp := MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
div := doc.Find("div.file-view")
data, err := div.Html()
assert.NoError(t, err)
assert.Equal(t, `<script>window.alert("hi")</script>`, strings.TrimSpace(data))
})
})
doc := NewHTMLParser(t, bytes.NewBuffer(bs))
div := doc.Find("div.file-view")
data, err := div.Html()
assert.NoError(t, err)
assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
t.Run("RenderContentDirectly", func(t *testing.T) {
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
r := markup.GetRendererByFileName("a.html").(*external.Renderer)
r.RenderContentMode = setting.RenderContentModeIframe
doc := NewHTMLParser(t, resp.Body)
div := doc.Find("div.file-view")
data, err := div.Html()
assert.NoError(t, err)
assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
})
req = NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
bs, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
doc = NewHTMLParser(t, bytes.NewBuffer(bs))
iframe := doc.Find("iframe")
assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", ""))
r := markup.GetRendererByFileName("any-file.html").(*external.Renderer)
defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)()
req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html")
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
bs, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy"))
assert.Equal(t, "<div>\n\ttest external renderer\n</div>\n", string(bs))
t.Run("RenderContentInIFrame", func(t *testing.T) {
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
doc := NewHTMLParser(t, resp.Body)
iframe := doc.Find("iframe")
assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", ""))
req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html")
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
bs, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy"))
assert.Equal(t, "<div>\n\ttest external renderer\n</div>\n", string(bs))
})
}

View File

@ -114,9 +114,16 @@ ENABLED = true
[markup.html]
ENABLED = true
FILE_EXTENSIONS = .html
RENDER_COMMAND = `go run build/test-echo.go`
IS_INPUT_FILE = false
RENDER_CONTENT_MODE=sanitized
RENDER_COMMAND = go run build/test-echo.go
;RENDER_COMMAND = cat
;IS_INPUT_FILE = true
RENDER_CONTENT_MODE = sanitized
[markup.no-sanitizer]
ENABLED = true
FILE_EXTENSIONS = .no-sanitizer
RENDER_COMMAND = echo '<script>window.alert("hi")</script>'
RENDER_CONTENT_MODE = no-sanitizer
[actions]
ENABLED = true

View File

@ -6,11 +6,9 @@ import {html, htmlRaw} from '../utils/html.ts';
const {mermaidMaxSourceCharacters} = window.config;
// margin removal is for https://github.com/mermaid-js/mermaid/issues/4907
const iframeCss = `:root {color-scheme: normal}
body {margin: 0; padding: 0; overflow: hidden}
#mermaid {display: block; margin: 0 auto}
blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`;
#mermaid {display: block; margin: 0 auto}`;
export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> {
// .markup code.language-mermaid