feat: add deploy tokens (#37306)

Deploy keys only work over SSH. A deploy token is their counterpart for HTTPS: a repository scoped credential, used as the password of a Git request, with read or read and write access. It covers Git operations and LFS, and can be regenerated in place.

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Claude Mythos <noreply@anthropic.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
ToastyTheBot
2026-08-27 03:32:44 +08:00
committed by GitHub
parent 3c4d5a6a5c
commit 646ea0f253
76 changed files with 1594 additions and 831 deletions
+3 -8
View File
@@ -189,13 +189,7 @@ func repoAssignment() func(ctx *context.APIContext) {
repo.Owner = owner
ctx.Repo.Repository = repo
if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
ctx.Repo.Permission, err = access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
} else {
{
needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
@@ -228,7 +222,7 @@ func doerNeedTwoFactorAuth(ctx gocontext.Context, doer *user_model.User) (bool,
if !setting.TwoFactorAuthEnforced {
return false, nil
}
if doer == nil {
if doer == nil || !doer.IsIndividual() { // system doers like Actions tasks or deploy-keys can never enroll 2FA
return false, nil
}
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, doer.ID)
@@ -1448,6 +1442,7 @@ func Routes() *web.Router {
m.Group("/keys", func() {
m.Combo("").Get(repo.ListDeployKeys).
Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)
m.Post("/tokens", bind(api.CreateDeployKeyTokenOption{}), repo.CreateDeployToken)
m.Combo("/{id}").Get(repo.GetDeployKey).
Delete(repo.DeleteDeployKey)
}, reqToken(), reqAdmin())
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1
import (
"testing"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDoerNeedTwoFactorAuth(t *testing.T) {
defer test.MockVariableValue(&setting.TwoFactorAuthEnforced, true)()
for _, doer := range []*user_model.User{nil, user_model.NewActionsUser(), user_model.NewDeployKeyUser()} {
need, err := doerNeedTwoFactorAuth(t.Context(), doer)
require.NoError(t, err)
assert.False(t, need)
}
}
+53 -8
View File
@@ -11,6 +11,7 @@ import (
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/db"
deploykey_model "gitea.dev/models/deploykey"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
@@ -24,7 +25,7 @@ import (
)
// appendPrivateInformation appends the owner and key type information to api.PublicKey
func appendPrivateInformation(ctx stdCtx.Context, apiKey *api.DeployKey, key *asymkey_model.DeployKey, repository *repo_model.Repository) (*api.DeployKey, error) {
func appendPrivateInformation(ctx stdCtx.Context, apiKey *api.DeployKey, key *deploykey_model.DeployKey, repository *repo_model.Repository) (*api.DeployKey, error) {
apiKey.ReadOnly = key.Mode == perm.AccessModeRead
if repository.ID == key.RepoID {
apiKey.Repository = convert.ToRepo(ctx, repository, access_model.Permission{AccessMode: key.Mode})
@@ -78,14 +79,14 @@ func ListDeployKeys(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
opts := asymkey_model.ListDeployKeysOptions{
opts := deploykey_model.ListDeployKeysOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
KeyID: ctx.FormInt64("key_id"),
Fingerprint: ctx.FormString("fingerprint"),
}
keys, count, err := db.FindAndCount[asymkey_model.DeployKey](ctx, opts)
keys, count, err := db.FindAndCount[deploykey_model.DeployKey](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -133,7 +134,7 @@ func GetDeployKey(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
key, err := asymkey_model.GetDeployKeyByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
key, err := deploykey_model.GetDeployKeyByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorAuto(err)
return
@@ -160,13 +161,13 @@ func HandleCheckKeyStringError(ctx *context.APIContext, err error) {
// HandleAddKeyError handle add key error
func HandleAddKeyError(ctx *context.APIContext, err error) {
switch {
case asymkey_model.IsErrDeployKeyAlreadyExist(err):
case deploykey_model.IsErrDeployKeyAlreadyExist(err):
ctx.APIError(http.StatusUnprocessableEntity, "This key has already been added to this repository")
case asymkey_model.IsErrKeyAlreadyExist(err):
ctx.APIError(http.StatusUnprocessableEntity, "Key content has been used as non-deploy key")
case asymkey_model.IsErrKeyNameAlreadyUsed(err):
ctx.APIError(http.StatusUnprocessableEntity, "Key title has been used")
case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err):
case deploykey_model.IsErrDeployKeyNameAlreadyUsed(err):
ctx.APIError(http.StatusUnprocessableEntity, "A key with the same name already exists")
default:
ctx.APIErrorInternal(err)
@@ -213,7 +214,7 @@ func CreateDeployKey(ctx *context.APIContext) {
}
accessMode := util.Iif(form.ReadOnly, perm.AccessModeRead, perm.AccessModeWrite)
key, err := asymkey_model.AddDeployKey(ctx, ctx.Repo.Repository.ID, form.Title, content, accessMode)
key, err := deploykey_model.AddDeployKeySSH(ctx, ctx.Repo.Repository.ID, form.Title, content, accessMode)
if err != nil {
HandleAddKeyError(ctx, err)
return
@@ -221,6 +222,49 @@ func CreateDeployKey(ctx *context.APIContext) {
ctx.JSON(http.StatusCreated, convert.ToDeployKey(ctx, ctx.Repo.Repository, key))
}
// CreateDeployToken create a deploy token for a repository
func CreateDeployToken(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/keys/tokens repository repoCreateDeployToken
// ---
// summary: Add a deploy token to a repository, it authenticates git over HTTPS
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateDeployKeyTokenOption"
// responses:
// "201":
// "$ref": "#/responses/DeployKey"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm[*api.CreateDeployKeyTokenOption](ctx)
accessMode := util.Iif(form.ReadOnly, perm.AccessModeRead, perm.AccessModeWrite)
key, err := deploykey_model.AddDeployKeyToken(ctx, ctx.Repo.Repository.ID, form.Title, accessMode)
if err != nil {
HandleAddKeyError(ctx, err)
return
}
ctx.JSON(http.StatusCreated, convert.ToDeployKey(ctx, ctx.Repo.Repository, key))
}
// DeleteDeployKey delete deploy key for a repository
func DeleteDeployKey(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/keys/{id} repository repoDeleteKey
@@ -251,7 +295,8 @@ func DeleteDeployKey(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.PathParamInt64("id")); err != nil {
// a key that is already gone still leaves the caller with the state it asked for
if _, err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.PathParamInt64("id")); err != nil && !deploykey_model.IsErrDeployKeyNotExist(err) {
if asymkey_model.IsErrKeyAccessDenied(err) {
ctx.APIError(http.StatusForbidden, "You do not have access to this key")
} else {
+3
View File
@@ -53,6 +53,9 @@ type swaggerParameterBodies struct {
// in:body
CreateKeyOption api.CreateKeyOption
// in:body
CreateDeployKeyTokenOption api.CreateDeployKeyTokenOption
// in:body
RenameUserOption api.RenameUserOption
+8 -10
View File
@@ -58,16 +58,13 @@ func Search(ctx *context.APIContext) {
uid := ctx.FormInt64("uid")
var users []*user_model.User
var maxResults int64
var err error
switch uid {
case user_model.GhostUserID:
maxResults = 1
users = []*user_model.User{user_model.NewGhostUser()}
case user_model.ActionsUserID:
maxResults = 1
users = []*user_model.User{user_model.NewActionsUser()}
default:
if uid < 0 {
_, sysUser, _ := user_model.GetPossibleUserByID(ctx, uid)
if sysUser != nil && sysUser.ID == uid {
maxResults = 1
users = []*user_model.User{sysUser}
}
} else {
opts := user_model.SearchUserOptions{
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
@@ -77,6 +74,7 @@ func Search(ctx *context.APIContext) {
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
var err error
users, maxResults, err = user_model.SearchUsers(ctx, opts)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]any{
+2 -2
View File
@@ -21,7 +21,7 @@ func TestRenderPanicErrorPage(t *testing.T) {
t.Run("HTML", func(t *testing.T) {
w := httptest.NewRecorder()
req := &http.Request{URL: &url.URL{}, Header: http.Header{"Accept": []string{"text/html"}}}
req = req.WithContext(reqctx.NewRequestContextForTest(t.Context()))
req = req.WithContext(reqctx.NewRequestContextForTest(t))
renderPanicErrorPage(w, req, errors.New("fake panic error (for test only)"))
respContent := w.Body.String()
assert.Contains(t, respContent, `class="page-content status-page-500"`)
@@ -36,7 +36,7 @@ func TestRenderPanicErrorPage(t *testing.T) {
t.Run("Plain", func(t *testing.T) {
w := httptest.NewRecorder()
req := &http.Request{URL: &url.URL{}}
req = req.WithContext(reqctx.NewRequestContextForTest(t.Context()))
req = req.WithContext(reqctx.NewRequestContextForTest(t))
renderServiceUnavailable(w, req)
assert.Equal(t, "Service Unavailable", w.Body.String())
})
+6 -35
View File
@@ -4,18 +4,13 @@
package private
import (
"context"
"errors"
"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/log"
"gitea.dev/modules/private"
@@ -103,15 +98,11 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
setting.PanicInDevOrTesting("wiki hook-post-receive is not supported")
return
}
ownerName := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
repo := loadRepository(ctx, ownerName, repoName)
if ctx.Written() {
if !loadContextDoerPermission(ctx, opts.UserID, opts.UserExtDoerData) {
return
}
// now, repo can't be nil
repo := ctx.Repo.Repository
// first, collect updates and sync branches
updates := hookPostReceiveCollectPushUpdates(opts, repo)
if !hookPostReceiveSyncDatabaseBranches(ctx, opts, repo, updates) {
@@ -144,17 +135,7 @@ func hookPostReceiveUpdateRepoByOptions(ctx *gitea_context.PrivateContext, opts
isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate)
// Handle Push Options
if isPrivate.Has() || isTemplate.Has() {
pusher, err := loadContextCacheUser(ctx, opts.UserID)
if err != nil {
ctx.PrivateInternalErrorf("failed to load pusher user: %v", err)
return false
}
perm, err := access_model.GetDoerRepoPermission(ctx, repo, pusher)
if err != nil {
ctx.PrivateInternalErrorf("failed to load doer repo permission: %v", err)
return false
}
if !perm.IsOwner() && !perm.IsAdmin() {
if !ctx.Repo.Permission.IsAdmin() {
ctx.PrivateUserErrorf(http.StatusNotFound, "permission denied")
return false
}
@@ -171,13 +152,13 @@ func hookPostReceiveUpdateRepoByOptions(ctx *gitea_context.PrivateContext, opts
// yet; setting the flags directly is sufficient in this push-to-create case.
if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() {
repo.IsPrivate = isPrivate.Value()
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
log.Error("failed to update repo is_private: %v", err)
}
}
if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() {
repo.IsTemplate = isTemplate.Value()
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_template"); err != nil {
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_template"); err != nil {
log.Error("failed to update repo is_template: %v", err)
}
}
@@ -244,10 +225,6 @@ func hookPostReceiveRespondWithTrailer(ctx *gitea_context.PrivateContext, opts *
ctx.JSON(http.StatusOK, private.HookPostReceiveResult{Results: results})
}
func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, error) {
return cache.GetWithContextCache(ctx, cachegroup.User, id, user_model.GetUserByID)
}
// hookPostReceiveHandlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit
func hookPostReceiveHandlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, updates []*repo_module.PushUpdateOptions) bool {
if len(updates) == 0 {
@@ -261,15 +238,9 @@ func hookPostReceiveHandlePullRequestMerging(ctx *gitea_context.PrivateContext,
return false
}
pusher, err := loadContextCacheUser(ctx, opts.UserID)
if err != nil {
ctx.PrivateInternalErrorf("failed to load pusher user %d: %v", opts.UserID, err)
return false
}
// 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
_, err = pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), pusher, pr.Status)
_, err = pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), ctx.Doer, pr.Status)
if err != nil {
ctx.PrivateInternalErrorf("failed to set pr %d to merged: %v", pr.ID, err)
return false
+2 -1
View File
@@ -25,13 +25,14 @@ func TestHandlePullRequestMerging(t *testing.T) {
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
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, "/")
ctx.Doer = user2
hookPostReceiveHandlePullRequestMerging(ctx, &private.HookOptions{
PullRequestID: pr.ID,
UserID: 2,
+18 -76
View File
@@ -8,11 +8,8 @@ import (
"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"
@@ -27,29 +24,18 @@ import (
type preReceiveContext struct {
*gitea_context.PrivateContext
user *user_model.User // the "pusher", it's the org user if a DeployKey is used
userPerm access_model.Permission
deployKeyAccessMode perm_model.AccessMode
canCreatePullRequest bool
checkedCanCreatePullRequest bool
protectedTags []*git_model.ProtectedTag
gotProtectedTags bool
env []string
env []string
opts *private.HookOptions
// this context should only contain shared variables, mutable variables like "current branch name" shouldn't be put here
canWriteCodeUnitCached *bool
canCreatePullRequest *bool
protectedTags []*git_model.ProtectedTag
}
func (ctx *preReceiveContext) canWriteCodeUnit() bool {
if ctx.canWriteCodeUnitCached == nil {
canWrite := ctx.userPerm.CanWrite(unit.TypeCode) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
ctx.canWriteCodeUnitCached = &canWrite
ctx.canWriteCodeUnitCached = new(ctx.Repo.Permission.CanWrite(unit.TypeCode))
}
return *ctx.canWriteCodeUnitCached
}
@@ -63,7 +49,7 @@ func (ctx *preReceiveContext) canWriteCodeRef(refFullName git.RefName) bool {
if !refFullName.IsBranch() {
return false
}
return issues_model.CanMaintainerWriteToBranch(ctx, ctx.userPerm, refFullName.BranchName(), ctx.user)
return issues_model.CanMaintainerWriteToBranch(ctx, ctx.Repo.Permission, refFullName.BranchName(), ctx.Doer)
}
// assertCanWriteRef returns true if pusher can write to the code ref, otherwise it responds with 403 Forbidden and returns false
@@ -80,11 +66,10 @@ func (ctx *preReceiveContext) assertCanWriteRef(refFullName git.RefName) bool {
// CanCreatePullRequest returns true if pusher can create pull requests
func (ctx *preReceiveContext) CanCreatePullRequest() bool {
if !ctx.checkedCanCreatePullRequest {
ctx.canCreatePullRequest = ctx.userPerm.CanRead(unit.TypePullRequests)
ctx.checkedCanCreatePullRequest = true
if ctx.canCreatePullRequest == nil {
ctx.canCreatePullRequest = new(ctx.Repo.Permission.CanRead(unit.TypePullRequests))
}
return ctx.canCreatePullRequest
return *ctx.canCreatePullRequest
}
// AssertCreatePullRequest returns true if can create pull requests
@@ -102,6 +87,9 @@ func (ctx *preReceiveContext) AssertCreatePullRequest() bool {
// HookPreReceive checks whether a individual commit is acceptable
func HookPreReceive(ctx *gitea_context.PrivateContext) {
opts := web.GetForm[*private.HookOptions](ctx)
if !loadContextDoerPermission(ctx, opts.UserID, opts.UserExtDoerData) {
return
}
ourCtx := &preReceiveContext{
PrivateContext: ctx,
@@ -109,10 +97,6 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
opts: opts,
}
if !ourCtx.loadPusherAndPermission() {
return // if error occurs, loadPusherAndPermission had written the error response
}
// Iterate across the provided old commit IDs
for i := range opts.OldCommitIDs {
oldCommitID := opts.OldCommitIDs[i]
@@ -236,7 +220,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
// 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 {
if ctx.opts.UserID == user_model.DeployKeyUserID {
// This flag is only ever true if protectBranch.CanForcePush is true
if isForcePush {
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableForcePushAllowlist || protectBranch.ForcePushAllowlistDeployKeys)
@@ -245,9 +229,9 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
}
} else {
if isForcePush {
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, ctx.user)
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, ctx.Doer)
} else {
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, ctx.user)
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, ctx.Doer)
}
}
@@ -296,7 +280,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
// 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)
allowedMerge, err := pull_service.IsUserAllowedToMerge(ctx, pr, ctx.Repo.Permission, ctx.Doer)
if err != nil {
ctx.PrivateInternalErrorf("Error calculating if allowed to merge: %v", err)
return
@@ -308,7 +292,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
}
// 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()) {
if git_model.CanBypassBranchProtection(ctx, protectBranch, ctx.Doer, ctx.Repo.Permission.IsAdmin()) {
return
}
@@ -337,14 +321,14 @@ func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) {
tagName := refFullName.TagName()
if !ctx.gotProtectedTags {
if ctx.protectedTags == nil {
var err error
ctx.protectedTags, err = git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.PrivateInternalErrorf("Unable to get protected tags: %v", err)
return
}
ctx.gotProtectedTags = true
ctx.protectedTags = util.SliceNilAsEmpty(ctx.protectedTags)
}
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID)
@@ -399,45 +383,3 @@ func generateGitEnv(opts *private.HookOptions) (env []string) {
}
return env
}
// loadPusherAndPermission returns false if an error occurs, and it writes the error response
func (ctx *preReceiveContext) loadPusherAndPermission() bool {
if ctx.opts.UserID == user_model.ActionsUserID {
taskID := ctx.opts.ActionsTaskID
ctx.user = user_model.NewActionsUserWithTaskID(taskID)
if taskID == 0 {
ctx.PrivateUserErrorf(http.StatusInternalServerError, "ActionsUser with task ID 0")
return false
}
userPerm, err := access_model.GetActionsUserRepoPermission(ctx, ctx.Repo.Repository, ctx.user, taskID)
if err != nil {
ctx.PrivateInternalErrorf("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 {
ctx.PrivateInternalErrorf("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 {
ctx.PrivateInternalErrorf("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.Repo.Repository.ID, ctx.opts.DeployKeyID)
if err != nil {
ctx.PrivateInternalErrorf("Unable to get DeployKey id %d Error: %v", ctx.opts.DeployKeyID, err)
return false
}
ctx.deployKeyAccessMode = deployKey.Mode
}
return true
}
+6 -11
View File
@@ -7,7 +7,6 @@ import (
"testing"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"gitea.dev/modules/git"
@@ -19,7 +18,7 @@ import (
// TestPreReceiveCanWriteCodePerBranch ensures the maintainer-edit write grant is evaluated against
// the exact ref being pushed on every call, derived from that ref rather than shared mutable state.
// Otherwise a per-branch grant (an open PR with "allow edits from maintainers") could be batched
// Otherwise, a per-branch grant (an open PR with "allow edits from maintainers") could be batched
// together with a protected branch or a tag to escalate into full repository write.
func TestPreReceiveCanWriteCodePerBranch(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
@@ -45,16 +44,12 @@ func TestPreReceiveCanWriteCodePerBranch(t *testing.T) {
require.NoError(t, issues_model.NewPullRequest(t.Context(), baseRepo, pr.Issue, nil, nil, pr))
// The pusher is the base repo owner (the maintainer) with only read access on the head repo.
maintainer := baseRepo.Owner
headPerm, err := access.GetIndividualUserRepoPermission(t.Context(), headRepo, maintainer)
require.NoError(t, err)
mockCtx, _ := contexttest.MockPrivateContext(t, "/")
ctx := &preReceiveContext{
PrivateContext: mockCtx,
user: maintainer,
userPerm: headPerm,
}
ctx := &preReceiveContext{PrivateContext: mockCtx}
ctx.SetPathParam("owner", headRepo.OwnerName)
ctx.SetPathParam("repo", headRepo.Name)
RepoAssignment(ctx.PrivateContext)
loadContextDoerPermission(ctx.PrivateContext, baseRepo.OwnerID, "")
// The granted branch must be writable...
assert.True(t, ctx.canWriteCodeRef(git.RefNameFromBranch("granted-branch")))
+10 -1
View File
@@ -23,8 +23,17 @@ func HookProcReceive(ctx *gitea_context.PrivateContext) {
ctx.Status(http.StatusNotFound)
return
}
if !loadContextDoerPermission(ctx, opts.UserID, opts.UserExtDoerData) {
return
}
results, err := agit.ProcReceive(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, opts)
results, err := agit.ProcReceive(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, &agit.ProcReceiveOptions{
OldCommitIDs: opts.OldCommitIDs,
NewCommitIDs: opts.NewCommitIDs,
RefFullNames: opts.RefFullNames,
GitPushOptions: opts.GitPushOptions,
Doer: ctx.Doer,
})
if err != nil {
if errors.Is(err, issues_model.ErrMustCollaborator) {
ctx.PrivateUserErrorf(http.StatusUnauthorized, "You must be a collaborator to create pull request.")
+1 -1
View File
@@ -80,7 +80,7 @@ func Routes() *web.Router {
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/post-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookPostReceive)
r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive)
r.Get("/serv/none/{keyid}", ServNoCommand)
r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand)
+18 -4
View File
@@ -4,7 +4,9 @@
package private
import (
"gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/user"
"gitea.dev/modules/git"
gitea_context "gitea.dev/services/context"
)
@@ -27,10 +29,7 @@ func RepoAssignment(ctx *gitea_context.PrivateContext) {
ctx.PrivateInternalErrorf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
return
}
ctx.Repo = &gitea_context.Repository{
Repository: repo,
GitRepo: gitRepo,
}
ctx.Repo = &gitea_context.Repository{Repository: repo, GitRepo: gitRepo}
}
func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string) *repo_model.Repository {
@@ -44,3 +43,18 @@ func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName strin
}
return repo
}
func loadContextDoerPermission(ctx *gitea_context.PrivateContext, userID int64, extDoerData string) bool {
doer, err := user.GetDoerUser(ctx, userID, extDoerData)
if err != nil {
ctx.PrivateInternalErrorf("Failed to get user: %d, error: %v", userID, err)
return false
}
ctx.Doer = doer
ctx.Repo.Permission, err = access.GetDoerRepoPermission(ctx, ctx.Repo.Repository, doer)
if err != nil {
ctx.PrivateInternalErrorf("Failed to get permission for user: %d, error: %v", userID, err)
return false
}
return true
}
+4 -5
View File
@@ -7,7 +7,7 @@ import (
"net/http"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/modules/timeutil"
deploykey_model "gitea.dev/models/deploykey"
"gitea.dev/services/context"
)
@@ -20,17 +20,16 @@ func UpdatePublicKeyInRepo(ctx *context.PrivateContext) {
return
}
deployKey, err := asymkey_model.GetDeployKeyByRepoPublicKey(ctx, repoID, keyID)
deployKey, err := deploykey_model.GetDeployKeyByRepoPublicKey(ctx, repoID, keyID)
if err != nil {
if asymkey_model.IsErrDeployKeyNotExist(err) {
if deploykey_model.IsErrDeployKeyNotExist(err) {
ctx.PlainText(http.StatusOK, "success")
return
}
ctx.PrivateInternalErrorf("%v", err)
return
}
deployKey.UpdatedUnix = timeutil.TimeStampNow()
if err = asymkey_model.UpdateDeployKeyCols(ctx, deployKey, "updated_unix"); err != nil {
if err = deploykey_model.UpdateDeployKeyLastUsed(ctx, deployKey.ID); err != nil {
ctx.PrivateInternalErrorf("%v", err)
return
}
+35 -54
View File
@@ -8,6 +8,7 @@ import (
"strings"
asymkey_model "gitea.dev/models/asymkey"
deploykey_model "gitea.dev/models/deploykey"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
@@ -73,9 +74,9 @@ func ServCommand(ctx *context.PrivateContext) {
// Set the basic parts of the results to return
results := private.ServCommandResults{
OwnerName: reqOwnerName, // it might be changed if there is "renamed user redirection"
RepoName: reqRepoName, // it might be changed if there is "renamed repo redirection", or the repo is a wiki
KeyID: keyID,
OwnerName: reqOwnerName, // it might be changed if there is "renamed user redirection"
RepoName: reqRepoName, // it might be changed if there is "renamed repo redirection", or the repo is a wiki
PublicKeyID: keyID,
}
repoLogName := reqOwnerName + "/" + reqRepoName
@@ -184,40 +185,25 @@ func ServCommand(ctx *context.PrivateContext) {
ctx.PrivateInternalErrorf("Unable to get key: %d, error: %v", keyID, err)
return
}
results.KeyName = key.Name
results.KeyID = key.ID
results.UserID = key.OwnerID
results.PublicKeyID = key.ID
// 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 deployKey *deploykey_model.DeployKey
var user *user_model.User
if key.Type == asymkey_model.KeyTypeDeploy {
if repo == nil {
ctx.PrivateUserErrorf(http.StatusNotFound, "Cannot find repository %s", repoLogName)
return
}
deployKey, err = asymkey_model.GetDeployKeyByRepoPublicKey(ctx, repo.ID, key.ID)
deployKey, err = deploykey_model.GetDeployKeyByRepoPublicKey(ctx, repo.ID, key.ID)
if err != nil {
if asymkey_model.IsErrDeployKeyNotExist(err) {
ctx.PrivateUserErrorf(http.StatusNotFound, "Deploy key %d:%s has no %q permission for %s.", key.ID, key.Name, modeString, repoLogName)
if deploykey_model.IsErrDeployKeyNotExist(err) {
ctx.PrivateUserErrorf(http.StatusNotFound, "Deploy-key %d:%s has no %q permission for %s.", key.ID, key.Name, modeString, repoLogName)
return
}
ctx.PrivateInternalErrorf("Unable to get deploy for public (deploy) key %d for %s, error: %v", key.ID, repoLogName, err)
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
}
user = user_model.NewDeployKeyUserWithKeyID(deployKey.ID)
} else {
// Get the user represented by the Key
user, err = user_model.GetUserByID(ctx, key.OwnerID)
@@ -229,16 +215,19 @@ func ServCommand(ctx *context.PrivateContext) {
ctx.PrivateInternalErrorf("Unable to get key owner %d for public key %d:%s, error: %v", key.OwnerID, key.ID, key.Name, err)
return
}
if !user.IsActive || user.ProhibitLogin {
ctx.PrivateUserErrorf(http.StatusForbidden, "Your account is disabled.")
return
}
}
results.UserName = user.Name
if !user.KeepEmailPrivate {
results.UserEmail = user.Email
}
results.UserID = user.ID
results.UserName = user.Name
if !user.KeepEmailPrivate {
results.UserEmail = user.Email
}
if user.ExtDoerData != nil {
results.UserExtDoerData = user.ExtDoerData.EncodeToString()
}
// Don't allow pushing if the repo is archived
@@ -252,37 +241,29 @@ func ServCommand(ctx *context.PrivateContext) {
(mode > perm.AccessModeRead ||
repo.IsPrivate ||
owner.Visibility.IsPrivate() ||
(user != nil && user.IsRestricted) || // user will be nil if the key is a deploy key
user.IsRestricted ||
setting.Service.RequireSignInViewStrict) {
if key.Type == asymkey_model.KeyTypeDeploy {
if deployKey == nil || deployKey.Mode < mode {
ctx.PrivateUserErrorf(http.StatusUnauthorized, "Deploy key %d:%s has no %q permission for %s.", key.ID, key.Name, modeString, repoLogName)
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
}
// 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
}
userPerm, err := access_model.GetDoerRepoPermission(ctx, repo, user)
if err != nil {
ctx.PrivateInternalErrorf("Unable to get permissions for %-v with key %d in %-v, error: %v", user, key.ID, repo, err)
return
}
userPerm, err := access_model.GetDoerRepoPermission(ctx, repo, user)
if err != nil {
ctx.PrivateInternalErrorf("Unable to get permissions for %-v with key %d in %-v, error: %v", user, key.ID, repo, err)
return
}
userMode := userPerm.UnitAccessMode(unitType)
if userMode < mode {
ctx.PrivateUserErrorf(http.StatusUnauthorized, "User %d with key %d:%s has no %q permission for %s", key.OwnerID, key.ID, key.Name, modeString, repoLogName)
return
}
userMode := userPerm.UnitAccessMode(unitType)
if userMode < mode {
ctx.PrivateUserErrorf(http.StatusUnauthorized, "User key %d:%s has no %q permission for %s", key.ID, key.Name, modeString, repoLogName)
return
}
}
// We already know we aren't using a deploy key
if repo == nil {
if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
ctx.PrivateUserErrorf(http.StatusForbidden, "Push to create is not enabled for organizations.")
+1 -1
View File
@@ -212,7 +212,7 @@ func newWorkflowBadgeTestContext(t *testing.T) *web_context.Context {
req := httptest.NewRequest(http.MethodGet, "https://gitea.example.com/user1/repo1/actions", nil)
resp := httptest.NewRecorder()
ctx := web_context.NewWebContext(web_context.NewBaseContextForTest(resp, req), nil, nil)
ctx := web_context.NewWebContext(web_context.NewBaseContextForTest(t, resp, req), nil, nil)
ctx.Repo.Repository = &repo_model.Repository{
OwnerName: "user1",
Name: "repo1",
+1 -2
View File
@@ -163,7 +163,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
if ctx.IsBasicAuth && ctx.Data["ApiTokenScope"] == nil && !ctx.Doer.IsGiteaActions() {
if ctx.IsBasicAuth && ctx.Data["ApiTokenScope"] == nil && ctx.Doer.IsIndividual() {
_, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err == nil {
// TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
@@ -252,7 +252,6 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
var environ []string
if !isPull {
// if not "pull", then must be "push", and doer must exist
environ = repo_module.DoerPushingEnvironment(ctx.Doer, repo, isWiki)
}
+49 -14
View File
@@ -9,7 +9,9 @@ import (
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/db"
deploykey_model "gitea.dev/models/deploykey"
"gitea.dev/models/perm"
"gitea.dev/modules/htmlutil"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
asymkey_service "gitea.dev/services/asymkey"
@@ -17,23 +19,20 @@ import (
"gitea.dev/services/forms"
)
// DeployKeys render the deploy-keys list of a repository page
func DeployKeys(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") + " / " + ctx.Tr("secrets.secrets")
ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
keys, err := db.Find[asymkey_model.DeployKey](ctx, asymkey_model.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID})
keys, err := db.Find[deploykey_model.DeployKey](ctx, deploykey_model.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID})
if err != nil {
ctx.ServerError("ListDeployKeys", err)
return
}
ctx.Data["RepoDeployKeys"] = keys
ctx.HTML(http.StatusOK, tplDeployKeys)
}
// DeployKeysPost response for adding a deploy-key of a repository
func DeployKeysPost(ctx *context.Context) {
form := context.GetFetchActionForm[*forms.AddKeyForm](ctx)
if form == nil {
@@ -54,16 +53,14 @@ func DeployKeysPost(ctx *context.Context) {
}
accessMode := util.Iif(form.IsWritable, perm.AccessModeWrite, perm.AccessModeRead)
key, err := asymkey_model.AddDeployKey(ctx, ctx.Repo.Repository.ID, form.Title, content, accessMode)
key, err := deploykey_model.AddDeployKeySSH(ctx, ctx.Repo.Repository.ID, form.Title, content, accessMode)
if err != nil {
switch {
case asymkey_model.IsErrDeployKeyAlreadyExist(err):
case deploykey_model.IsErrDeployKeyAlreadyExist(err):
ctx.JSONErrorWithField(ctx.Tr("repo.settings.key_been_used"), "content")
case asymkey_model.IsErrKeyAlreadyExist(err):
ctx.JSONErrorWithField(ctx.Tr("settings.ssh_key_been_used"), "content")
case asymkey_model.IsErrKeyNameAlreadyUsed(err):
ctx.JSONErrorWithField(ctx.Tr("repo.settings.key_name_used"), "title")
case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err):
case asymkey_model.IsErrKeyNameAlreadyUsed(err), deploykey_model.IsErrDeployKeyNameAlreadyUsed(err):
ctx.JSONErrorWithField(ctx.Tr("repo.settings.key_name_used"), "title")
default:
ctx.ServerError("AddDeployKey", err)
@@ -75,12 +72,50 @@ func DeployKeysPost(ctx *context.Context) {
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/keys")
}
// DeleteDeployKey response for deleting a deploy-key
func DeleteDeployKey(ctx *context.Context) {
if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.FormInt64("id")); err != nil {
key, err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.FormInt64("id"))
if err != nil && !deploykey_model.IsErrDeployKeyNotExist(err) { // a key that is already gone leaves the caller with the state it asked for
ctx.ServerError("DeleteDeployKey", err)
} else {
ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success"))
return
}
if key != nil {
ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success", key.Name))
}
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/keys")
}
func DeployKeyGenerateToken(ctx *context.Context) {
form := context.GetFetchActionForm[*forms.AddDeployTokenForm](ctx)
if form == nil {
return
}
accessMode := util.Iif(form.IsWritable, perm.AccessModeWrite, perm.AccessModeRead)
key, err := deploykey_model.AddDeployKeyToken(ctx, ctx.Repo.Repository.ID, form.Title, accessMode)
if err != nil {
if deploykey_model.IsErrDeployKeyNameAlreadyUsed(err) {
ctx.JSONErrorWithField(ctx.Tr("repo.settings.key_name_used"), "title")
} else {
ctx.ServerError("AddDeployToken", err)
}
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.generate_deploy_token_success", htmlutil.HTMLFormat("<code>%s</code>", key.Token)))
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/keys")
}
func DeployKeyRegenerateToken(ctx *context.Context) {
key, err := deploykey_model.RegenerateDeployKeyToken(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id"))
if err != nil {
if deploykey_model.IsErrDeployKeyNotExist(err) {
ctx.JSONErrorNotFound()
} else {
ctx.ServerError("RegenerateDeployToken", err)
}
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.regenerate_deploy_token_success", htmlutil.HTMLFormat("<code>%s</code>", key.Token)))
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/keys")
}
+3 -3
View File
@@ -8,7 +8,7 @@ import (
"net/url"
"testing"
asymkey_model "gitea.dev/models/asymkey"
deploykey_model "gitea.dev/models/deploykey"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
@@ -38,7 +38,7 @@ func TestAddDeployKey(t *testing.T) {
contexttest.LoadRepo(t, ctx, 2)
DeployKeysPost(ctx)
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{Name: "read-only", Mode: perm.AccessModeRead})
unittest.AssertExistsAndLoadBean(t, &deploykey_model.DeployKey{Name: "read-only", Mode: perm.AccessModeRead})
})
t.Run("ReadWrite", func(t *testing.T) {
const testKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEHjnNEfE88W1pvBLdV3otv28x760gdmPao3lVD5uAt9\n"
@@ -47,7 +47,7 @@ func TestAddDeployKey(t *testing.T) {
contexttest.LoadRepo(t, ctx, 2)
DeployKeysPost(ctx)
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{Name: "read-write", Mode: perm.AccessModeWrite})
unittest.AssertExistsAndLoadBean(t, &deploykey_model.DeployKey{Name: "read-write", Mode: perm.AccessModeWrite})
})
}
+12 -3
View File
@@ -94,12 +94,14 @@ func optionsCorsHandler() func(next http.Handler) http.Handler {
type AuthMiddleware struct {
AllowOAuth2 types.PreMiddlewareProvider
AllowBasic types.PreMiddlewareProvider
AllowDeployToken types.PreMiddlewareProvider
MiddlewareHandler func(*context.Context)
}
func newWebAuthMiddleware() *AuthMiddleware {
type keyAllowOAuth2 struct{}
type keyAllowBasic struct{}
type keyAllowDeployToken struct{}
webAuth := &AuthMiddleware{}
middlewareSetContextValue := func(key, val any) types.PreMiddlewareProvider {
@@ -114,11 +116,13 @@ func newWebAuthMiddleware() *AuthMiddleware {
webAuth.AllowBasic = middlewareSetContextValue(keyAllowBasic{}, true)
webAuth.AllowOAuth2 = middlewareSetContextValue(keyAllowOAuth2{}, true)
webAuth.AllowDeployToken = middlewareSetContextValue(keyAllowDeployToken{}, true)
enableSSPI := setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext())
webAuth.MiddlewareHandler = func(ctx *context.Context) {
allowBasic := ctx.GetContextValue(keyAllowBasic{}) == true
allowOAuth2 := ctx.GetContextValue(keyAllowOAuth2{}) == true
allowDeployToken := ctx.GetContextValue(keyAllowDeployToken{}) == true
group := auth_service.NewGroup()
@@ -127,13 +131,16 @@ func newWebAuthMiddleware() *AuthMiddleware {
if allowOAuth2 {
group.Add(&auth_service.OAuth2{})
}
if allowDeployToken {
group.Add(&auth_service.DeployToken{}) // before Basic, which would try the token as a password
}
if allowBasic {
group.Add(&auth_service.Basic{})
}
// Sessionless means the route's auth can be done without web ui, then it doesn't need to create a session
// For example: accessing git via http, access rss feeds, downloading attachments, etc
isSessionless := allowOAuth2 || allowBasic
isSessionless := allowOAuth2 || allowBasic || allowDeployToken
if setting.Service.EnableReverseProxyAuth {
// reverse-proxy should before Session, otherwise the header will be ignored if user has login
@@ -1223,6 +1230,8 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("/keys", func() {
m.Combo("").Get(repo_setting.DeployKeys).
Post(repo_setting.DeployKeysPost)
m.Post("/generate-token", repo_setting.DeployKeyGenerateToken)
m.Post("/regenerate-token", repo_setting.DeployKeyRegenerateToken)
m.Post("/delete", repo_setting.DeleteDeployKey)
})
@@ -1743,12 +1752,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
// git lfs uses its own jwt key, and it handles the token & auth by itself, it conflicts with the general "OAuth2" auth method
// pattern: "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters
common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, webAuth.AllowBasic, repo.CorsHandler(), optSignInFromAnyOrigin)
common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, webAuth.AllowBasic, webAuth.AllowDeployToken, repo.CorsHandler(), optSignInFromAnyOrigin)
// Some users want to use "web-based git client" to access Gitea's repositories,
// so the CORS handler and OPTIONS method are used.
// pattern: "/{username}/{reponame}/{git-paths}": git http support
addOwnerRepoGitHTTPRouters(m, repo.HTTPGitEnabledHandler, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.CorsHandler(), optSignInFromAnyOrigin, context.UserAssignmentWeb())
addOwnerRepoGitHTTPRouters(m, repo.HTTPGitEnabledHandler, webAuth.AllowBasic, webAuth.AllowOAuth2, webAuth.AllowDeployToken, repo.CorsHandler(), optSignInFromAnyOrigin, context.UserAssignmentWeb())
m.Group("/notifications", func() {
m.Get("", user.Notifications)