mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-24 03:21:49 +01:00
01c10a951b
1. A key can either be an ssh user key or a deploy key. It cannot be both. 2. If a key is a user key - it can only be associated with one user. 3. If a key is a deploy key - it can be used in multiple repositories and the permissions it has on those repositories can be different. 4. If a repository is deleted, its deploy keys must be deleted too. We currently don't enforce any of this and multiple repositories access with different permissions doesn't work at all. This PR enforces the following constraints: - [x] You should not be able to add the same user key as another user - [x] You should not be able to add a ssh user key which is being used as a deploy key - [x] You should not be able to add a ssh deploy key which is being used as a user key - [x] If you add an ssh deploy key to another repository you should be able to use it in different modes without losing the ability to use it in the other mode. - [x] If you delete a repository you must delete all its deploy keys. Fix #1357
364 lines
10 KiB
Go
364 lines
10 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// Copyright 2016 The Gitea Authors. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.gitea.io/git"
|
|
"code.gitea.io/gitea/models"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/pprof"
|
|
"code.gitea.io/gitea/modules/private"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
|
|
"github.com/Unknwon/com"
|
|
"github.com/dgrijalva/jwt-go"
|
|
version "github.com/mcuadros/go-version"
|
|
"github.com/urfave/cli"
|
|
)
|
|
|
|
const (
|
|
accessDenied = "Repository does not exist or you do not have access"
|
|
lfsAuthenticateVerb = "git-lfs-authenticate"
|
|
)
|
|
|
|
// CmdServ represents the available serv sub-command.
|
|
var CmdServ = cli.Command{
|
|
Name: "serv",
|
|
Usage: "This command should only be called by SSH shell",
|
|
Description: `Serv provide access auth for repositories`,
|
|
Action: runServ,
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "config, c",
|
|
Value: "custom/conf/app.ini",
|
|
Usage: "Custom configuration file path",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "enable-pprof",
|
|
},
|
|
},
|
|
}
|
|
|
|
func checkLFSVersion() {
|
|
if setting.LFS.StartServer {
|
|
//Disable LFS client hooks if installed for the current OS user
|
|
//Needs at least git v2.1.2
|
|
binVersion, err := git.BinVersion()
|
|
if err != nil {
|
|
fail(fmt.Sprintf("Error retrieving git version: %v", err), fmt.Sprintf("Error retrieving git version: %v", err))
|
|
}
|
|
|
|
if !version.Compare(binVersion, "2.1.2", ">=") {
|
|
setting.LFS.StartServer = false
|
|
println("LFS server support needs at least Git v2.1.2, disabled")
|
|
} else {
|
|
git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "filter.lfs.required=",
|
|
"-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=")
|
|
}
|
|
}
|
|
}
|
|
|
|
func setup(logPath string) {
|
|
log.DelLogger("console")
|
|
setting.NewContext()
|
|
checkLFSVersion()
|
|
log.NewGitLogger(filepath.Join(setting.LogRootPath, logPath))
|
|
}
|
|
|
|
func parseCmd(cmd string) (string, string) {
|
|
ss := strings.SplitN(cmd, " ", 2)
|
|
if len(ss) != 2 {
|
|
return "", ""
|
|
}
|
|
return ss[0], strings.Replace(ss[1], "'/", "'", 1)
|
|
}
|
|
|
|
var (
|
|
allowedCommands = map[string]models.AccessMode{
|
|
"git-upload-pack": models.AccessModeRead,
|
|
"git-upload-archive": models.AccessModeRead,
|
|
"git-receive-pack": models.AccessModeWrite,
|
|
lfsAuthenticateVerb: models.AccessModeNone,
|
|
}
|
|
)
|
|
|
|
func fail(userMessage, logMessage string, args ...interface{}) {
|
|
fmt.Fprintln(os.Stderr, "Gitea:", userMessage)
|
|
|
|
if len(logMessage) > 0 {
|
|
if !setting.ProdMode {
|
|
fmt.Fprintf(os.Stderr, logMessage+"\n", args...)
|
|
}
|
|
log.GitLogger.Fatal(3, logMessage, args...)
|
|
return
|
|
}
|
|
|
|
log.GitLogger.Close()
|
|
os.Exit(1)
|
|
}
|
|
|
|
func runServ(c *cli.Context) error {
|
|
if c.IsSet("config") {
|
|
setting.CustomConf = c.String("config")
|
|
}
|
|
setup("serv.log")
|
|
|
|
if setting.SSH.Disabled {
|
|
println("Gitea: SSH has been disabled")
|
|
return nil
|
|
}
|
|
|
|
if len(c.Args()) < 1 {
|
|
cli.ShowSubcommandHelp(c)
|
|
return nil
|
|
}
|
|
|
|
cmd := os.Getenv("SSH_ORIGINAL_COMMAND")
|
|
if len(cmd) == 0 {
|
|
println("Hi there, You've successfully authenticated, but Gitea does not provide shell access.")
|
|
println("If this is unexpected, please log in with password and setup Gitea under another user.")
|
|
return nil
|
|
}
|
|
|
|
verb, args := parseCmd(cmd)
|
|
|
|
var lfsVerb string
|
|
if verb == lfsAuthenticateVerb {
|
|
if !setting.LFS.StartServer {
|
|
fail("Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled")
|
|
}
|
|
|
|
argsSplit := strings.Split(args, " ")
|
|
if len(argsSplit) >= 2 {
|
|
args = strings.TrimSpace(argsSplit[0])
|
|
lfsVerb = strings.TrimSpace(argsSplit[1])
|
|
}
|
|
}
|
|
|
|
repoPath := strings.ToLower(strings.Trim(args, "'"))
|
|
rr := strings.SplitN(repoPath, "/", 2)
|
|
if len(rr) != 2 {
|
|
fail("Invalid repository path", "Invalid repository path: %v", args)
|
|
}
|
|
|
|
username := strings.ToLower(rr[0])
|
|
reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git"))
|
|
|
|
if setting.EnablePprof || c.Bool("enable-pprof") {
|
|
if err := os.MkdirAll(setting.PprofDataPath, os.ModePerm); err != nil {
|
|
fail("Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err)
|
|
}
|
|
|
|
stopCPUProfiler := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username)
|
|
defer func() {
|
|
stopCPUProfiler()
|
|
pprof.DumpMemProfileForUsername(setting.PprofDataPath, username)
|
|
}()
|
|
}
|
|
|
|
var (
|
|
isWiki bool
|
|
unitType = models.UnitTypeCode
|
|
unitName = "code"
|
|
)
|
|
if strings.HasSuffix(reponame, ".wiki") {
|
|
isWiki = true
|
|
unitType = models.UnitTypeWiki
|
|
unitName = "wiki"
|
|
reponame = reponame[:len(reponame)-5]
|
|
}
|
|
|
|
os.Setenv(models.EnvRepoUsername, username)
|
|
if isWiki {
|
|
os.Setenv(models.EnvRepoIsWiki, "true")
|
|
} else {
|
|
os.Setenv(models.EnvRepoIsWiki, "false")
|
|
}
|
|
os.Setenv(models.EnvRepoName, reponame)
|
|
|
|
repo, err := private.GetRepositoryByOwnerAndName(username, reponame)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "Failed to get repository: repository does not exist") {
|
|
fail(accessDenied, "Repository does not exist: %s/%s", username, reponame)
|
|
}
|
|
fail("Internal error", "Failed to get repository: %v", err)
|
|
}
|
|
|
|
requestedMode, has := allowedCommands[verb]
|
|
if !has {
|
|
fail("Unknown git command", "Unknown git command %s", verb)
|
|
}
|
|
|
|
if verb == lfsAuthenticateVerb {
|
|
if lfsVerb == "upload" {
|
|
requestedMode = models.AccessModeWrite
|
|
} else if lfsVerb == "download" {
|
|
requestedMode = models.AccessModeRead
|
|
} else {
|
|
fail("Unknown LFS verb", "Unknown lfs verb %s", lfsVerb)
|
|
}
|
|
}
|
|
|
|
// Prohibit push to mirror repositories.
|
|
if requestedMode > models.AccessModeRead && repo.IsMirror {
|
|
fail("mirror repository is read-only", "")
|
|
}
|
|
|
|
// Allow anonymous clone for public repositories.
|
|
var (
|
|
keyID int64
|
|
user *models.User
|
|
)
|
|
if requestedMode == models.AccessModeWrite || repo.IsPrivate || setting.Service.RequireSignInView {
|
|
keys := strings.Split(c.Args()[0], "-")
|
|
if len(keys) != 2 {
|
|
fail("Key ID format error", "Invalid key argument: %s", c.Args()[0])
|
|
}
|
|
|
|
key, err := private.GetPublicKeyByID(com.StrTo(keys[1]).MustInt64())
|
|
if err != nil {
|
|
fail("Invalid key ID", "Invalid key ID[%s]: %v", c.Args()[0], err)
|
|
}
|
|
keyID = key.ID
|
|
|
|
// Check deploy key or user key.
|
|
if key.Type == models.KeyTypeDeploy {
|
|
// Now we have to get the deploy key for this repo
|
|
deployKey, err := private.GetDeployKey(key.ID, repo.ID)
|
|
if err != nil {
|
|
fail("Key access denied", "Failed to access internal api: [key_id: %d, repo_id: %d]", key.ID, repo.ID)
|
|
}
|
|
|
|
if deployKey == nil {
|
|
fail("Key access denied", "Deploy key access denied: [key_id: %d, repo_id: %d]", key.ID, repo.ID)
|
|
}
|
|
|
|
if deployKey.Mode < requestedMode {
|
|
fail("Key permission denied", "Cannot push with read-only deployment key: %d to repo_id: %d", key.ID, repo.ID)
|
|
}
|
|
|
|
// Update deploy key activity.
|
|
if err = private.UpdateDeployKeyUpdated(key.ID, repo.ID); err != nil {
|
|
fail("Internal error", "UpdateDeployKey: %v", err)
|
|
}
|
|
|
|
// 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
|
|
os.Setenv(models.EnvPusherName, username)
|
|
os.Setenv(models.EnvPusherID, fmt.Sprintf("%d", repo.OwnerID))
|
|
} else {
|
|
user, err = private.GetUserByKeyID(key.ID)
|
|
if err != nil {
|
|
fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err)
|
|
}
|
|
|
|
if !user.IsActive || user.ProhibitLogin {
|
|
fail("Your account is not active or has been disabled by Administrator",
|
|
"User %s is disabled and have no access to repository %s",
|
|
user.Name, repoPath)
|
|
}
|
|
|
|
mode, err := private.CheckUnitUser(user.ID, repo.ID, user.IsAdmin, unitType)
|
|
if err != nil {
|
|
fail("Internal error", "Failed to check access: %v", err)
|
|
} else if *mode < requestedMode {
|
|
clientMessage := accessDenied
|
|
if *mode >= models.AccessModeRead {
|
|
clientMessage = "You do not have sufficient authorization for this action"
|
|
}
|
|
fail(clientMessage,
|
|
"User %s does not have level %v access to repository %s's "+unitName,
|
|
user.Name, requestedMode, repoPath)
|
|
}
|
|
|
|
os.Setenv(models.EnvPusherName, user.Name)
|
|
os.Setenv(models.EnvPusherID, fmt.Sprintf("%d", user.ID))
|
|
}
|
|
}
|
|
|
|
//LFS token authentication
|
|
if verb == lfsAuthenticateVerb {
|
|
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, username, repo.Name)
|
|
|
|
now := time.Now()
|
|
claims := jwt.MapClaims{
|
|
"repo": repo.ID,
|
|
"op": lfsVerb,
|
|
"exp": now.Add(setting.LFS.HTTPAuthExpiry).Unix(),
|
|
"nbf": now.Unix(),
|
|
}
|
|
if user != nil {
|
|
claims["user"] = user.ID
|
|
}
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
|
|
// Sign and get the complete encoded token as a string using the secret
|
|
tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
|
|
if err != nil {
|
|
fail("Internal error", "Failed to sign JWT token: %v", err)
|
|
}
|
|
|
|
tokenAuthentication := &models.LFSTokenResponse{
|
|
Header: make(map[string]string),
|
|
Href: url,
|
|
}
|
|
tokenAuthentication.Header["Authorization"] = fmt.Sprintf("Bearer %s", tokenString)
|
|
|
|
enc := json.NewEncoder(os.Stdout)
|
|
err = enc.Encode(tokenAuthentication)
|
|
if err != nil {
|
|
fail("Internal error", "Failed to encode LFS json response: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Special handle for Windows.
|
|
if setting.IsWindows {
|
|
verb = strings.Replace(verb, "-", " ", 1)
|
|
}
|
|
|
|
var gitcmd *exec.Cmd
|
|
verbs := strings.Split(verb, " ")
|
|
if len(verbs) == 2 {
|
|
gitcmd = exec.Command(verbs[0], verbs[1], repoPath)
|
|
} else {
|
|
gitcmd = exec.Command(verb, repoPath)
|
|
}
|
|
if isWiki {
|
|
if err = private.InitWiki(repo.ID); err != nil {
|
|
fail("Internal error", "Failed to init wiki repo: %v", err)
|
|
}
|
|
}
|
|
|
|
os.Setenv(models.ProtectedBranchRepoID, fmt.Sprintf("%d", repo.ID))
|
|
|
|
gitcmd.Dir = setting.RepoRootPath
|
|
gitcmd.Stdout = os.Stdout
|
|
gitcmd.Stdin = os.Stdin
|
|
gitcmd.Stderr = os.Stderr
|
|
if err = gitcmd.Run(); err != nil {
|
|
fail("Internal error", "Failed to execute git command: %v", err)
|
|
}
|
|
|
|
// Update user key activity.
|
|
if keyID > 0 {
|
|
if err = private.UpdatePublicKeyUpdated(keyID); err != nil {
|
|
fail("Internal error", "UpdatePublicKey: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|