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

Use DeployKeyID to replace the IsDeployKey, then CanWriteCode uses the DeployKeyID to check the write permission.
pull/19163/head^2
wxiaoguang 3 years ago committed by GitHub
parent 80fd25524e
commit 2b55422cd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      cmd/hook.go
  2. 2
      cmd/serv.go
  3. 10
      integrations/api_private_serv_test.go
  4. 8
      models/asymkey/ssh_key_deploy.go
  5. 4
      models/helper_environment.go
  6. 2
      modules/private/hook.go
  7. 6
      modules/private/serv.go
  8. 2
      routers/api/v1/repo/key.go
  9. 96
      routers/private/hook_pre_receive.go
  10. 7
      routers/private/serv.go
  11. 1
      routers/web/repo/http.go

@ -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)

@ -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)

@ -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)

@ -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

@ -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"

@ -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
} }

@ -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

@ -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()

@ -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 ctx.loadedPusher {
return true
}
user, err := user_model.GetUserByID(ctx.opts.UserID)
if err != nil { if err != nil {
log.Error("Unable to get User id %d Error: %v", id, err) log.Error("Unable to get User id %d Error: %v", ctx.opts.UserID, err)
ctx.JSON(http.StatusInternalServerError, private.Response{ ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get User id %d Error: %v", id, err), Err: fmt.Sprintf("Unable to get User id %d Error: %v", ctx.opts.UserID, err),
}) })
return return false
} }
ctx.user = user
perm, err = models.GetUserRepoPermission(ctx.Repo.Repository, 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
} }

@ -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,

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

Loading…
Cancel
Save