Skip to content
8 changes: 7 additions & 1 deletion config.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,13 @@ properties:
forwarded_env_vars:
type: array
items: { type: string }
description: Host environment variables forwarded into task runs.
description: >-
Host environment variables copied into task runs and into the child git
processes that clone and update repositories. Proxy variables are not
forwarded implicitly: they reach a task only if they are named here or set
in env_vars. A bare-metal (systemd) installation behind a corporate proxy
therefore has to list the proxy variables explicitly, including the bypass
list for internal hosts: ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"].

# ==========================================================================
# Nested subsystems
Expand Down
109 changes: 84 additions & 25 deletions db/Repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package db
import (
"crypto/sha1"
"fmt"
"net/url"
"path"
"regexp"
"strconv"
Expand All @@ -11,6 +12,8 @@ import (
"github.com/semaphoreui/semaphore/pkg/common_errors"
"github.com/semaphoreui/semaphore/pkg/git"
"github.com/semaphoreui/semaphore/util"

log "github.com/sirupsen/logrus"
)

type RepositoryType string
Expand Down Expand Up @@ -100,50 +103,106 @@ func (r Repository) GetInternalPath(templateID int) string {
// directory (e.g. repository_15_template_114_main_1a2b3c4d).
func (r Repository) GetFullPath(templateID int) string {
if r.GetType() == RepositoryLocal {
return r.GetGitURL(true)
return r.GetGitURL(false)
}
return path.Join(util.Config.GetProjectTmpDir(r.ProjectID), r.GetCheckoutDirName(templateID))
}

func (r Repository) GetGitURL(secure bool) string {
url := r.GitURL
// GetGitURL returns the URL git is invoked with. With embedCredentials set,
// the repository's login/password access key is embedded in the userinfo,
// percent encoded, so credentials containing "@", ":", "#" or "%" survive
// intact. Otherwise the URL is returned exactly as configured. It may still
// carry userinfo typed into the URL itself, which go-git uses as basic auth,
// so it is not safe to log in either mode. Use GetRedactedGitURL for that.
//
// A URL net/url cannot parse is returned with its userinfo removed in both
// modes. git cannot authenticate with it anyway, and go-git quotes the whole
// URL in the parse error it returns, so handing it over unchanged would leak
// the credentials into task logs.
func (r Repository) GetGitURL(embedCredentials bool) string {
rawURL := r.GitURL

if r.GetType() == RepositoryLocal {
return util.NormalizeLocalFilesystemPath(url)
return util.NormalizeLocalFilesystemPath(rawURL)
}

if !hasURLScheme(rawURL) {
return rawURL
Comment thread
Copilot marked this conversation as resolved.
}

if secure {
return url
parsed, err := url.Parse(rawURL)
if err != nil {
// Do not fail the task here, but make the reason visible: a URL git
// cannot be handed credentials for shows up later only as an opaque
// authentication error. The error itself quotes the URL and is not logged.
log.WithFields(log.Fields{
"context": "repository",
"repository_id": r.ID,
}).Warn("can not parse repository url, using it without credentials")
return redactURLUserinfo(rawURL)
}

if r.GetType() == RepositoryHTTP {
auth := ""
switch r.SSHKey.Type {
case AccessKeyLoginPassword:
if r.SSHKey.LoginPassword.Login == "" {
auth = r.SSHKey.LoginPassword.Password
} else {
auth = r.SSHKey.LoginPassword.Login + ":" + r.SSHKey.LoginPassword.Password
if !embedCredentials {
return rawURL
}

if r.GetType() == RepositoryHTTP && r.SSHKey.Type == AccessKeyLoginPassword {
if r.SSHKey.LoginPassword.Login == "" {
if r.SSHKey.LoginPassword.Password == "" {
return rawURL
}
parsed.User = url.User(r.SSHKey.LoginPassword.Password)
} else {
parsed.User = url.UserPassword(r.SSHKey.LoginPassword.Login, r.SSHKey.LoginPassword.Password)
}
if auth != "" {
auth += "@"

// Credentials are still embedded for plain http so existing installations
// keep working, but the transport is unencrypted and the credentials are
// sent in the clear.
if strings.EqualFold(parsed.Scheme, "http") {
log.WithFields(log.Fields{
"context": "repository",
"repository_id": r.ID,
}).Warn("sending git credentials over an unencrypted http connection, use https instead")
}

re := regexp.MustCompile(`^(https?)://`)
m := re.FindStringSubmatch(url)
var protocol string
return parsed.String()
}

if m == nil {
panic(fmt.Errorf("invalid git url: %s", url))
}
return rawURL
}

protocol = m[1]
// GetRedactedGitURL returns the repository URL with any userinfo removed, for
// writing to task logs. Unlike GetGitURL(false) it never returns credentials,
// including ones typed into the URL itself.
func (r Repository) GetRedactedGitURL() string {
if r.GetType() == RepositoryLocal {
return util.NormalizeLocalFilesystemPath(r.GitURL)
}
return redactURLUserinfo(r.GitURL)
}

url = protocol + "://" + auth + r.GitURL[len(protocol)+3:]
// redactURLUserinfo drops everything between "://" and the last "@" of a URL.
//
// net/url is deliberately not used: credentials are typed in by hand and are
// often not valid URL syntax. A token containing "/" or "#" makes net/url read
// it as the host or the fragment and report no userinfo at all, so a parser
// based redaction would log it verbatim. Cutting at the last "@" can over-trim
// a URL that has an "@" after the host, which only affects how it is displayed.
// scp-style SSH addresses ("git@host:path") have no scheme, carry no secret and
// are returned unchanged.
func redactURLUserinfo(rawURL string) string {
schemeEnd := strings.Index(rawURL, "://")
if schemeEnd < 0 {
return rawURL
}
rest := rawURL[schemeEnd+3:]

return url
at := strings.LastIndex(rest, "@")
if at < 0 {
return rawURL
}
return rawURL[:schemeEnd+3] + rest[at+1:]
}

func (r Repository) GetType() RepositoryType {
Expand Down
Loading
Loading