初始提交: Gitea 项目代码

This commit is contained in:
root
2026-05-30 22:47:36 +08:00
commit f288f76350
6116 changed files with 776822 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"errors"
"fmt"
"net/http"
"strings"
actions_model "gitea.dev/models/actions"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
"gitea.dev/modules/util"
"gitea.dev/services/context"
)
// GenerateActionsRunnerToken generates a new runner token for a given scope
func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
var genRequest private.GenerateTokenRequest
rd := ctx.Req.Body
defer rd.Close()
if err := json.NewDecoder(rd).Decode(&genRequest); err != nil {
log.Error("JSON Decode failed: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
owner, repo, err := parseScope(ctx, genRequest.Scope)
if err != nil {
log.Error("parseScope failed: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
}
token, err := actions_model.GetLatestRunnerToken(ctx, owner, repo)
if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) {
token, err = actions_model.NewRunnerToken(ctx, owner, repo)
if err != nil {
errMsg := fmt.Sprintf("error while creating runner token: %v", err)
log.Error("NewRunnerToken failed: %v", errMsg)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: errMsg,
})
return
}
} else if err != nil {
errMsg := fmt.Sprintf("could not get unactivated runner token: %v", err)
log.Error("GetLatestRunnerToken failed: %v", errMsg)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: errMsg,
})
return
}
ctx.PlainText(http.StatusOK, token.Token)
}
func parseScope(ctx *context.PrivateContext, scope string) (ownerID, repoID int64, err error) {
ownerID = 0
repoID = 0
if scope == "" {
return ownerID, repoID, nil
}
ownerName, repoName, found := strings.Cut(scope, "/")
u, err := user_model.GetUserByName(ctx, ownerName)
if err != nil {
return ownerID, repoID, err
}
ownerID = u.ID
if !found {
return ownerID, repoID, nil
}
r, err := repo_model.GetRepositoryByName(ctx, u.ID, repoName)
if err != nil {
return ownerID, repoID, err
}
repoID = r.ID
return ownerID, repoID, nil
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"fmt"
"net/http"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/private"
gitea_context "gitea.dev/services/context"
repo_service "gitea.dev/services/repository"
)
// SetDefaultBranch updates the default branch
func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
ownerName := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
branch := ctx.PathParam("branch")
ctx.Repo.Repository.DefaultBranch = branch
if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
if err := repo_model.UpdateDefaultBranch(ctx, ctx.Repo.Repository); err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
RepoID: ctx.Repo.Repository.ID,
}); err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
ctx.PlainText(http.StatusOK, "success")
}
+356
View File
@@ -0,0 +1,356 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"context"
"fmt"
"net/http"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/cache"
"gitea.dev/modules/cachegroup"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
gitea_context "gitea.dev/services/context"
pull_service "gitea.dev/services/pull"
repo_service "gitea.dev/services/repository"
)
// HookPostReceive updates services and users
func HookPostReceive(ctx *gitea_context.PrivateContext) {
opts := web.GetForm(ctx).(*private.HookOptions)
// We don't rely on RepoAssignment here because:
// a) we don't need the git repo in this function
// OUT OF DATE: we do need the git repo to sync the branch to the db now.
// b) our update function will likely change the repository in the db so we will need to refresh it
// c) we don't always need the repo
ownerName := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
// defer getting the repository at this point - as we should only retrieve it if we're going to call update
var (
repo *repo_model.Repository
gitRepo *git.Repository
)
defer gitRepo.Close() // it's safe to call Close on a nil pointer
updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs))
wasEmpty := false
for i := range opts.OldCommitIDs {
refFullName := opts.RefFullNames[i]
// Only trigger activity updates for changes to branches or
// tags. Updates to other refs (eg, refs/notes, refs/changes,
// or other less-standard refs spaces are ignored since there
// may be a very large number of them).
if refFullName.IsBranch() || refFullName.IsTag() {
if repo == nil {
repo = loadRepository(ctx, ownerName, repoName)
if ctx.Written() {
// Error handled in loadRepository
return
}
wasEmpty = repo.IsEmpty
}
option := &repo_module.PushUpdateOptions{
RefFullName: refFullName,
OldCommitID: opts.OldCommitIDs[i],
NewCommitID: opts.NewCommitIDs[i],
PusherID: opts.UserID,
PusherName: opts.UserName,
RepoUserName: ownerName,
RepoName: repoName,
}
updates = append(updates, option)
if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") {
// put the master/main branch first
// FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates.
// If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
// See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
// If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch.
copy(updates[1:], updates)
updates[0] = option
}
}
}
if repo != nil && len(updates) > 0 {
branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates))
for _, update := range updates {
if !update.RefFullName.IsBranch() {
continue
}
if repo == nil {
repo = loadRepository(ctx, ownerName, repoName)
if ctx.Written() {
return
}
wasEmpty = repo.IsEmpty
}
if update.IsDelRef() {
if err := git_model.MarkBranchAsDeleted(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil {
log.Error("Failed to mark branch as deleted: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to mark branch as deleted: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
} else {
branchesToSync = append(branchesToSync, update)
// TODO: should we return the error and return the error when pushing? Currently it will log the error and not prevent the pushing
pull_service.UpdatePullsRefs(ctx, repo, update)
}
}
if len(branchesToSync) > 0 {
var err error
gitRepo, err = gitrepo.OpenRepository(ctx, repo)
if err != nil {
log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
var (
branchNames = make([]string, 0, len(branchesToSync))
commitIDs = make([]string, 0, len(branchesToSync))
)
for _, update := range branchesToSync {
branchNames = append(branchNames, update.RefFullName.BranchName())
commitIDs = append(commitIDs, update.NewCommitID)
}
if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil {
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
}
if err := repo_service.PushUpdates(updates); err != nil {
log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
for i, update := range updates {
log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.RefFullName.BranchName())
}
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
}
// handle pull request merging, a pull request action should push at least 1 commit
if opts.PushTrigger == repo_module.PushTriggerPRMergeToBase {
handlePullRequestMerging(ctx, opts, ownerName, repoName, updates)
if ctx.Written() {
return
}
}
isPrivate := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate)
isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate)
// Handle Push Options
if isPrivate.Has() || isTemplate.Has() {
// load the repository
if repo == nil {
repo = loadRepository(ctx, ownerName, repoName)
if ctx.Written() {
// Error handled in loadRepository
return
}
wasEmpty = repo.IsEmpty
}
pusher, err := loadContextCacheUser(ctx, opts.UserID)
if err != nil {
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
perm, err := access_model.GetDoerRepoPermission(ctx, repo, pusher)
if err != nil {
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
if !perm.IsOwner() && !perm.IsAdmin() {
ctx.JSON(http.StatusNotFound, private.HookPostReceiveResult{
Err: "Permissions denied",
})
return
}
// FIXME: these options are not quite right, for example: changing visibility should do more works than just setting the is_private flag
// These options should only be used for "push-to-create"
if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() {
// TODO: it needs to do more work
repo.IsPrivate = isPrivate.Value()
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change visibility"})
}
}
if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() {
repo.IsTemplate = isTemplate.Value()
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_template"); err != nil {
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change template status"})
}
}
}
results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs))
// We have to reload the repo in case its state is changed above
repo = nil
var baseRepo *repo_model.Repository
// Now handle the pull request notification trailers
for i := range opts.OldCommitIDs {
refFullName := opts.RefFullNames[i]
newCommitID := opts.NewCommitIDs[i]
// If we've pushed a branch (and not deleted it)
if !git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() {
// First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo
if repo == nil {
repo = loadRepository(ctx, ownerName, repoName)
if ctx.Written() {
return
}
baseRepo = repo
if repo.IsFork {
if err := repo.GetBaseRepo(ctx); err != nil {
log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
RepoWasEmpty: wasEmpty,
})
return
}
if repo.BaseRepo.AllowsPulls(ctx) {
baseRepo = repo.BaseRepo
}
}
if !baseRepo.AllowsPulls(ctx) {
// We can stop there's no need to go any further
ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
RepoWasEmpty: wasEmpty,
})
return
}
}
branch := refFullName.BranchName()
if branch == baseRepo.DefaultBranch {
if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
RepoID: repo.ID,
}); err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{Err: err.Error()})
return
}
// If our branch is the default branch of an unforked repo - there's no PR to create or refer to
if !repo.IsFork {
results = append(results, private.HookPostReceiveBranchResult{})
continue
}
}
pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub)
if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf(
"Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err),
RepoWasEmpty: wasEmpty,
})
return
}
if pr == nil {
results = append(results, private.HookPostReceiveBranchResult{
Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(ctx),
Create: true,
Branch: branch,
URL: fmt.Sprintf("%s/pulls/new/%s", repo.HTMLURL(), util.PathEscapeSegments(branch)),
})
} else {
results = append(results, private.HookPostReceiveBranchResult{
Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(ctx),
Create: false,
Branch: branch,
URL: fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index),
})
}
}
}
ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
Results: results,
RepoWasEmpty: wasEmpty,
})
}
func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, error) {
return cache.GetWithContextCache(ctx, cachegroup.User, id, user_model.GetUserByID)
}
// handlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit
func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, ownerName, repoName string, updates []*repo_module.PushUpdateOptions) {
if len(updates) == 0 {
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID),
})
return
}
pr, err := issues_model.GetPullRequestByID(ctx, opts.PullRequestID)
if err != nil {
log.Error("GetPullRequestByID[%d]: %v", opts.PullRequestID, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "GetPullRequestByID failed"})
return
}
pusher, err := loadContextCacheUser(ctx, opts.UserID)
if err != nil {
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Load pusher user failed"})
return
}
// FIXME: Maybe we need a `PullRequestStatusMerged` status for PRs that are merged, currently we use the previous status
// here to keep it as before, that maybe PullRequestStatusMergeable
if _, err := pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), pusher, pr.Status); err != nil {
log.Error("Failed to update PR to merged: %v", err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to update PR to merged"})
}
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"testing"
issues_model "gitea.dev/models/issues"
pull_model "gitea.dev/models/pull"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/private"
repo_module "gitea.dev/modules/repository"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
func TestHandlePullRequestMerging(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
pr, err := issues_model.GetUnmergedPullRequest(t.Context(), 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub)
assert.NoError(t, err)
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
err = pull_model.ScheduleAutoMerge(t.Context(), user1, pr.ID, repo_model.MergeStyleSquash, "squash merge a pr", false)
assert.NoError(t, err)
autoMerge := unittest.AssertExistsAndLoadBean(t, &pull_model.AutoMerge{PullID: pr.ID})
ctx, resp := contexttest.MockPrivateContext(t, "/")
handlePullRequestMerging(ctx, &private.HookOptions{
PullRequestID: pr.ID,
UserID: 2,
}, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, []*repo_module.PushUpdateOptions{
{NewCommitID: "01234567"},
})
assert.Empty(t, resp.Body.String())
pr, err = issues_model.GetPullRequestByID(t.Context(), pr.ID)
assert.NoError(t, err)
assert.True(t, pr.HasMerged)
assert.Equal(t, "01234567", pr.MergedCommitID)
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{ID: autoMerge.ID})
}
+553
View File
@@ -0,0 +1,553 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"errors"
"fmt"
"net/http"
"os"
asymkey_model "gitea.dev/models/asymkey"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
perm_model "gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/agit"
gitea_context "gitea.dev/services/context"
pull_service "gitea.dev/services/pull"
)
type preReceiveContext struct {
*gitea_context.PrivateContext
// loadedPusher indicates that where the following information are loaded
loadedPusher bool
user *user_model.User // it's the org user if a DeployKey is used
userPerm access_model.Permission
deployKeyAccessMode perm_model.AccessMode
canCreatePullRequest bool
checkedCanCreatePullRequest bool
canWriteCode bool
checkedCanWriteCode bool
protectedTags []*git_model.ProtectedTag
gotProtectedTags bool
env []string
opts *private.HookOptions
branchName string
}
// CanWriteCode returns true if pusher can write code
func (ctx *preReceiveContext) CanWriteCode() bool {
if !ctx.checkedCanWriteCode {
if !ctx.loadPusherAndPermission() {
return false
}
ctx.canWriteCode = issues_model.CanMaintainerWriteToBranch(ctx, ctx.userPerm, ctx.branchName, ctx.user) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
ctx.checkedCanWriteCode = true
}
return ctx.canWriteCode
}
// AssertCanWriteCode returns true if pusher can write code
func (ctx *preReceiveContext) AssertCanWriteCode() bool {
if !ctx.CanWriteCode() {
if ctx.Written() {
return false
}
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "User permission denied for writing.",
})
return false
}
return true
}
// CanCreatePullRequest returns true if pusher can create pull requests
func (ctx *preReceiveContext) CanCreatePullRequest() bool {
if !ctx.checkedCanCreatePullRequest {
if !ctx.loadPusherAndPermission() {
return false
}
ctx.canCreatePullRequest = ctx.userPerm.CanRead(unit.TypePullRequests)
ctx.checkedCanCreatePullRequest = true
}
return ctx.canCreatePullRequest
}
// AssertCreatePullRequest returns true if can create pull requests
func (ctx *preReceiveContext) AssertCreatePullRequest() bool {
if !ctx.CanCreatePullRequest() {
if ctx.Written() {
return false
}
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "User permission denied for creating pull-request.",
})
return false
}
return true
}
// HookPreReceive checks whether a individual commit is acceptable
func HookPreReceive(ctx *gitea_context.PrivateContext) {
opts := web.GetForm(ctx).(*private.HookOptions)
ourCtx := &preReceiveContext{
PrivateContext: ctx,
env: generateGitEnv(opts), // Generate git environment for checking commits
opts: opts,
}
// Iterate across the provided old commit IDs
for i := range opts.OldCommitIDs {
oldCommitID := opts.OldCommitIDs[i]
newCommitID := opts.NewCommitIDs[i]
refFullName := opts.RefFullNames[i]
switch {
case refFullName.IsBranch():
preReceiveBranch(ourCtx, oldCommitID, newCommitID, refFullName)
case refFullName.IsTag():
preReceiveTag(ourCtx, refFullName)
case git.DefaultFeatures().SupportProcReceive && refFullName.IsFor():
preReceiveFor(ourCtx, refFullName)
default:
ourCtx.AssertCanWriteCode()
}
if ctx.Written() {
return
}
}
ctx.PlainText(http.StatusOK, "ok")
}
func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) {
branchName := refFullName.BranchName()
ctx.branchName = branchName
if !ctx.AssertCanWriteCode() {
return
}
repo := ctx.Repo.Repository
gitRepo := ctx.Repo.GitRepo
objectFormat := ctx.Repo.GetObjectFormat()
defaultBranch := repo.DefaultBranch
if ctx.opts.IsWiki && repo.DefaultWikiBranch != "" {
defaultBranch = repo.DefaultWikiBranch
}
if branchName == defaultBranch && newCommitID == objectFormat.EmptyObjectID().String() {
log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName),
})
return
}
protectBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
if err != nil {
log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
// Allow pushes to non-protected branches
if protectBranch == nil {
return
}
protectBranch.Repo = repo
// This ref is a protected branch.
//
// First of all we need to enforce absolutely:
//
// 1. Detect and prevent deletion of the branch
if newCommitID == objectFormat.EmptyObjectID().String() {
log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("branch %s is protected from deletion", branchName),
})
return
}
isForcePush := false
// 2. Disallow force pushes to protected branches
if oldCommitID != objectFormat.EmptyObjectID().String() {
output, _, err := gitrepo.RunCmdString(ctx,
repo,
gitcmd.NewCommand("rev-list", "--max-count=1").
AddDynamicArguments(oldCommitID, "^"+newCommitID).
WithEnv(ctx.env),
)
if err != nil {
log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Fail to detect force push: %v", err),
})
return
} else if len(output) > 0 {
if protectBranch.CanForcePush {
isForcePush = true
} else {
log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("branch %s is protected from force push", branchName),
})
return
}
}
}
// 3. Enforce require signed commits
if protectBranch.RequireSignedCommits {
err := verifyCommits(oldCommitID, newCommitID, gitRepo, ctx.env)
if err != nil {
if !isErrUnverifiedCommit(err) {
log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err),
})
return
}
unverifiedCommit := err.(*errUnverifiedCommit).sha
log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit),
})
return
}
}
// Now there are several tests which can be overridden:
//
// 4. Check protected file patterns - this is overridable from the UI
changedProtectedfiles := false
protectedFilePath := ""
globs := protectBranch.GetProtectedFilePatterns()
if len(globs) > 0 {
_, err := pull_service.CheckFileProtection(gitRepo, branchName, oldCommitID, newCommitID, globs, 1, ctx.env)
if err != nil {
if !pull_service.IsErrFilePathProtected(err) {
log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
})
return
}
changedProtectedfiles = true
protectedFilePath = err.(pull_service.ErrFilePathProtected).Path
}
}
// 5. Check if the doer is allowed to push (and force-push if the incoming push is a force-push)
var canPush bool
if ctx.opts.DeployKeyID != 0 {
// This flag is only ever true if protectBranch.CanForcePush is true
if isForcePush {
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableForcePushAllowlist || protectBranch.ForcePushAllowlistDeployKeys)
} else {
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
}
} else {
user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
if err != nil {
log.Error("Unable to GetUserByID for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to GetUserByID for commits from %s to %s: %v", oldCommitID, newCommitID, err),
})
return
}
if isForcePush {
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, user)
} else {
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user)
}
}
// 6. If we're not allowed to push directly
if !canPush {
// Is this is a merge from the UI/API?
if ctx.opts.PullRequestID == 0 {
// 6a. If we're not merging from the UI/API then there are two ways we got here:
//
// We are changing a protected file and we're not allowed to do that
if changedProtectedfiles {
log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
})
return
}
// Allow commits that only touch unprotected files
globs := protectBranch.GetUnprotectedFilePatterns()
if len(globs) > 0 {
unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(gitRepo, branchName, oldCommitID, newCommitID, globs, ctx.env)
if err != nil {
log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
})
return
}
if unprotectedFilesOnly {
// Commit only touches unprotected files, this is allowed
return
}
}
// Or we're simply not able to push to this protected branch
if isForcePush {
log.Warn("Forbidden: User %d is not allowed to force-push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Not allowed to force-push to protected branch " + branchName,
})
return
}
log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Not allowed to push to protected branch " + branchName,
})
return
}
// 6b. Merge (from UI or API)
// Get the PR, user and permissions for the user in the repository
pr, err := issues_model.GetPullRequestByID(ctx, ctx.opts.PullRequestID)
if err != nil {
log.Error("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err),
})
return
}
// although we should have called `loadPusherAndPermission` before, here we call it explicitly again because we need to access ctx.user below
if !ctx.loadPusherAndPermission() {
// if error occurs, loadPusherAndPermission had written the error response
return
}
// Now check if the user is allowed to merge PRs for this repository
// Note: we can use ctx.perm and ctx.user directly as they will have been loaded above
allowedMerge, err := pull_service.IsUserAllowedToMerge(ctx, pr, ctx.userPerm, ctx.user)
if err != nil {
log.Error("Error calculating if allowed to merge: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err),
})
return
}
if !allowedMerge {
log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", ctx.opts.UserID, branchName, repo, pr.Index)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Not allowed to push to protected branch " + branchName,
})
return
}
// If we can bypass branch protection we can ignore status checks, reviews and protected files
if git_model.CanBypassBranchProtection(ctx, protectBranch, ctx.user, ctx.userPerm.IsAdmin()) {
return
}
// Now if we're not an admin - we can't overwrite protected files so fail now
if changedProtectedfiles {
log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
})
return
}
// Check all status checks and reviews are ok
if err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil {
if errors.Is(err, pull_service.ErrNotReadyToMerge) {
log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error())
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()),
})
return
}
log.Error("Unable to check if mergeable: protected branch %s in %-v and pr #%d. Error: %v", ctx.opts.UserID, branchName, repo, pr.Index, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", ctx.opts.PullRequestID, err),
})
return
}
}
}
func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) {
if !ctx.AssertCanWriteCode() {
return
}
tagName := refFullName.TagName()
if !ctx.gotProtectedTags {
var err error
ctx.protectedTags, err = git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
if err != nil {
log.Error("Unable to get protected tags for %-v Error: %v", ctx.Repo.Repository, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
ctx.gotProtectedTags = true
}
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
if !isAllowed {
log.Warn("Forbidden: Tag %s in %-v is protected", tagName, ctx.Repo.Repository)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Tag %s is protected", tagName),
})
return
}
}
func preReceiveFor(ctx *preReceiveContext, refFullName git.RefName) {
if !ctx.AssertCreatePullRequest() {
return
}
if ctx.Repo.Repository.IsEmpty {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Can't create pull request for an empty repository.",
})
return
}
if ctx.opts.IsWiki {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Pull requests are not supported on the wiki.",
})
return
}
_, _, err := agit.GetAgitBranchInfo(ctx, ctx.Repo.Repository.ID, refFullName.ForBranchName())
if err != nil {
if !errors.Is(err, util.ErrNotExist) {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Unexpected ref: %s", refFullName),
})
} else {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
}
}
}
func generateGitEnv(opts *private.HookOptions) (env []string) {
env = os.Environ()
if opts.GitAlternativeObjectDirectories != "" {
env = append(env,
private.GitAlternativeObjectDirectories+"="+opts.GitAlternativeObjectDirectories)
}
if opts.GitObjectDirectory != "" {
env = append(env,
private.GitObjectDirectory+"="+opts.GitObjectDirectory)
}
if opts.GitQuarantinePath != "" {
env = append(env,
private.GitQuarantinePath+"="+opts.GitQuarantinePath)
}
return env
}
// loadPusherAndPermission returns false if an error occurs, and it writes the error response
func (ctx *preReceiveContext) loadPusherAndPermission() bool {
if ctx.loadedPusher {
return true
}
if ctx.opts.UserID == user_model.ActionsUserID {
taskID := ctx.opts.ActionsTaskID
ctx.user = user_model.NewActionsUserWithTaskID(taskID)
if taskID == 0 {
log.Error("HookPreReceive: ActionsUser with task ID 0")
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: "ActionsUser with task ID 0",
})
return false
}
userPerm, err := access_model.GetActionsUserRepoPermission(ctx, ctx.Repo.Repository, ctx.user, taskID)
if err != nil {
log.Error("Unable to get Actions user repo permission for task %d Error: %v", taskID, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get Actions user repo permission for task %d Error: %v", taskID, err),
})
return false
}
ctx.userPerm = userPerm
} else {
user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
if err != nil {
log.Error("Unable to get User id %d Error: %v", ctx.opts.UserID, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get User id %d Error: %v", ctx.opts.UserID, err),
})
return false
}
ctx.user = user
userPerm, err := access_model.GetDoerRepoPermission(ctx, ctx.Repo.Repository, user)
if err != nil {
log.Error("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err),
})
return false
}
ctx.userPerm = userPerm
}
if ctx.opts.DeployKeyID != 0 {
deployKey, err := asymkey_model.GetDeployKeyByID(ctx, ctx.opts.DeployKeyID)
if err != nil {
log.Error("Unable to get DeployKey id %d Error: %v", ctx.opts.DeployKeyID, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get DeployKey id %d Error: %v", ctx.opts.DeployKeyID, err),
})
return false
}
ctx.deployKeyAccessMode = deployKey.Mode
}
ctx.loadedPusher = true
return true
}
+51
View File
@@ -0,0 +1,51 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"errors"
"net/http"
issues_model "gitea.dev/models/issues"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
"gitea.dev/modules/web"
"gitea.dev/services/agit"
gitea_context "gitea.dev/services/context"
)
// HookProcReceive proc-receive hook - only handles agit Proc-Receive requests at present
func HookProcReceive(ctx *gitea_context.PrivateContext) {
opts := web.GetForm(ctx).(*private.HookOptions)
if !git.DefaultFeatures().SupportProcReceive {
ctx.Status(http.StatusNotFound)
return
}
results, err := agit.ProcReceive(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, opts)
if err != nil {
if errors.Is(err, issues_model.ErrMustCollaborator) {
ctx.JSON(http.StatusUnauthorized, private.Response{
Err: err.Error(), UserMsg: "You must be a collaborator to create pull request.",
})
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSON(http.StatusUnauthorized, private.Response{
Err: err.Error(), UserMsg: "Cannot create pull request because you are blocked by the repository owner.",
})
} else {
log.Error("agit.ProcReceive failed: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
}
return
}
ctx.JSON(http.StatusOK, private.HookProcReceiveResult{
Results: results,
})
}
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"bufio"
"io"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
asymkey_service "gitea.dev/services/asymkey"
)
// This file contains commit verification functions for refs passed across in hooks
func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []string) error {
var command *gitcmd.Command
objectFormat, _ := repo.GetObjectFormat()
if oldCommitID == objectFormat.EmptyObjectID().String() {
// When creating a new branch, the oldCommitID is empty, by using "newCommitID --not --all":
// List commits that are reachable by following the newCommitID, exclude "all" existing heads/tags commits
// So, it only lists the new commits received, doesn't list the commits already present in the receiving repository
command = gitcmd.NewCommand("rev-list").AddDynamicArguments(newCommitID).AddArguments("--not", "--all")
} else {
command = gitcmd.NewCommand("rev-list").AddDynamicArguments(oldCommitID + "..." + newCommitID)
}
// This is safe as force pushes are already forbidden
stdoutReader, stdoutReaderClose := command.MakeStdoutPipe()
defer stdoutReaderClose()
err := command.WithEnv(env).
WithDir(repo.Path).
WithPipelineFunc(func(ctx gitcmd.Context) error {
err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
return ctx.CancelPipeline(err)
}).
Run(repo.Ctx)
if err != nil && !isErrUnverifiedCommit(err) {
log.Error("Unable to check commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err)
}
return err
}
func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository, env []string) error {
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Text()
err := readAndVerifyCommit(line, repo, env)
if err != nil {
return err
}
}
return scanner.Err()
}
func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error {
commitID := git.MustIDFromString(sha)
cmd := gitcmd.NewCommand("cat-file", "commit").AddDynamicArguments(sha)
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
defer stdoutReaderClose()
return cmd.WithEnv(env).
WithDir(repo.Path).
WithPipelineFunc(func(ctx gitcmd.Context) error {
commit, err := git.CommitFromReader(repo, commitID, stdoutReader)
if err != nil {
return err
}
verification := asymkey_service.ParseCommitWithSignature(ctx, commit)
if !verification.Verified {
return ctx.CancelPipeline(&errUnverifiedCommit{commit.ID.String()})
}
return nil
}).
Run(repo.Ctx)
}
type errUnverifiedCommit struct {
sha string
}
func (e *errUnverifiedCommit) Error() string {
return "Unverified commit: " + e.sha
}
func isErrUnverifiedCommit(err error) bool {
_, ok := err.(*errUnverifiedCommit)
return ok
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"testing"
"gitea.dev/models/unittest"
"gitea.dev/modules/git"
"github.com/stretchr/testify/assert"
)
var testReposDir = "tests/repos/"
func TestVerifyCommits(t *testing.T) {
unittest.PrepareTestEnv(t)
gitRepo, err := git.OpenRepository(t.Context(), testReposDir+"repo1_hook_verification")
if err != nil {
defer gitRepo.Close()
}
assert.NoError(t, err)
objectFormat, err := gitRepo.GetObjectFormat()
assert.NoError(t, err)
testCases := []struct {
base, head string
verified bool
}{
{"72920278f2f999e3005801e5d5b8ab8139d3641c", "d766f2917716d45be24bfa968b8409544941be32", true},
{objectFormat.EmptyObjectID().String(), "93eac826f6188f34646cea81bf426aa5ba7d3bfe", true}, // New branch with verified commit
{"9779d17a04f1e2640583d35703c62460b2d86e0a", "72920278f2f999e3005801e5d5b8ab8139d3641c", false},
{objectFormat.EmptyObjectID().String(), "9ce3f779ae33f31fce17fac3c512047b75d7498b", false}, // New branch with unverified commit
}
for _, tc := range testCases {
err = verifyCommits(tc.base, tc.head, gitRepo, nil)
if tc.verified {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
}
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package private contains all internal routes. The package name "internal" isn't usable because Golang reserves it for disabling cross-package usage.
package private
import (
"crypto/subtle"
"net/http"
"strings"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
"gitea.dev/modules/setting"
"gitea.dev/modules/web"
"gitea.dev/routers/common"
"gitea.dev/routers/web/misc"
"gitea.dev/services/context"
"gitea.com/go-chi/binding"
chi_middleware "github.com/go-chi/chi/v5/middleware"
)
func authInternal(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if setting.InternalToken == "" {
log.Warn(`The INTERNAL_TOKEN setting is missing from the configuration file: %q, internal API can't work.`, setting.CustomConf)
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
tokens := req.Header.Get("X-Gitea-Internal-Auth") // TODO: use something like JWT or HMAC to avoid passing the token in the clear
after, found := strings.CutPrefix(tokens, "Bearer ")
authSucceeded := found && subtle.ConstantTimeCompare([]byte(after), []byte(setting.InternalToken)) == 1
if !authSucceeded {
log.Debug("Forbidden attempt to access internal url: Authorization header: %s", tokens)
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
next.ServeHTTP(w, req)
})
}
// bind binding an obj to a handler
func bind[T any](_ T) any {
return func(ctx *context.PrivateContext) {
theObj := new(T) // create a new form obj for every request but not use obj directly
binding.Bind(ctx.Req, theObj)
web.SetForm(ctx, theObj)
}
}
// Routes registers all internal APIs routes to web application.
// These APIs will be invoked by internal commands for example `gitea serv` and etc.
func Routes() *web.Router {
r := web.NewRouter()
r.AfterRouting(context.PrivateContexter())
r.AfterRouting(authInternal)
// Log the real ip address of the request from SSH is really helpful for diagnosing sometimes.
// Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers.
r.AfterRouting(chi_middleware.RealIP)
r.Get("/dummy", misc.DummyOK)
r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent)
r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo)
r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog)
r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive)
r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext(), bind(private.HookOptions{}), HookPostReceive)
r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive)
r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch)
r.Get("/serv/none/{keyid}", ServNoCommand)
r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand)
r.Post("/manager/shutdown", Shutdown)
r.Post("/manager/restart", Restart)
r.Post("/manager/reload-templates", ReloadTemplates)
r.Post("/manager/flush-queues", bind(private.FlushOptions{}), FlushQueues)
r.Post("/manager/pause-logging", PauseLogging)
r.Post("/manager/resume-logging", ResumeLogging)
r.Post("/manager/release-and-reopen-logging", ReleaseReopenLogging)
r.Post("/manager/set-log-sql", SetLogSQL)
r.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger)
r.Post("/manager/remove-logger/{logger}/{writer}", RemoveLogger)
r.Get("/manager/processes", Processes)
r.Post("/mail/send", SendEmail)
r.Post("/restore_repo", RestoreRepo)
r.Post("/actions/generate_actions_runner_token", GenerateActionsRunnerToken)
r.Group("/repo", func() {
// FIXME: it is not right to use context.Contexter here because all routes here should use PrivateContext
// Fortunately, the LFS handlers are able to handle requests without a complete web context
common.AddOwnerRepoGitLFSRoutes(r, func(ctx *context.PrivateContext) {
webContext := &context.Context{Base: ctx.Base} // see above, it shouldn't manually construct the web context
ctx.SetContextValue(context.WebContextKey, webContext) // FIXME: this is not ideal but no other way at the moment
})
})
return r
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"fmt"
"net/http"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
gitea_context "gitea.dev/services/context"
)
// This file contains common functions relating to setting the Repository for the internal routes
// RepoAssignment assigns the repository and git repository to the private context
func RepoAssignment(ctx *gitea_context.PrivateContext) {
ownerName := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
repo := loadRepository(ctx, ownerName, repoName)
if ctx.Written() {
// Error handled in loadRepository
return
}
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
if err != nil {
log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
ctx.Repo = &gitea_context.Repository{
Repository: repo,
GitRepo: gitRepo,
}
}
func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string) *repo_model.Repository {
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
if err != nil {
log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
})
return nil
}
if repo.OwnerName == "" {
repo.OwnerName = ownerName
}
return repo
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"net/http"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/modules/private"
"gitea.dev/modules/timeutil"
"gitea.dev/services/context"
)
// UpdatePublicKeyInRepo update public key and deploy key updates
func UpdatePublicKeyInRepo(ctx *context.PrivateContext) {
keyID := ctx.PathParamInt64("id")
repoID := ctx.PathParamInt64("repoid")
if err := asymkey_model.UpdatePublicKeyUpdated(ctx, keyID); err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
deployKey, err := asymkey_model.GetDeployKeyByRepo(ctx, keyID, repoID)
if err != nil {
if asymkey_model.IsErrDeployKeyNotExist(err) {
ctx.PlainText(http.StatusOK, "success")
return
}
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
deployKey.UpdatedUnix = timeutil.TimeStampNow()
if err = asymkey_model.UpdateDeployKeyCols(ctx, deployKey, "updated_unix"); err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
ctx.PlainText(http.StatusOK, "success")
}
// AuthorizedPublicKeyByContent searches content as prefix (without comment part)
// and returns public key found.
func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) {
content := ctx.FormString("content")
publicKey, err := asymkey_model.SearchPublicKeyByContent(ctx, content)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
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)
}
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
stdCtx "context"
"fmt"
"net/http"
"strconv"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
"gitea.dev/modules/setting"
"gitea.dev/services/context"
"gitea.dev/services/mailer"
sender_service "gitea.dev/services/mailer/sender"
)
// SendEmail pushes messages to mail queue
//
// It doesn't wait before each message will be processed
func SendEmail(ctx *context.PrivateContext) {
if setting.MailService == nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: "Mail service is not enabled.",
})
return
}
var mail private.Email
rd := ctx.Req.Body
defer rd.Close()
if err := json.NewDecoder(rd).Decode(&mail); err != nil {
log.Error("JSON Decode failed: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
var emails []string
if len(mail.To) > 0 {
for _, uname := range mail.To {
user, err := user_model.GetUserByName(ctx, uname)
if err != nil {
err := fmt.Sprintf("Failed to get user information: %v", err)
log.Error(err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err,
})
return
}
if user != nil && len(user.Email) > 0 {
emails = append(emails, user.Email)
}
}
} else {
err := db.Iterate(ctx, nil, func(ctx stdCtx.Context, user *user_model.User) error {
if len(user.Email) > 0 && user.IsActive {
emails = append(emails, user.Email)
}
return nil
})
if err != nil {
err := fmt.Sprintf("Failed to find users: %v", err)
log.Error(err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err,
})
return
}
}
sendEmail(ctx, mail.Subject, mail.Message, emails)
}
func sendEmail(ctx *context.PrivateContext, subject, message string, to []string) {
for _, email := range to {
msg := sender_service.NewMessage(email, subject, message)
mailer.SendAsync(msg)
}
wasSent := strconv.Itoa(len(to))
ctx.PlainText(http.StatusOK, wasSent)
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"testing"
"gitea.dev/models/unittest"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
+195
View File
@@ -0,0 +1,195 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"fmt"
"net/http"
"gitea.dev/models/db"
"gitea.dev/modules/graceful"
"gitea.dev/modules/graceful/releasereopen"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
"gitea.dev/modules/queue"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/web"
"gitea.dev/services/context"
)
// ReloadTemplates reloads all the templates
func ReloadTemplates(ctx *context.PrivateContext) {
err := templates.ReloadAllTemplates()
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
UserMsg: fmt.Sprintf("Template error: %v", err),
})
return
}
ctx.PlainText(http.StatusOK, "success")
}
// FlushQueues flushes all the Queues
func FlushQueues(ctx *context.PrivateContext) {
opts := web.GetForm(ctx).(*private.FlushOptions)
if opts.NonBlocking {
// Save the hammer ctx here - as a new one is created each time you call this.
baseCtx := graceful.GetManager().HammerContext()
go func() {
err := queue.GetManager().FlushAll(baseCtx, opts.Timeout)
if err != nil {
log.Error("Flushing request timed-out with error: %v", err)
}
}()
ctx.JSON(http.StatusAccepted, private.Response{
UserMsg: "Flushing",
})
return
}
err := queue.GetManager().FlushAll(ctx, opts.Timeout)
if err != nil {
ctx.JSON(http.StatusRequestTimeout, private.Response{
UserMsg: fmt.Sprintf("%v", err),
})
}
ctx.PlainText(http.StatusOK, "success")
}
// PauseLogging pauses logging
func PauseLogging(ctx *context.PrivateContext) {
log.GetManager().PauseAll()
ctx.PlainText(http.StatusOK, "success")
}
// ResumeLogging resumes logging
func ResumeLogging(ctx *context.PrivateContext) {
log.GetManager().ResumeAll()
ctx.PlainText(http.StatusOK, "success")
}
// ReleaseReopenLogging releases and reopens logging files
func ReleaseReopenLogging(ctx *context.PrivateContext) {
if err := releasereopen.GetManager().ReleaseReopen(); err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Error during release and reopen: %v", err),
})
return
}
ctx.PlainText(http.StatusOK, "success")
}
// SetLogSQL re-sets database SQL logging
func SetLogSQL(ctx *context.PrivateContext) {
db.SetLogSQL(ctx, ctx.FormBool("on"))
ctx.PlainText(http.StatusOK, "success")
}
// RemoveLogger removes a logger
func RemoveLogger(ctx *context.PrivateContext) {
logger := ctx.PathParam("logger")
writer := ctx.PathParam("writer")
err := log.GetManager().GetLogger(logger).RemoveWriter(writer)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to remove log writer: %s %s %v", logger, writer, err),
})
return
}
ctx.PlainText(http.StatusOK, fmt.Sprintf("Removed %s %s", logger, writer))
}
// AddLogger adds a logger
func AddLogger(ctx *context.PrivateContext) {
opts := web.GetForm(ctx).(*private.LoggerOptions)
if len(opts.Logger) == 0 {
opts.Logger = log.DEFAULT
}
writerMode := log.WriterMode{}
writerType := opts.Mode
var flags string
var ok bool
if flags, ok = opts.Config["flags"].(string); !ok {
switch opts.Logger {
case "access":
flags = ""
case "router":
flags = "date,time"
default:
flags = "stdflags"
}
}
writerMode.Flags = log.FlagsFromString(flags)
if writerMode.Colorize, ok = opts.Config["colorize"].(bool); !ok && opts.Mode == "console" {
if _, ok := opts.Config["stderr"]; ok {
writerMode.Colorize = log.CanColorStderr
} else {
writerMode.Colorize = log.CanColorStdout
}
}
writerMode.Level = setting.Log.Level
if level, ok := opts.Config["level"].(string); ok {
writerMode.Level = log.LevelFromString(level)
}
writerMode.StacktraceLevel = setting.Log.StacktraceLogLevel
if stacktraceLevel, ok := opts.Config["level"].(string); ok {
writerMode.StacktraceLevel = log.LevelFromString(stacktraceLevel)
}
writerMode.Prefix, _ = opts.Config["prefix"].(string)
writerMode.Expression, _ = opts.Config["expression"].(string)
switch writerType {
case "console":
writerOption := log.WriterConsoleOption{}
writerOption.Stderr, _ = opts.Config["stderr"].(bool)
writerMode.WriterOption = writerOption
case "file":
writerOption := log.WriterFileOption{}
fileName, _ := opts.Config["filename"].(string)
writerOption.FileName = setting.LogPrepareFilenameForWriter(fileName, opts.Writer+".log")
writerOption.LogRotate, _ = opts.Config["rotate"].(bool)
maxSizeShift, _ := opts.Config["maxsize"].(int)
if maxSizeShift == 0 {
maxSizeShift = 28
}
writerOption.MaxSize = 1 << maxSizeShift
writerOption.DailyRotate, _ = opts.Config["daily"].(bool)
writerOption.MaxDays, _ = opts.Config["maxdays"].(int)
if writerOption.MaxDays == 0 {
writerOption.MaxDays = 7
}
writerOption.Compress, _ = opts.Config["compress"].(bool)
writerOption.CompressionLevel, _ = opts.Config["compressionLevel"].(int)
if writerOption.CompressionLevel == 0 {
writerOption.CompressionLevel = -1
}
writerMode.WriterOption = writerOption
case "conn":
writerOption := log.WriterConnOption{}
writerOption.ReconnectOnMsg, _ = opts.Config["reconnectOnMsg"].(bool)
writerOption.Reconnect, _ = opts.Config["reconnect"].(bool)
writerOption.Protocol, _ = opts.Config["net"].(string)
writerOption.Addr, _ = opts.Config["address"].(string)
writerMode.WriterOption = writerOption
default:
panic("invalid log writer mode: " + writerType)
}
writer, err := log.NewEventWriter(opts.Writer, writerType, writerMode)
if err != nil {
log.Error("Failed to create new log writer: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to create new log writer: %v", err),
})
return
}
log.GetManager().GetLogger(opts.Logger).AddWriters(writer)
ctx.PlainText(http.StatusOK, "success")
}
+160
View File
@@ -0,0 +1,160 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"bytes"
"fmt"
"io"
"net/http"
"runtime"
"time"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
process_module "gitea.dev/modules/process"
"gitea.dev/services/context"
)
// Processes prints out the processes
func Processes(ctx *context.PrivateContext) {
pid := ctx.FormString("cancel-pid")
if pid != "" {
process_module.GetManager().Cancel(process_module.IDType(pid))
runtime.Gosched()
time.Sleep(100 * time.Millisecond)
}
flat := ctx.FormBool("flat")
noSystem := ctx.FormBool("no-system")
stacktraces := ctx.FormBool("stacktraces")
json := ctx.FormBool("json")
var processes []*process_module.Process
goroutineCount := int64(0)
var processCount int
var err error
if stacktraces {
processes, processCount, goroutineCount, err = process_module.GetManager().ProcessStacktraces(flat, noSystem)
if err != nil {
log.Error("Unable to get stacktrace: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to get stacktraces: %v", err),
})
return
}
} else {
processes, processCount = process_module.GetManager().Processes(flat, noSystem)
}
if json {
ctx.JSON(http.StatusOK, map[string]any{
"TotalNumberOfGoroutines": goroutineCount,
"TotalNumberOfProcesses": processCount,
"Processes": processes,
})
return
}
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
if err := writeProcesses(ctx.Resp, processes, processCount, goroutineCount, "", flat); err != nil {
log.Error("Unable to write out process stacktrace: %v", err)
if !ctx.Written() {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to get stacktraces: %v", err),
})
}
return
}
}
func writeProcesses(out io.Writer, processes []*process_module.Process, processCount int, goroutineCount int64, indent string, flat bool) error {
if goroutineCount > 0 {
if _, err := fmt.Fprintf(out, "%sTotal Number of Goroutines: %d\n", indent, goroutineCount); err != nil {
return err
}
}
if _, err := fmt.Fprintf(out, "%sTotal Number of Processes: %d\n", indent, processCount); err != nil {
return err
}
if len(processes) > 0 {
if err := writeProcess(out, processes[0], " ", flat); err != nil {
return err
}
}
if len(processes) > 1 {
for _, process := range processes[1:] {
if _, err := fmt.Fprintf(out, "%s | \n", indent); err != nil {
return err
}
if err := writeProcess(out, process, " ", flat); err != nil {
return err
}
}
}
return nil
}
func writeProcess(out io.Writer, process *process_module.Process, indent string, flat bool) error {
sb := &bytes.Buffer{}
if flat {
if process.ParentPID != "" {
_, _ = fmt.Fprintf(sb, "%s+ PID: %s\t\tType: %s\n", indent, process.PID, process.Type)
} else {
_, _ = fmt.Fprintf(sb, "%s+ PID: %s:%s\tType: %s\n", indent, process.ParentPID, process.PID, process.Type)
}
} else {
_, _ = fmt.Fprintf(sb, "%s+ PID: %s\tType: %s\n", indent, process.PID, process.Type)
}
indent += "| "
_, _ = fmt.Fprintf(sb, "%sDescription: %s\n", indent, process.Description)
_, _ = fmt.Fprintf(sb, "%sStart: %s\n", indent, process.Start)
if len(process.Stacks) > 0 {
_, _ = fmt.Fprintf(sb, "%sGoroutines:\n", indent)
for _, stack := range process.Stacks {
indent := indent + " "
_, _ = fmt.Fprintf(sb, "%s+ Description: %s", indent, stack.Description)
if stack.Count > 1 {
_, _ = fmt.Fprintf(sb, "* %d", stack.Count)
}
_, _ = fmt.Fprintf(sb, "\n")
indent += "| "
if len(stack.Labels) > 0 {
_, _ = fmt.Fprintf(sb, "%sLabels: %q:%q", indent, stack.Labels[0].Name, stack.Labels[0].Value)
if len(stack.Labels) > 1 {
for _, label := range stack.Labels[1:] {
_, _ = fmt.Fprintf(sb, ", %q:%q", label.Name, label.Value)
}
}
_, _ = fmt.Fprintf(sb, "\n")
}
_, _ = fmt.Fprintf(sb, "%sStack:\n", indent)
indent += " "
for _, entry := range stack.Entry {
_, _ = fmt.Fprintf(sb, "%s+ %s\n", indent, entry.Function)
_, _ = fmt.Fprintf(sb, "%s| %s:%d\n", indent, entry.File, entry.Line)
}
}
}
if _, err := out.Write(sb.Bytes()); err != nil {
return err
}
sb.Reset()
if len(process.Children) > 0 {
if _, err := fmt.Fprintf(out, "%sChildren:\n", indent); err != nil {
return err
}
for _, child := range process.Children {
if err := writeProcess(out, child, indent+" ", flat); err != nil {
return err
}
}
}
return nil
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !windows
package private
import (
"net/http"
"gitea.dev/modules/graceful"
"gitea.dev/services/context"
)
// Restart causes the server to perform a graceful restart
func Restart(ctx *context.PrivateContext) {
graceful.GetManager().DoGracefulRestart()
ctx.PlainText(http.StatusOK, "success")
}
// Shutdown causes the server to perform a graceful shutdown
func Shutdown(ctx *context.PrivateContext) {
graceful.GetManager().DoGracefulShutdown()
ctx.PlainText(http.StatusOK, "success")
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build windows
package private
import (
"net/http"
"gitea.dev/modules/graceful"
"gitea.dev/modules/private"
"gitea.dev/services/context"
)
// Restart is not implemented for Windows based servers as they can't fork
func Restart(ctx *context.PrivateContext) {
ctx.JSON(http.StatusNotImplemented, private.Response{
UserMsg: "windows servers cannot be gracefully restarted - shutdown and restart manually",
})
}
// Shutdown causes the server to perform a graceful shutdown
func Shutdown(ctx *context.PrivateContext) {
graceful.GetManager().DoGracefulShutdown()
ctx.PlainText(http.StatusOK, "success")
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"io"
"net/http"
"gitea.dev/modules/json"
"gitea.dev/modules/private"
myCtx "gitea.dev/services/context"
"gitea.dev/services/migrations"
)
// RestoreRepo restore a repository from data
func RestoreRepo(ctx *myCtx.PrivateContext) {
bs, err := io.ReadAll(ctx.Req.Body)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
params := struct {
RepoDir string
OwnerName string
RepoName string
Units []string
Validation bool
}{}
if err = json.Unmarshal(bs, &params); err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
if err := migrations.RestoreRepository(
ctx,
params.RepoDir,
params.OwnerName,
params.RepoName,
params.Units,
params.Validation,
); err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
} else {
ctx.PlainText(http.StatusOK, "success")
}
}
+434
View File
@@ -0,0 +1,434 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"fmt"
"net/http"
"strings"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
"gitea.dev/modules/setting"
"gitea.dev/services/context"
repo_service "gitea.dev/services/repository"
wiki_service "gitea.dev/services/wiki"
)
// ServNoCommand returns information about the provided keyid
func ServNoCommand(ctx *context.PrivateContext) {
keyID := ctx.PathParamInt64("keyid")
if keyID <= 0 {
ctx.JSON(http.StatusBadRequest, private.Response{
UserMsg: fmt.Sprintf("Bad key id: %d", keyID),
})
}
results := private.KeyAndOwner{}
key, err := asymkey_model.GetPublicKeyByID(ctx, keyID)
if err != nil {
if asymkey_model.IsErrKeyNotExist(err) {
ctx.JSON(http.StatusUnauthorized, private.Response{
UserMsg: fmt.Sprintf("Cannot find key: %d", keyID),
})
return
}
log.Error("Unable to get public key: %d Error: %v", keyID, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
results.Key = key
if key.Type == asymkey_model.KeyTypeUser || key.Type == asymkey_model.KeyTypePrincipal {
user, err := user_model.GetUserByID(ctx, key.OwnerID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.JSON(http.StatusUnauthorized, private.Response{
UserMsg: fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID),
})
return
}
log.Error("Unable to get owner with id: %d for public key: %d Error: %v", key.OwnerID, keyID, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
return
}
if !user.IsActive || user.ProhibitLogin {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Your account is disabled.",
})
return
}
results.Owner = user
}
ctx.JSON(http.StatusOK, &results)
}
// ServCommand returns information about the provided keyid
func ServCommand(ctx *context.PrivateContext) {
keyID := ctx.PathParamInt64("keyid")
ownerName := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
mode := perm.AccessMode(ctx.FormInt("mode"))
verb := ctx.FormString("verb")
// Set the basic parts of the results to return
results := private.ServCommandResults{
RepoName: repoName,
OwnerName: ownerName,
KeyID: keyID,
}
// Now because we're not translating things properly let's just default some English strings here
modeString := "read"
if mode > perm.AccessModeRead {
modeString = "write to"
}
// The default unit we're trying to look at is code
unitType := unit.TypeCode
// Unless we're a wiki...
if strings.HasSuffix(repoName, ".wiki") {
// in which case we need to look at the wiki
unitType = unit.TypeWiki
// And we'd better munge the reponame and tell downstream we're looking at a wiki
results.IsWiki = true
results.RepoName = repoName[:len(repoName)-5]
}
owner, err := user_model.GetUserByName(ctx, results.OwnerName)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err),
})
return
}
// Check if there is a user redirect for the requested owner
redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName)
if err != nil {
// User is fetching/cloning a non-existent repository
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
ctx.JSON(http.StatusNotFound, private.Response{
UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
})
return
}
redirectUser, err := user_model.GetUserByID(ctx, redirectedUserID)
if err != nil {
// User is fetching/cloning a non-existent repository
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
ctx.JSON(http.StatusNotFound, private.Response{
UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
})
return
}
log.Info("User %s has been redirected to %s", results.OwnerName, redirectUser.Name)
results.OwnerName = redirectUser.Name
owner = redirectUser
}
if !owner.IsOrganization() && !owner.IsActive {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Repository cannot be accessed, you could retry it later",
})
return
}
// Now get the Repository and set the results section
repoExist := true
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName)
if err != nil {
if !repo_model.IsErrRepoNotExist(err) {
log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err),
})
return
}
redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName)
if err == nil {
redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID)
if err == nil {
log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name)
results.RepoName = redirectedRepo.Name
results.OwnerName = redirectedRepo.OwnerName
repo = redirectedRepo
owner.ID = redirectedRepo.OwnerID
} else {
log.Warn("Repo %s/%s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", results.OwnerName, results.RepoName, redirectedRepoID)
}
}
if repo == nil {
repoExist = false
if mode == perm.AccessModeRead {
// User is fetching/cloning a non-existent repository
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
ctx.JSON(http.StatusNotFound, private.Response{
UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
})
return
}
}
}
if repoExist {
repo.Owner = owner
repo.OwnerName = ownerName
results.RepoID = repo.ID
if repo.IsBeingCreated() {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: "Repository is being created, you could retry after it finished",
})
return
}
if repo.IsBroken() {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: "Repository is in a broken state",
})
return
}
// We can shortcut at this point if the repo is a mirror
if mode > perm.AccessModeRead && repo.IsMirror {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName),
})
return
}
}
// Get the Public Key represented by the keyID
key, err := asymkey_model.GetPublicKeyByID(ctx, keyID)
if err != nil {
if asymkey_model.IsErrKeyNotExist(err) {
ctx.JSON(http.StatusNotFound, private.Response{
UserMsg: fmt.Sprintf("Cannot find key: %d", keyID),
})
return
}
log.Error("Unable to get public key: %d Error: %v", keyID, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err),
})
return
}
results.KeyName = key.Name
results.KeyID = key.ID
results.UserID = key.OwnerID
// If repo doesn't exist, deploy key doesn't make sense
if !repoExist && key.Type == asymkey_model.KeyTypeDeploy {
ctx.JSON(http.StatusNotFound, private.Response{
UserMsg: fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName),
})
return
}
// Deploy Keys have ownerID set to 0 therefore we can't use the owner
// So now we need to check if the key is a deploy key
// We'll keep hold of the deploy key here for permissions checking
var deployKey *asymkey_model.DeployKey
var user *user_model.User
if key.Type == asymkey_model.KeyTypeDeploy {
var err error
deployKey, err = asymkey_model.GetDeployKeyByRepo(ctx, key.ID, repo.ID)
if err != nil {
if asymkey_model.IsErrDeployKeyNotExist(err) {
ctx.JSON(http.StatusNotFound, private.Response{
UserMsg: fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName),
})
return
}
log.Error("Unable to get deploy for public (deploy) key: %d in %-v Error: %v", key.ID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName),
})
return
}
results.DeployKeyID = deployKey.ID
results.KeyName = deployKey.Name
// FIXME: Deploy keys aren't really the owner of the repo pushing changes
// however we don't have good way of representing deploy keys in hook.go
// so for now use the owner of the repository
results.UserName = results.OwnerName
results.UserID = repo.OwnerID
if !repo.Owner.KeepEmailPrivate {
results.UserEmail = repo.Owner.Email
}
} else {
// Get the user represented by the Key
var err error
user, err = user_model.GetUserByID(ctx, key.OwnerID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.JSON(http.StatusUnauthorized, private.Response{
UserMsg: fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID),
})
return
}
log.Error("Unable to get owner: %d for public key: %d:%s Error: %v", key.OwnerID, key.ID, key.Name, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName),
})
return
}
if !user.IsActive || user.ProhibitLogin {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Your account is disabled.",
})
return
}
results.UserName = user.Name
if !user.KeepEmailPrivate {
results.UserEmail = user.Email
}
}
// Don't allow pushing if the repo is archived
if repoExist && mode > perm.AccessModeRead && repo.IsArchived {
ctx.JSON(http.StatusUnauthorized, private.Response{
UserMsg: fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName),
})
return
}
// Permissions checking:
if repoExist &&
(mode > perm.AccessModeRead ||
repo.IsPrivate ||
owner.Visibility.IsPrivate() ||
(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
setting.Service.RequireSignInViewStrict) {
if key.Type == asymkey_model.KeyTypeDeploy {
if deployKey.Mode < mode {
ctx.JSON(http.StatusUnauthorized, private.Response{
UserMsg: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName),
})
return
}
} else {
// Because of the special ref "refs/for" (AGit) we will need to delay write permission check,
// AGit flow needs to write its own ref when the doer has "reader" permission (allowing to create PR).
// The real permission check is done in HookPreReceive (routers/private/hook_pre_receive.go).
// Here it should relax the permission check for "git push (git-receive-pack)", but not for others like LFS operations.
if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode && verb == git.CmdVerbReceivePack {
mode = perm.AccessModeRead
}
perm, err := access_model.GetDoerRepoPermission(ctx, repo, user)
if err != nil {
log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err),
})
return
}
userMode := perm.UnitAccessMode(unitType)
if userMode < mode {
log.Warn("Failed authentication attempt for %s with key %s (not authorized to %s %s/%s) from %s", user.Name, key.Name, modeString, ownerName, repoName, ctx.RemoteAddr())
ctx.JSON(http.StatusUnauthorized, private.Response{
UserMsg: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName),
})
return
}
}
}
// We already know we aren't using a deploy key
if !repoExist {
owner, err := user_model.GetUserByName(ctx, ownerName)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err),
})
return
}
if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Push to create is not enabled for organizations.",
})
return
}
if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Push to create is not enabled for users.",
})
return
}
repo, err = repo_service.PushCreateRepo(ctx, user, owner, results.RepoName)
if err != nil {
log.Error("pushCreateRepo: %v", err)
ctx.JSON(http.StatusNotFound, private.Response{
UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
})
return
}
results.RepoID = repo.ID
}
if results.IsWiki {
// Ensure the wiki is enabled before we allow access to it
if _, err := repo.GetUnit(ctx, unit.TypeWiki); err != nil {
if repo_model.IsErrUnitTypeNotExist(err) {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "repository wiki is disabled",
})
return
}
log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err),
})
return
}
// Finally if we're trying to touch the wiki we should init it
if err = wiki_service.InitWiki(ctx, repo); err != nil {
log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err),
})
return
}
}
log.Debug("Serv Results:\nIsWiki: %t\nDeployKeyID: %d\nKeyID: %d\tKeyName: %s\nUserName: %s\nUserID: %d\nOwnerName: %s\nRepoName: %s\nRepoID: %d",
results.IsWiki,
results.DeployKeyID,
results.KeyID,
results.KeyName,
results.UserName,
results.UserID,
results.OwnerName,
results.RepoName,
results.RepoID)
ctx.JSON(http.StatusOK, results)
// We will update the keys in a different call.
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"net/http"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
"gitea.dev/modules/setting"
"gitea.dev/modules/web"
"gitea.dev/services/context"
)
// SSHLog hook to response ssh log
func SSHLog(ctx *context.PrivateContext) {
if !setting.Log.EnableSSHLog {
ctx.Status(http.StatusOK)
return
}
opts := web.GetForm(ctx).(*private.SSHLogOption)
if opts.IsError {
log.Error("ssh: %v", opts.Message)
ctx.Status(http.StatusOK)
return
}
log.Debug("ssh: %v", opts.Message)
ctx.Status(http.StatusOK)
}
@@ -0,0 +1 @@
ref: refs/heads/main
@@ -0,0 +1,6 @@
[core]
repositoryformatversion = 0
filemode = false
bare = true
symlinks = false
ignorecase = true
@@ -0,0 +1 @@
d766f2917716d45be24bfa968b8409544941be32 refs/heads/main
@@ -0,0 +1 @@
0000000000000000000000000000000000000000 d766f2917716d45be24bfa968b8409544941be32 Gitea <gitea@fake.local> 1693148474 +0800 push
@@ -0,0 +1 @@
0000000000000000000000000000000000000000 d766f2917716d45be24bfa968b8409544941be32 Gitea <gitea@fake.local> 1693148474 +0800 push
@@ -0,0 +1,2 @@
x•ŽK
1Ù ÒéüAÄS¸ï$Í"32 ooð®ŠWð òÞ{!žæ`–˜JC%¡.˜ $Ár]sѱe$ïmòâMƒ·)£÷±(O`ªbtlÐE[:;4–àHÐ1_û”rayýáþl“é’÷~“ÊE­L@cå€Xv…Mþã":µMÛƒG«_}À?Ý
@@ -0,0 +1,2 @@
x1
!ES{ŠéAwGGa 9EúQg W·Èí#¹AªÞû©ÕZ§/£‹€³Œ–p±ì(¤(ó®óBhÈÛ¼&áŸãÝ:pLY`ûÍãU†ð-µzŸÁ°ô†\µ×ZM:†ü¡¨Êå€óxJ/ûG}:µ3
@@ -0,0 +1,3 @@
x•ŽA
Â0E]ç³$™L“ ˆx•L2µ]´•
ÞÞê
@@ -0,0 +1,3 @@
x•’Ë®«FE3æ+zn%44æ!%QxƒÛ€s˜AÓ` 8Øæëã{£Ì2IM¶j•ª´¥Údèûf²Ìý2”‚"‡$§e‰¶
-(â ­Ä!´ÝJ"åaŲ@•BaîùHo3 ŸVØòå<$/)å$JøJDB¡•H¤§˜ü{¾ RRðûOù«nfšÿF†þOÀ‰
âq[°2„̇~ŒÍô¬Ô÷zjjðëÒLÛÅÀ·}prm¬Fqhþä `@Ø«¦ªš®ª¥Õ˜fî?3Ç[7г…ê¨Ð) ^™þuÿÖ¿,µÆl7©zÝÿr|&«Ou4Ø9Ó:µÎQjôû·êÕ1x±õå6ÍQ‡÷ƒÀ%Áåtû‰sò¸íV‰| ( V¿,aL,ù«G~²Ç¹‹‹r¥ùûî@·`·Àþ$[! XËŠep©Œæ[8 oýä(›« k£Z´Î³yóeÐ¹ÙÆÄ«Y²¿kÖd€¯6•3¾;3ÜÔ RÔi Þ‹dYÓDk91V]/Cê#º¾&ÿêpo´Fáb¯‹¶}§¹ô¦òuW&]+m xaqdÜIõX¯þ3
@@ -0,0 +1,3 @@
x•ŽA
Â0E]ç³$™L“ ˆx•L2µ]´•
ÞÞê
@@ -0,0 +1 @@
d766f2917716d45be24bfa968b8409544941be32
@@ -0,0 +1,127 @@
# GPG key for abcde@gitea.com
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
4akc
=OYIo
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
ZZtqph2TItCeV04HoaKHHc254akc
=PPG4
-----END PGP PRIVATE KEY BLOCK-----