Fix the bug: deploy key with write access can not push (#19010) (#19182)

Backport #19010

Use DeployKeyID to replace the IsDeployKey, then CanWriteCode uses the DeployKeyID to check the write permission.

Fix #19009

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
zeripath 2022-03-23 13:44:41 +00:00 committed by GitHub
parent bb0e2121a3
commit 93feb1a666
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 81 additions and 67 deletions

View File

@ -185,7 +185,7 @@ Gitea or set your environment appropriately.`, "")
reponame := os.Getenv(models.EnvRepoName) reponame := os.Getenv(models.EnvRepoName)
userID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64) userID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
prID, _ := strconv.ParseInt(os.Getenv(models.EnvPRID), 10, 64) prID, _ := strconv.ParseInt(os.Getenv(models.EnvPRID), 10, 64)
isDeployKey, _ := strconv.ParseBool(os.Getenv(models.EnvIsDeployKey)) deployKeyID, _ := strconv.ParseInt(os.Getenv(models.EnvDeployKeyID), 10, 64)
hookOptions := private.HookOptions{ hookOptions := private.HookOptions{
UserID: userID, UserID: userID,
@ -194,7 +194,7 @@ Gitea or set your environment appropriately.`, "")
GitQuarantinePath: os.Getenv(private.GitQuarantinePath), GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
GitPushOptions: pushOptions(), GitPushOptions: pushOptions(),
PullRequestID: prID, PullRequestID: prID,
IsDeployKey: isDeployKey, DeployKeyID: deployKeyID,
} }
scanner := bufio.NewScanner(os.Stdin) scanner := bufio.NewScanner(os.Stdin)

View File

@ -243,7 +243,7 @@ func runServ(c *cli.Context) error {
os.Setenv(models.EnvPusherID, strconv.FormatInt(results.UserID, 10)) os.Setenv(models.EnvPusherID, strconv.FormatInt(results.UserID, 10))
os.Setenv(models.EnvRepoID, strconv.FormatInt(results.RepoID, 10)) os.Setenv(models.EnvRepoID, strconv.FormatInt(results.RepoID, 10))
os.Setenv(models.EnvPRID, fmt.Sprintf("%d", 0)) os.Setenv(models.EnvPRID, fmt.Sprintf("%d", 0))
os.Setenv(models.EnvIsDeployKey, fmt.Sprintf("%t", results.IsDeployKey)) os.Setenv(models.EnvDeployKeyID, fmt.Sprintf("%d", results.DeployKeyID))
os.Setenv(models.EnvKeyID, fmt.Sprintf("%d", results.KeyID)) os.Setenv(models.EnvKeyID, fmt.Sprintf("%d", results.KeyID))
os.Setenv(models.EnvAppURL, setting.AppURL) os.Setenv(models.EnvAppURL, setting.AppURL)

View File

@ -47,7 +47,7 @@ func TestAPIPrivateServ(t *testing.T) {
results, err := private.ServCommand(ctx, 1, "user2", "repo1", perm.AccessModeWrite, "git-upload-pack", "") results, err := private.ServCommand(ctx, 1, "user2", "repo1", perm.AccessModeWrite, "git-upload-pack", "")
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, results.IsWiki) assert.False(t, results.IsWiki)
assert.False(t, results.IsDeployKey) assert.Zero(t, results.DeployKeyID)
assert.Equal(t, int64(1), results.KeyID) assert.Equal(t, int64(1), results.KeyID)
assert.Equal(t, "user2@localhost", results.KeyName) assert.Equal(t, "user2@localhost", results.KeyName)
assert.Equal(t, "user2", results.UserName) assert.Equal(t, "user2", results.UserName)
@ -70,7 +70,7 @@ func TestAPIPrivateServ(t *testing.T) {
results, err = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "") results, err = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "")
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, results.IsWiki) assert.False(t, results.IsWiki)
assert.False(t, results.IsDeployKey) assert.Zero(t, results.DeployKeyID)
assert.Equal(t, int64(1), results.KeyID) assert.Equal(t, int64(1), results.KeyID)
assert.Equal(t, "user2@localhost", results.KeyName) assert.Equal(t, "user2@localhost", results.KeyName)
assert.Equal(t, "user2", results.UserName) assert.Equal(t, "user2", results.UserName)
@ -92,7 +92,7 @@ func TestAPIPrivateServ(t *testing.T) {
results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "") results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "")
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, results.IsWiki) assert.False(t, results.IsWiki)
assert.True(t, results.IsDeployKey) assert.NotZero(t, results.DeployKeyID)
assert.Equal(t, deployKey.KeyID, results.KeyID) assert.Equal(t, deployKey.KeyID, results.KeyID)
assert.Equal(t, "test-deploy", results.KeyName) assert.Equal(t, "test-deploy", results.KeyName)
assert.Equal(t, "user15", results.UserName) assert.Equal(t, "user15", results.UserName)
@ -129,7 +129,7 @@ func TestAPIPrivateServ(t *testing.T) {
results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "") results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "")
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, results.IsWiki) assert.False(t, results.IsWiki)
assert.True(t, results.IsDeployKey) assert.NotZero(t, results.DeployKeyID)
assert.Equal(t, deployKey.KeyID, results.KeyID) assert.Equal(t, deployKey.KeyID, results.KeyID)
assert.Equal(t, "test-deploy", results.KeyName) assert.Equal(t, "test-deploy", results.KeyName)
assert.Equal(t, "user15", results.UserName) assert.Equal(t, "user15", results.UserName)
@ -142,7 +142,7 @@ func TestAPIPrivateServ(t *testing.T) {
results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeWrite, "git-upload-pack", "") results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeWrite, "git-upload-pack", "")
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, results.IsWiki) assert.False(t, results.IsWiki)
assert.True(t, results.IsDeployKey) assert.NotZero(t, results.DeployKeyID)
assert.Equal(t, deployKey.KeyID, results.KeyID) assert.Equal(t, deployKey.KeyID, results.KeyID)
assert.Equal(t, "test-deploy", results.KeyName) assert.Equal(t, "test-deploy", results.KeyName)
assert.Equal(t, "user15", results.UserName) assert.Equal(t, "user15", results.UserName)

View File

@ -58,7 +58,7 @@ func (key *DeployKey) GetContent() error {
return nil return nil
} }
// IsReadOnly checks if the key can only be used for read operations // IsReadOnly checks if the key can only be used for read operations, used by template
func (key *DeployKey) IsReadOnly() bool { func (key *DeployKey) IsReadOnly() bool {
return key.Mode == perm.AccessModeRead return key.Mode == perm.AccessModeRead
} }
@ -203,12 +203,6 @@ func UpdateDeployKeyCols(key *DeployKey, cols ...string) error {
return err return err
} }
// UpdateDeployKey updates deploy key information.
func UpdateDeployKey(key *DeployKey) error {
_, err := db.GetEngine(db.DefaultContext).ID(key.ID).AllCols().Update(key)
return err
}
// ListDeployKeysOptions are options for ListDeployKeys // ListDeployKeysOptions are options for ListDeployKeys
type ListDeployKeysOptions struct { type ListDeployKeysOptions struct {
db.ListOptions db.ListOptions

View File

@ -23,8 +23,8 @@ const (
EnvPusherName = "GITEA_PUSHER_NAME" EnvPusherName = "GITEA_PUSHER_NAME"
EnvPusherEmail = "GITEA_PUSHER_EMAIL" EnvPusherEmail = "GITEA_PUSHER_EMAIL"
EnvPusherID = "GITEA_PUSHER_ID" EnvPusherID = "GITEA_PUSHER_ID"
EnvKeyID = "GITEA_KEY_ID" EnvKeyID = "GITEA_KEY_ID" // public key ID
EnvIsDeployKey = "GITEA_IS_DEPLOY_KEY" EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID"
EnvPRID = "GITEA_PR_ID" EnvPRID = "GITEA_PR_ID"
EnvIsInternal = "GITEA_INTERNAL_PUSH" EnvIsInternal = "GITEA_INTERNAL_PUSH"
EnvAppURL = "GITEA_ROOT_URL" EnvAppURL = "GITEA_ROOT_URL"

View File

@ -56,7 +56,7 @@ type HookOptions struct {
GitQuarantinePath string GitQuarantinePath string
GitPushOptions GitPushOptions GitPushOptions GitPushOptions
PullRequestID int64 PullRequestID int64
IsDeployKey bool DeployKeyID int64 // if the pusher is a DeployKey, then UserID is the repo's org user.
IsWiki bool IsWiki bool
} }

View File

@ -46,9 +46,9 @@ func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey,
// ServCommandResults are the results of a call to the private route serv // ServCommandResults are the results of a call to the private route serv
type ServCommandResults struct { type ServCommandResults struct {
IsWiki bool IsWiki bool
IsDeployKey bool DeployKeyID int64
KeyID int64 KeyID int64 // public key
KeyName string KeyName string // this field is ambiguous, it can be the name of DeployKey, or the name of the PublicKey
UserName string UserName string
UserEmail string UserEmail string
UserID int64 UserID int64

View File

@ -144,7 +144,7 @@ func GetDeployKey(ctx *context.APIContext) {
// "200": // "200":
// "$ref": "#/responses/DeployKey" // "$ref": "#/responses/DeployKey"
key, err := asymkey_model.GetDeployKeyByID(db.DefaultContext, ctx.ParamsInt64(":id")) key, err := asymkey_model.GetDeployKeyByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if asymkey_model.IsErrDeployKeyNotExist(err) { if asymkey_model.IsErrDeployKeyNotExist(err) {
ctx.NotFound() ctx.NotFound()

View File

@ -12,6 +12,8 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
perm_model "code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
gitea_context "code.gitea.io/gitea/modules/context" gitea_context "code.gitea.io/gitea/modules/context"
@ -24,8 +26,12 @@ import (
type preReceiveContext struct { type preReceiveContext struct {
*gitea_context.PrivateContext *gitea_context.PrivateContext
user *user_model.User
perm models.Permission // 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 models.Permission
deployKeyAccessMode perm_model.AccessMode
canCreatePullRequest bool canCreatePullRequest bool
checkedCanCreatePullRequest bool checkedCanCreatePullRequest bool
@ -41,62 +47,52 @@ type preReceiveContext struct {
opts *private.HookOptions opts *private.HookOptions
} }
// User gets or loads User // CanWriteCode returns true if pusher can write code
func (ctx *preReceiveContext) User() *user_model.User {
if ctx.user == nil {
ctx.user, ctx.perm = loadUserAndPermission(ctx.PrivateContext, ctx.opts.UserID)
}
return ctx.user
}
// Perm gets or loads Perm
func (ctx *preReceiveContext) Perm() *models.Permission {
if ctx.user == nil {
ctx.user, ctx.perm = loadUserAndPermission(ctx.PrivateContext, ctx.opts.UserID)
}
return &ctx.perm
}
// CanWriteCode returns true if can write code
func (ctx *preReceiveContext) CanWriteCode() bool { func (ctx *preReceiveContext) CanWriteCode() bool {
if !ctx.checkedCanWriteCode { if !ctx.checkedCanWriteCode {
ctx.canWriteCode = ctx.Perm().CanWrite(unit.TypeCode) if !ctx.loadPusherAndPermission() {
return false
}
ctx.canWriteCode = ctx.userPerm.CanWrite(unit.TypeCode) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
ctx.checkedCanWriteCode = true ctx.checkedCanWriteCode = true
} }
return ctx.canWriteCode return ctx.canWriteCode
} }
// AssertCanWriteCode returns true if can write code // AssertCanWriteCode returns true if pusher can write code
func (ctx *preReceiveContext) AssertCanWriteCode() bool { func (ctx *preReceiveContext) AssertCanWriteCode() bool {
if !ctx.CanWriteCode() { if !ctx.CanWriteCode() {
if ctx.Written() { if ctx.Written() {
return false return false
} }
ctx.JSON(http.StatusForbidden, map[string]interface{}{ ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": "User permission denied.", "err": "User permission denied for writing.",
}) })
return false return false
} }
return true return true
} }
// CanCreatePullRequest returns true if can create pull requests // CanCreatePullRequest returns true if pusher can create pull requests
func (ctx *preReceiveContext) CanCreatePullRequest() bool { func (ctx *preReceiveContext) CanCreatePullRequest() bool {
if !ctx.checkedCanCreatePullRequest { if !ctx.checkedCanCreatePullRequest {
ctx.canCreatePullRequest = ctx.Perm().CanRead(unit.TypePullRequests) if !ctx.loadPusherAndPermission() {
return false
}
ctx.canCreatePullRequest = ctx.userPerm.CanRead(unit.TypePullRequests)
ctx.checkedCanCreatePullRequest = true ctx.checkedCanCreatePullRequest = true
} }
return ctx.canCreatePullRequest return ctx.canCreatePullRequest
} }
// AssertCanCreatePullRequest returns true if can create pull requests // AssertCreatePullRequest returns true if can create pull requests
func (ctx *preReceiveContext) AssertCreatePullRequest() bool { func (ctx *preReceiveContext) AssertCreatePullRequest() bool {
if !ctx.CanCreatePullRequest() { if !ctx.CanCreatePullRequest() {
if ctx.Written() { if ctx.Written() {
return false return false
} }
ctx.JSON(http.StatusForbidden, map[string]interface{}{ ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": "User permission denied.", "err": "User permission denied for creating pull-request.",
}) })
return false return false
} }
@ -246,7 +242,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
// 5. Check if the doer is allowed to push // 5. Check if the doer is allowed to push
canPush := false canPush := false
if ctx.opts.IsDeployKey { if ctx.opts.DeployKeyID != 0 {
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
} else { } else {
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx.opts.UserID) canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx.opts.UserID)
@ -303,9 +299,15 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
return 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 // 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 // Note: we can use ctx.perm and ctx.user directly as they will have been loaded above
allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.perm, ctx.user) allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.userPerm, ctx.user)
if err != nil { if err != nil {
log.Error("Error calculating if allowed to merge: %v", err) log.Error("Error calculating if allowed to merge: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{ ctx.JSON(http.StatusInternalServerError, private.Response{
@ -323,7 +325,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
} }
// If we're an admin for the repository we can ignore status checks, reviews and override protected files // If we're an admin for the repository we can ignore status checks, reviews and override protected files
if ctx.perm.IsAdmin() { if ctx.userPerm.IsAdmin() {
return return
} }
@ -450,24 +452,44 @@ func generateGitEnv(opts *private.HookOptions) (env []string) {
return env return env
} }
func loadUserAndPermission(ctx *gitea_context.PrivateContext, id int64) (user *user_model.User, perm models.Permission) { // loadPusherAndPermission returns false if an error occurs, and it writes the error response
user, err := user_model.GetUserByID(id) func (ctx *preReceiveContext) loadPusherAndPermission() bool {
if err != nil { if ctx.loadedPusher {
log.Error("Unable to get User id %d Error: %v", id, err) return true
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get User id %d Error: %v", id, err),
})
return
} }
perm, err = models.GetUserRepoPermission(ctx.Repo.Repository, user) user, err := user_model.GetUserByID(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 := models.GetUserRepoPermission(ctx.Repo.Repository, user)
if err != nil { if err != nil {
log.Error("Unable to get Repo permission of repo %s/%s of User %s", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err) log.Error("Unable to get Repo permission of repo %s/%s of User %s", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err)
ctx.JSON(http.StatusInternalServerError, private.Response{ 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), 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 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
} }
return ctx.loadedPusher = true
return true
} }

View File

@ -229,8 +229,6 @@ func ServCommand(ctx *context.PrivateContext) {
var deployKey *asymkey_model.DeployKey var deployKey *asymkey_model.DeployKey
var user *user_model.User var user *user_model.User
if key.Type == asymkey_model.KeyTypeDeploy { if key.Type == asymkey_model.KeyTypeDeploy {
results.IsDeployKey = true
var err error var err error
deployKey, err = asymkey_model.GetDeployKeyByRepo(key.ID, repo.ID) deployKey, err = asymkey_model.GetDeployKeyByRepo(key.ID, repo.ID)
if err != nil { if err != nil {
@ -248,6 +246,7 @@ func ServCommand(ctx *context.PrivateContext) {
}) })
return return
} }
results.DeployKeyID = deployKey.ID
results.KeyName = deployKey.Name results.KeyName = deployKey.Name
// FIXME: Deploy keys aren't really the owner of the repo pushing changes // FIXME: Deploy keys aren't really the owner of the repo pushing changes
@ -410,9 +409,9 @@ func ServCommand(ctx *context.PrivateContext) {
return return
} }
} }
log.Debug("Serv Results:\nIsWiki: %t\nIsDeployKey: %t\nKeyID: %d\tKeyName: %s\nUserName: %s\nUserID: %d\nOwnerName: %s\nRepoName: %s\nRepoID: %d", 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.IsWiki,
results.IsDeployKey, results.DeployKeyID,
results.KeyID, results.KeyID,
results.KeyName, results.KeyName,
results.UserName, results.UserName,

View File

@ -223,7 +223,6 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
models.EnvRepoName + "=" + reponame, models.EnvRepoName + "=" + reponame,
models.EnvPusherName + "=" + ctx.User.Name, models.EnvPusherName + "=" + ctx.User.Name,
models.EnvPusherID + fmt.Sprintf("=%d", ctx.User.ID), models.EnvPusherID + fmt.Sprintf("=%d", ctx.User.ID),
models.EnvIsDeployKey + "=false",
models.EnvAppURL + "=" + setting.AppURL, models.EnvAppURL + "=" + setting.AppURL,
} }