git: refactor AuthStrategy into AuthOptions

This commit moves the previous `AuthStrategy` wiring to a more generic
`AuthOptions`, breaking free from implementation specific details in
the `git` package.

Signed-off-by: Hidde Beydals <hello@hidde.co>
This commit is contained in:
Hidde Beydals 2021-10-23 12:43:55 +02:00 committed by Sunny
parent 46a5b9c27d
commit 0cf0d4e756
13 changed files with 856 additions and 539 deletions

View File

@ -229,34 +229,23 @@ func (r *GitRepositoryReconciler) reconcile(ctx context.Context, repository sour
}
defer os.RemoveAll(tmpGit)
// determine auth method
auth := &git.Auth{}
// Configure auth options using secret
authOpts := &git.AuthOptions{}
if repository.Spec.SecretRef != nil {
authStrategy, err := strategy.AuthSecretStrategyForURL(
repository.Spec.URL,
git.CheckoutOptions{
GitImplementation: repository.Spec.GitImplementation,
RecurseSubmodules: repository.Spec.RecurseSubmodules,
})
if err != nil {
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
}
name := types.NamespacedName{
Namespace: repository.GetNamespace(),
Name: repository.Spec.SecretRef.Name,
}
var secret corev1.Secret
err = r.Client.Get(ctx, name, &secret)
secret := &corev1.Secret{}
err = r.Client.Get(ctx, name, secret)
if err != nil {
err = fmt.Errorf("auth secret error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
}
auth, err = authStrategy.Method(secret)
authOpts, err = git.AuthOptionsFromSecret(repository.Spec.URL, secret)
if err != nil {
err = fmt.Errorf("auth error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
}
}
@ -275,7 +264,7 @@ func (r *GitRepositoryReconciler) reconcile(ctx context.Context, repository sour
gitCtx, cancel := context.WithTimeout(ctx, repository.Spec.Timeout.Duration)
defer cancel()
commit, revision, err := checkoutStrategy.Checkout(gitCtx, tmpGit, repository.Spec.URL, auth)
commit, revision, err := checkoutStrategy.Checkout(gitCtx, tmpGit, repository.Spec.URL, authOpts)
if err != nil {
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}

View File

@ -19,41 +19,14 @@ package git
import (
"context"
"github.com/go-git/go-git/v5/plumbing/transport"
git2go "github.com/libgit2/git2go/v31"
corev1 "k8s.io/api/core/v1"
)
const (
DefaultOrigin = "origin"
DefaultBranch = "master"
DefaultPublicKeyAuthUser = "git"
CAFile = "caFile"
)
type Commit interface {
Verify(secret corev1.Secret) error
Hash() string
}
type CheckoutStrategy interface {
Checkout(ctx context.Context, path, url string, auth *Auth) (Commit, string, error)
}
type CheckoutOptions struct {
GitImplementation string
RecurseSubmodules bool
}
// TODO(hidde): candidate for refactoring, so that we do not directly
// depend on implementation specifics here.
type Auth struct {
AuthMethod transport.AuthMethod
CABundle []byte
CredCallback git2go.CredentialsCallback
CertCallback git2go.CertificateCheckCallback
}
type AuthSecretStrategy interface {
Method(secret corev1.Secret) (*Auth, error)
Checkout(ctx context.Context, path, url string, config *AuthOptions) (Commit, string, error)
}

View File

@ -23,11 +23,10 @@ import (
"time"
"github.com/Masterminds/semver/v3"
extgogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/fluxcd/pkg/gitutil"
"github.com/fluxcd/pkg/version"
extgogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/source-controller/pkg/git"
@ -59,10 +58,14 @@ type CheckoutBranch struct {
recurseSubmodules bool
}
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (git.Commit, string, error) {
authMethod, err := transportAuth(opts)
if err != nil {
return nil, "", fmt.Errorf("could not construct auth method: %w", err)
}
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
URL: url,
Auth: auth.AuthMethod,
Auth: authMethod,
RemoteName: git.DefaultOrigin,
ReferenceName: plumbing.NewBranchReferenceName(c.branch),
SingleBranch: true,
@ -71,7 +74,7 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, auth *g
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
Progress: nil,
Tags: extgogit.NoTags,
CABundle: auth.CABundle,
CABundle: opts.CAFile,
})
if err != nil {
return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, gitutil.GoGitError(err))
@ -92,10 +95,14 @@ type CheckoutTag struct {
recurseSubmodules bool
}
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (git.Commit, string, error) {
authMethod, err := transportAuth(opts)
if err != nil {
return nil, "", fmt.Errorf("could not construct auth method: %w", err)
}
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
URL: url,
Auth: auth.AuthMethod,
Auth: authMethod,
RemoteName: git.DefaultOrigin,
ReferenceName: plumbing.NewTagReferenceName(c.tag),
SingleBranch: true,
@ -104,7 +111,7 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, auth *git.
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
Progress: nil,
Tags: extgogit.NoTags,
CABundle: auth.CABundle,
CABundle: opts.CAFile,
})
if err != nil {
return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err)
@ -126,10 +133,14 @@ type CheckoutCommit struct {
recurseSubmodules bool
}
func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (git.Commit, string, error) {
authMethod, err := transportAuth(opts)
if err != nil {
return nil, "", fmt.Errorf("could not construct transportAuth method: %w", err)
}
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
URL: url,
Auth: auth.AuthMethod,
Auth: authMethod,
RemoteName: git.DefaultOrigin,
ReferenceName: plumbing.NewBranchReferenceName(c.branch),
SingleBranch: true,
@ -137,7 +148,7 @@ func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, auth *g
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
Progress: nil,
Tags: extgogit.NoTags,
CABundle: auth.CABundle,
CABundle: opts.CAFile,
})
if err != nil {
return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err)
@ -165,22 +176,25 @@ type CheckoutSemVer struct {
recurseSubmodules bool
}
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (git.Commit, string, error) {
verConstraint, err := semver.NewConstraint(c.semVer)
if err != nil {
return nil, "", fmt.Errorf("semver parse range error: %w", err)
}
authMethod, err := transportAuth(opts)
if err != nil {
return nil, "", fmt.Errorf("could not construct transportAuth method: %w", err)
}
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
URL: url,
Auth: auth.AuthMethod,
Auth: authMethod,
RemoteName: git.DefaultOrigin,
NoCheckout: false,
Depth: 1,
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
Progress: nil,
Tags: extgogit.AllTags,
CABundle: auth.CABundle,
CABundle: opts.CAFile,
})
if err != nil {
return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err)

View File

@ -25,7 +25,7 @@ import (
)
func TestCheckoutTagSemVer_Checkout(t *testing.T) {
auth := &git.Auth{}
auth := &git.AuthOptions{}
tag := CheckoutTag{
tag: "v1.7.0",
}

View File

@ -17,88 +17,39 @@ limitations under the License.
package gogit
import (
"fmt"
"net/url"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
corev1 "k8s.io/api/core/v1"
"github.com/fluxcd/pkg/ssh/knownhosts"
"github.com/fluxcd/source-controller/pkg/git"
)
func AuthSecretStrategyForURL(URL string) (git.AuthSecretStrategy, error) {
u, err := url.Parse(URL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL to determine auth strategy: %w", err)
}
switch {
case u.Scheme == "http", u.Scheme == "https":
return &BasicAuth{}, nil
case u.Scheme == "ssh":
return &PublicKeyAuth{user: u.User.Username()}, nil
default:
return nil, fmt.Errorf("no auth secret strategy for scheme %s", u.Scheme)
// transportAuth constructs the transport.AuthMethod for the git.Transport of
// the given git.AuthOptions. It returns the result, or an error.
func transportAuth(opts *git.AuthOptions) (transport.AuthMethod, error) {
switch opts.Transport {
case git.HTTPS, git.HTTP:
return &http.BasicAuth{
Username: opts.Username,
Password: opts.Password,
}, nil
case git.SSH:
if len(opts.Identity) > 0 {
pk, err := ssh.NewPublicKeys(opts.Username, opts.Identity, opts.Password)
if err != nil {
return nil, err
}
if len(opts.KnownHosts) > 0 {
callback, err := knownhosts.New(opts.KnownHosts)
if err != nil {
return nil, err
}
pk.HostKeyCallback = callback
}
return pk, nil
}
}
}
type BasicAuth struct{}
func (s *BasicAuth) Method(secret corev1.Secret) (*git.Auth, error) {
auth := &git.Auth{}
basicAuth := &http.BasicAuth{}
if caBundle, ok := secret.Data[git.CAFile]; ok {
auth.CABundle = caBundle
}
if username, ok := secret.Data["username"]; ok {
basicAuth.Username = string(username)
}
if password, ok := secret.Data["password"]; ok {
basicAuth.Password = string(password)
}
if (basicAuth.Username == "" && basicAuth.Password != "") || (basicAuth.Username != "" && basicAuth.Password == "") {
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
}
if basicAuth.Username != "" && basicAuth.Password != "" {
auth.AuthMethod = basicAuth
}
return auth, nil
}
type PublicKeyAuth struct {
user string
}
func (s *PublicKeyAuth) Method(secret corev1.Secret) (*git.Auth, error) {
if _, ok := secret.Data[git.CAFile]; ok {
return nil, fmt.Errorf("found caFile key in secret '%s' but go-git SSH transport does not support custom certificates", secret.Name)
}
identity := secret.Data["identity"]
knownHosts := secret.Data["known_hosts"]
if len(identity) == 0 || len(knownHosts) == 0 {
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'identity' and 'known_hosts'", secret.Name)
}
user := s.user
if user == "" {
user = git.DefaultPublicKeyAuthUser
}
password := secret.Data["password"]
pk, err := ssh.NewPublicKeys(user, identity, string(password))
if err != nil {
return nil, err
}
callback, err := knownhosts.New(knownHosts)
if err != nil {
return nil, err
}
pk.HostKeyCallback = callback
return &git.Auth{AuthMethod: pk}, nil
return nil, nil
}

View File

@ -17,19 +17,21 @@ limitations under the License.
package gogit
import (
"reflect"
"errors"
"testing"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
corev1 "k8s.io/api/core/v1"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
. "github.com/onsi/gomega"
"github.com/fluxcd/source-controller/pkg/git"
)
const (
// secretKeyFixture is a randomly generated password less
// privateKeyFixture is a randomly generated password less
// 512bit RSA private key.
secretKeyFixture string = `-----BEGIN RSA PRIVATE KEY-----
privateKeyFixture = `-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCrakELAKxozvwJijQEggYlTvS1QTZx1DaBwOhW/4kRSuR21plu
xuQeyuUiztoWeb9jgW7wjzG4j1PIJjdbsgjPIcIZ4PBY7JeEW+QRopfwuN8MHXNp
uTLgIHbkmhoOg5qBEcjzO/lEOOPpV0EmbObgqv3+wRmLJrgfzWl/cTtRewIDAQAB
@ -45,9 +47,9 @@ Ngkgu4mLjc3RfenEhJECQAx8zjWUE6kHHPGAd9DfiAIQ4bChqnyS0Nwb9+Gd4hSE
P0Ah10mHiK/M0o3T8Eanwum0gbQHPnOwqZgsPkwXRqQ=
-----END RSA PRIVATE KEY-----`
// secretKeyFixture is a randomly generated
// privateKeyPassphraseFixture is a randomly generated
// 512bit RSA private key with password foobar.
secretPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
privateKeyPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,0B016973B2A761D31E6B388D0F327C35
@ -60,137 +62,133 @@ wGctSx4kHsZGhJv5qwKqqPEFPhUzph8D2tm2TABk8HJa5KJFDbGrcfvk2uODAoZr
MbcpIxCfl8oB09bWfY6tDQjyvwSYYo2Phdwm7kT92xc=
-----END RSA PRIVATE KEY-----`
// generated with sshkey-gen with password `password`. Fails test
secretEDCSAFicture = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCUNUDYpS
GJ0GjHSoOJvNzrAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIAUwMlCdqwINTCFe
0QTLK2w04AMyMDkH4keEHnTDB9KAAAAAoLv9vPS65ie3CQ9XYDXhX4TQUKg15kYmbt/Lqu
Eg5i6G2aJOIeq/ZwBOjySG328zucwptzScx1bgwIHfkPmUSBBoATcilGtglVFDmBuYSrky
r2bP9MJYmUIx3RkMZI0RcYIwuH/fMNPnyBbGMCwEEZP3xYXst8oNyGz47s9k6Woqy64bgh
Q0YEW1Vyqn/Tt8nBJrbtyY1iLnQjOZ167bYxc=
-----END OPENSSH PRIVATE KEY-----`
// knownHostsFixture is known_hosts fixture in the expected
// format.
knownHostsFixture string = `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`
)
var (
basicAuthSecretFixture = corev1.Secret{
Data: map[string][]byte{
"username": []byte("git"),
"password": []byte("password"),
},
}
privateKeySecretFixture = corev1.Secret{
Data: map[string][]byte{
"identity": []byte(secretKeyFixture),
"known_hosts": []byte(knownHostsFixture),
},
}
privateKeySecretWithPassphraseFixture = corev1.Secret{
Data: map[string][]byte{
"identity": []byte(secretPassphraseFixture),
"known_hosts": []byte(knownHostsFixture),
"password": []byte("foobar"),
},
}
failingPrivateKey = corev1.Secret{
Data: map[string][]byte{
"identity": []byte(secretEDCSAFicture),
"known_hosts": []byte(knownHostsFixture),
"password": []byte("password"),
},
}
)
func TestAuthSecretStrategyForURL(t *testing.T) {
func Test_transportAuth(t *testing.T) {
tests := []struct {
name string
url string
want git.AuthSecretStrategy
wantErr bool
name string
opts *git.AuthOptions
wantFunc func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions)
wantErr error
}{
{"HTTP", "http://git.example.com/org/repo.git", &BasicAuth{}, false},
{"HTTPS", "https://git.example.com/org/repo.git", &BasicAuth{}, false},
{"SSH", "ssh://git.example.com:2222/org/repo.git", &PublicKeyAuth{}, false},
{"SSH with username", "ssh://example@git.example.com:2222/org/repo.git", &PublicKeyAuth{user: "example"}, false},
{"unsupported", "protocol://example.com", nil, true},
{
name: "HTTP basic auth",
opts: &git.AuthOptions{
Transport: git.HTTP,
Username: "example",
Password: "password",
},
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
g.Expect(t).To(Equal(&http.BasicAuth{
Username: opts.Username,
Password: opts.Password,
}))
},
},
{
name: "HTTPS basic auth",
opts: &git.AuthOptions{
Transport: git.HTTPS,
Username: "example",
Password: "password",
},
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
g.Expect(t).To(Equal(&http.BasicAuth{
Username: opts.Username,
Password: opts.Password,
}))
},
},
{
name: "SSH private key",
opts: &git.AuthOptions{
Transport: git.SSH,
Username: "example",
Identity: []byte(privateKeyFixture),
},
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
tt, ok := t.(*ssh.PublicKeys)
g.Expect(ok).To(BeTrue())
g.Expect(tt.User).To(Equal(opts.Username))
g.Expect(tt.Signer.PublicKey().Type()).To(Equal("ssh-rsa"))
},
},
{
name: "SSH private key with passphrase",
opts: &git.AuthOptions{
Transport: git.SSH,
Username: "example",
Password: "foobar",
Identity: []byte(privateKeyPassphraseFixture),
},
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
tt, ok := t.(*ssh.PublicKeys)
g.Expect(ok).To(BeTrue())
g.Expect(tt.User).To(Equal(opts.Username))
g.Expect(tt.Signer.PublicKey().Type()).To(Equal("ssh-rsa"))
},
},
{
name: "SSH private key with invalid passphrase",
opts: &git.AuthOptions{
Transport: git.SSH,
Username: "example",
Password: "",
Identity: []byte(privateKeyPassphraseFixture),
},
wantErr: errors.New("x509: decryption password incorrect"),
},
{
name: "SSH private key with known_hosts",
opts: &git.AuthOptions{
Transport: git.SSH,
Username: "example",
Identity: []byte(privateKeyFixture),
KnownHosts: []byte(knownHostsFixture),
},
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
tt, ok := t.(*ssh.PublicKeys)
g.Expect(ok).To(BeTrue())
g.Expect(tt.User).To(Equal(opts.Username))
g.Expect(tt.Signer.PublicKey().Type()).To(Equal("ssh-rsa"))
g.Expect(tt.HostKeyCallback).ToNot(BeNil())
},
},
{
name: "SSH private key with invalid known_hosts",
opts: &git.AuthOptions{
Transport: git.SSH,
Username: "example",
Identity: []byte(privateKeyFixture),
KnownHosts: []byte("invalid"),
},
wantErr: errors.New("knownhosts: knownhosts: missing host pattern"),
},
{
name: "Empty",
opts: &git.AuthOptions{},
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
g.Expect(t).To(BeNil())
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := AuthSecretStrategyForURL(tt.url)
if (err != nil) != tt.wantErr {
t.Errorf("AuthSecretStrategyForURL() error = %v, wantErr %v", err, tt.wantErr)
g := NewWithT(t)
got, err := transportAuth(tt.opts)
if tt.wantErr != nil {
g.Expect(err).To(Equal(tt.wantErr))
g.Expect(got).To(BeNil())
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AuthSecretStrategyForURL() got = %v, want %v", got, tt.want)
}
})
}
}
func TestBasicAuthStrategy_Method(t *testing.T) {
tests := []struct {
name string
secret corev1.Secret
modify func(secret *corev1.Secret)
want *git.Auth
wantErr bool
}{
{"username and password", basicAuthSecretFixture, nil, &git.Auth{AuthMethod: &http.BasicAuth{Username: "git", Password: "password"}}, false},
{"without username", basicAuthSecretFixture, func(s *corev1.Secret) { delete(s.Data, "username") }, nil, true},
{"without password", basicAuthSecretFixture, func(s *corev1.Secret) { delete(s.Data, "password") }, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := tt.secret.DeepCopy()
if tt.modify != nil {
tt.modify(secret)
}
s := &BasicAuth{}
got, err := s.Method(*secret)
if (err != nil) != tt.wantErr {
t.Errorf("Method() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Method() got = %v, want %v", got, tt.want)
}
})
}
}
func TestPublicKeyStrategy_Method(t *testing.T) {
tests := []struct {
name string
secret corev1.Secret
modify func(secret *corev1.Secret)
wantErr bool
}{
{"private key and known_hosts", privateKeySecretFixture, nil, false},
{"private key with passphrase and known_hosts", privateKeySecretWithPassphraseFixture, nil, false},
{"edcsa private key with passphrase and known_hosts", failingPrivateKey, nil, false},
{"missing private key", privateKeySecretFixture, func(s *corev1.Secret) { delete(s.Data, "identity") }, true},
{"invalid private key", privateKeySecretFixture, func(s *corev1.Secret) { s.Data["identity"] = []byte(`-----BEGIN RSA PRIVATE KEY-----`) }, true},
{"missing known_hosts", privateKeySecretFixture, func(s *corev1.Secret) { delete(s.Data, "known_hosts") }, true},
{"invalid known_hosts", privateKeySecretFixture, func(s *corev1.Secret) { s.Data["known_hosts"] = []byte(`invalid`) }, true},
{"missing password", privateKeySecretWithPassphraseFixture, func(s *corev1.Secret) { delete(s.Data, "password") }, true},
{"wrong password", privateKeySecretWithPassphraseFixture, func(s *corev1.Secret) { s.Data["password"] = []byte("pass") }, true},
{"empty", corev1.Secret{}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := tt.secret.DeepCopy()
if tt.modify != nil {
tt.modify(secret)
}
s := &PublicKeyAuth{}
_, err := s.Method(*secret)
if (err != nil) != tt.wantErr {
t.Errorf("Method() error = %v, wantErr %v", err, tt.wantErr)
return
g.Expect(err).ToNot(HaveOccurred())
if tt.wantFunc != nil {
tt.wantFunc(g, got, tt.opts)
}
})
}

View File

@ -58,14 +58,11 @@ type CheckoutBranch struct {
branch string
}
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (git.Commit, string, error) {
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
FetchOptions: &git2go.FetchOptions{
DownloadTags: git2go.DownloadTagsNone,
RemoteCallbacks: git2go.RemoteCallbacks{
CredentialsCallback: auth.CredCallback,
CertificateCheckCallback: auth.CertCallback,
},
DownloadTags: git2go.DownloadTagsNone,
RemoteCallbacks: remoteCallbacks(opts),
},
CheckoutBranch: c.branch,
})
@ -88,14 +85,11 @@ type CheckoutTag struct {
tag string
}
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (git.Commit, string, error) {
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
FetchOptions: &git2go.FetchOptions{
DownloadTags: git2go.DownloadTagsAll,
RemoteCallbacks: git2go.RemoteCallbacks{
CredentialsCallback: auth.CredCallback,
CertificateCheckCallback: auth.CertCallback,
},
DownloadTags: git2go.DownloadTagsAll,
RemoteCallbacks: remoteCallbacks(opts),
},
})
if err != nil {
@ -113,14 +107,11 @@ type CheckoutCommit struct {
commit string
}
func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (git.Commit, string, error) {
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
FetchOptions: &git2go.FetchOptions{
DownloadTags: git2go.DownloadTagsNone,
RemoteCallbacks: git2go.RemoteCallbacks{
CredentialsCallback: auth.CredCallback,
CertificateCheckCallback: auth.CertCallback,
},
DownloadTags: git2go.DownloadTagsNone,
RemoteCallbacks: remoteCallbacks(opts),
},
})
if err != nil {
@ -142,7 +133,7 @@ type CheckoutSemVer struct {
semVer string
}
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (git.Commit, string, error) {
verConstraint, err := semver.NewConstraint(c.semVer)
if err != nil {
return nil, "", fmt.Errorf("semver parse range error: %w", err)
@ -150,11 +141,8 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
FetchOptions: &git2go.FetchOptions{
DownloadTags: git2go.DownloadTagsAll,
RemoteCallbacks: git2go.RemoteCallbacks{
CredentialsCallback: auth.CredCallback,
CertificateCheckCallback: auth.CertCallback,
},
DownloadTags: git2go.DownloadTagsAll,
RemoteCallbacks: remoteCallbacks(opts),
},
})
if err != nil {

View File

@ -27,8 +27,6 @@ import (
git2go "github.com/libgit2/git2go/v31"
. "github.com/onsi/gomega"
"github.com/fluxcd/source-controller/pkg/git"
)
func TestCheckoutBranch_Checkout(t *testing.T) {
@ -84,7 +82,7 @@ func TestCheckoutBranch_Checkout(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
_, ref, err := branch.Checkout(context.TODO(), tmpDir, repo.Path(), &git.Auth{})
_, ref, err := branch.Checkout(context.TODO(), tmpDir, repo.Path(), nil)
if tt.expectedErr != "" {
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
g.Expect(ref).To(BeEmpty())
@ -154,7 +152,7 @@ func TestCheckoutTag_Checkout(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
_, ref, err := tag.Checkout(context.TODO(), tmpDir, repo.Path(), &git.Auth{})
_, ref, err := tag.Checkout(context.TODO(), tmpDir, repo.Path(), nil)
if tt.expectErr != "" {
g.Expect(err.Error()).To(Equal(tt.expectErr))
g.Expect(ref).To(BeEmpty())
@ -194,7 +192,7 @@ func TestCheckoutCommit_Checkout(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "git2go")
defer os.RemoveAll(tmpDir)
_, ref, err := commit.Checkout(context.TODO(), tmpDir, repo.Path(), &git.Auth{})
_, ref, err := commit.Checkout(context.TODO(), tmpDir, repo.Path(), nil)
g.Expect(err).To(BeNil())
g.Expect(ref).To(Equal("main/" + c.String()))
g.Expect(filepath.Join(tmpDir, "commit")).To(BeARegularFile())
@ -206,7 +204,7 @@ func TestCheckoutCommit_Checkout(t *testing.T) {
tmpDir2, _ := os.MkdirTemp("", "git2go")
defer os.RemoveAll(tmpDir)
_, ref, err = commit.Checkout(context.TODO(), tmpDir2, repo.Path(), &git.Auth{})
_, ref, err = commit.Checkout(context.TODO(), tmpDir2, repo.Path(), nil)
g.Expect(err.Error()).To(HavePrefix("git checkout error: git commit '4dc3185c5fc94eb75048376edeb44571cece25f4' not found:"))
g.Expect(ref).To(BeEmpty())
}
@ -312,7 +310,7 @@ func TestCheckoutTagSemVer_Checkout(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
_, ref, err := semVer.Checkout(context.TODO(), tmpDir, repo.Path(), &git.Auth{})
_, ref, err := semVer.Checkout(context.TODO(), tmpDir, repo.Path(), nil)
if tt.expectErr != nil {
g.Expect(err).To(Equal(tt.expectErr))
g.Expect(ref).To(BeEmpty())

View File

@ -23,140 +23,119 @@ import (
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"fmt"
"hash"
"net"
"net/url"
"strings"
"time"
git2go "github.com/libgit2/git2go/v31"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
corev1 "k8s.io/api/core/v1"
"github.com/fluxcd/source-controller/pkg/git"
)
func AuthSecretStrategyForURL(URL string) (git.AuthSecretStrategy, error) {
u, err := url.Parse(URL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL to determine auth strategy: %w", err)
}
var (
now = time.Now
)
switch {
case u.Scheme == "http", u.Scheme == "https":
return &BasicAuth{}, nil
case u.Scheme == "ssh":
return &PublicKeyAuth{user: u.User.Username(), host: u.Host}, nil
default:
return nil, fmt.Errorf("no auth secret strategy for scheme %s", u.Scheme)
}
}
type BasicAuth struct{}
func (s *BasicAuth) Method(secret corev1.Secret) (*git.Auth, error) {
var credCallback git2go.CredentialsCallback
var username string
if d, ok := secret.Data["username"]; ok {
username = string(d)
}
var password string
if d, ok := secret.Data["password"]; ok {
password = string(d)
}
if username != "" && password != "" {
credCallback = func(url string, usernameFromURL string, allowedTypes git2go.CredentialType) (*git2go.Credential, error) {
cred, err := git2go.NewCredentialUserpassPlaintext(username, password)
if err != nil {
return nil, err
}
return cred, nil
// remoteCallbacks constructs RemoteCallbacks with credentialsCallback and
// certificateCallback, and the given options if the given opts is not nil.
func remoteCallbacks(opts *git.AuthOptions) git2go.RemoteCallbacks {
if opts != nil {
return git2go.RemoteCallbacks{
CredentialsCallback: credentialsCallback(opts),
CertificateCheckCallback: certificateCallback(opts),
}
}
return git2go.RemoteCallbacks{}
}
var certCallback git2go.CertificateCheckCallback
if caFile, ok := secret.Data[git.CAFile]; ok {
certCallback = func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM(caFile)
if !ok {
return git2go.ErrorCodeCertificate
// credentialsCallback constructs CredentialsCallbacks with the given options
// for git.Transport if the given opts is not nil, and returns the result.
func credentialsCallback(opts *git.AuthOptions) git2go.CredentialsCallback {
switch opts.Transport {
case git.HTTP:
if opts.Username != "" {
return func(u string, user string, allowedTypes git2go.CredentialType) (*git2go.Credential, error) {
return git2go.NewCredentialUsername(opts.Username)
}
opts := x509.VerifyOptions{
Roots: roots,
DNSName: hostname,
}
case git.HTTPS:
if opts.Username != "" && opts.Password != "" {
return func(u string, user string, allowedTypes git2go.CredentialType) (*git2go.Credential, error) {
return git2go.NewCredentialUserpassPlaintext(opts.Username, opts.Password)
}
_, err := cert.X509.Verify(opts)
if err != nil {
return git2go.ErrorCodeCertificate
}
case git.SSH:
if len(opts.Identity) > 0 {
return func(u string, user string, allowedTypes git2go.CredentialType) (*git2go.Credential, error) {
return git2go.NewCredentialSSHKeyFromMemory(opts.Username, "", string(opts.Identity), opts.Password)
}
return git2go.ErrorCodeOK
}
}
return &git.Auth{CredCallback: credCallback, CertCallback: certCallback}, nil
return nil
}
type PublicKeyAuth struct {
user string
host string
// certificateCallback constructs CertificateCallback with the given options
// for git.Transport if the given opts is not nil, and returns the result.
func certificateCallback(opts *git.AuthOptions) git2go.CertificateCheckCallback {
switch opts.Transport {
case git.HTTPS:
if len(opts.CAFile) > 0 {
return x509Callback(opts.CAFile)
}
case git.SSH:
if len(opts.KnownHosts) > 0 && opts.Host != "" {
return knownHostsCallback(opts.Host, opts.KnownHosts)
}
}
return nil
}
func (s *PublicKeyAuth) Method(secret corev1.Secret) (*git.Auth, error) {
if _, ok := secret.Data[git.CAFile]; ok {
return nil, fmt.Errorf("found %s key in secret '%s' but libgit2 SSH transport does not support custom certificates", git.CAFile, secret.Name)
}
identity := secret.Data["identity"]
knownHosts := secret.Data["known_hosts"]
if len(identity) == 0 || len(knownHosts) == 0 {
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'identity' and 'known_hosts'", secret.Name)
}
// x509Callback returns a CertificateCheckCallback that verifies the
// certificate against the given caBundle for git.HTTPS Transports.
func x509Callback(caBundle []byte) git2go.CertificateCheckCallback {
return func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
roots := x509.NewCertPool()
if ok := roots.AppendCertsFromPEM(caBundle); !ok {
return git2go.ErrorCodeCertificate
}
kk, err := parseKnownHosts(string(knownHosts))
if err != nil {
return nil, err
opts := x509.VerifyOptions{
Roots: roots,
DNSName: hostname,
CurrentTime: now(),
}
if _, err := cert.X509.Verify(opts); err != nil {
return git2go.ErrorCodeCertificate
}
return git2go.ErrorCodeOK
}
}
// Need to validate private key as it is not
// done by git2go when loading the key
password, ok := secret.Data["password"]
if ok {
_, err = ssh.ParsePrivateKeyWithPassphrase(identity, password)
} else {
_, err = ssh.ParsePrivateKey(identity)
}
if err != nil {
return nil, err
}
user := s.user
if user == "" {
user = git.DefaultPublicKeyAuthUser
}
credCallback := func(url string, usernameFromURL string, allowedTypes git2go.CredentialType) (*git2go.Credential, error) {
cred, err := git2go.NewCredentialSSHKeyFromMemory(user, "", string(identity), string(password))
// knownHostCallback returns a CertificateCheckCallback that verifies
// the key of Git server against the given host and known_hosts for
// git.SSH Transports.
func knownHostsCallback(host string, knownHosts []byte) git2go.CertificateCheckCallback {
return func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
kh, err := parseKnownHosts(string(knownHosts))
if err != nil {
return nil, err
return git2go.ErrorCodeCertificate
}
return cred, nil
}
certCallback := func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
// First, attempt to split the configured host and port to validate
// the port-less hostname given to the callback.
host, _, err := net.SplitHostPort(s.host)
h, _, err := net.SplitHostPort(host)
if err != nil {
// SplitHostPort returns an error if the host is missing
// a port, assume the host has no port.
host = s.host
h = host
}
// Check if the configured host matches the hostname given to
// the callback.
if host != hostname {
if h != hostname {
return git2go.ErrorCodeUser
}
@ -164,16 +143,14 @@ func (s *PublicKeyAuth) Method(secret corev1.Secret) (*git.Auth, error) {
// given to the callback match. Use the configured host (that
// includes the port), and normalize it, so we can check if there
// is an entry for the hostname _and_ port.
host = knownhosts.Normalize(s.host)
for _, k := range kk {
if k.matches(host, cert.Hostkey) {
h = knownhosts.Normalize(host)
for _, k := range kh {
if k.matches(h, cert.Hostkey) {
return git2go.ErrorCodeOK
}
}
return git2go.ErrorCodeCertificate
}
return &git.Auth{CredCallback: credCallback, CertCallback: certCallback}, nil
}
type knownKey struct {
@ -234,6 +211,5 @@ func containsHost(hosts []string, host string) bool {
return true
}
}
return false
}

View File

@ -17,163 +17,241 @@ limitations under the License.
package libgit2
import (
"bytes"
"crypto/x509"
"encoding/base64"
"reflect"
"encoding/pem"
"errors"
"testing"
"time"
git2go "github.com/libgit2/git2go/v31"
corev1 "k8s.io/api/core/v1"
"github.com/fluxcd/source-controller/pkg/git"
. "github.com/onsi/gomega"
)
const (
// secretKeyFixture is a randomly generated password less
// 512bit RSA private key.
secretKeyFixture string = `-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCrakELAKxozvwJijQEggYlTvS1QTZx1DaBwOhW/4kRSuR21plu
xuQeyuUiztoWeb9jgW7wjzG4j1PIJjdbsgjPIcIZ4PBY7JeEW+QRopfwuN8MHXNp
uTLgIHbkmhoOg5qBEcjzO/lEOOPpV0EmbObgqv3+wRmLJrgfzWl/cTtRewIDAQAB
AoGAawKFImpEN5Xn78iwWpQVZBsbV0AjzgHuGSiloxIZrorzf2DPHkHZzYNaclVx
/o/4tBTsfg7WumH3qr541qyZJDgU7iRMABwmx0v1vm2wQiX7NJzLzH2E9vlMC3mw
d8S99g9EqRuNH98XX8su34B9WGRPqiKvEm0RW8Hideo2/KkCQQDbs6rHcriKQyPB
paidHZAfguu0eVbyHT2EgLgRboWE+tEAqFEW2ycqNL3VPz9fRvwexbB6rpOcPpQJ
DEL4XB2XAkEAx7xJz8YlCQ2H38xggK8R8EUXF9Zhb0fqMJHMNmao1HCHVMtbsa8I
jR2EGyQ4CaIqNG5tdWukXQSJrPYDRWNvvQJAZX3rP7XUYDLB2twvN12HzbbKMhX3
v2MYnxRjc9INpi/Dyzz2MMvOnOW+aDuOh/If2AtVCmeJUx1pf4CFk3viQwJBAKyC
t824+evjv+NQBlme3AOF6PgxtV4D4wWoJ5Uk/dTejER0j/Hbl6sqPxuiILRRV9qJ
Ngkgu4mLjc3RfenEhJECQAx8zjWUE6kHHPGAd9DfiAIQ4bChqnyS0Nwb9+Gd4hSE
P0Ah10mHiK/M0o3T8Eanwum0gbQHPnOwqZgsPkwXRqQ=
-----END RSA PRIVATE KEY-----`
geoTrustRootFixture = `-----BEGIN CERTIFICATE-----
MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG
EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg
R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9
9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq
fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv
iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU
1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+
bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW
MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA
ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l
uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn
Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS
tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF
PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un
hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV
5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==
-----END CERTIFICATE-----`
// secretKeyFixture is a randomly generated
// 512bit RSA private key with password foobar.
secretPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,0B016973B2A761D31E6B388D0F327C35
giag2IntermediateFixture = `-----BEGIN CERTIFICATE-----
MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
YWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG
EwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy
bmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP
VaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv
h8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE
ahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ
EASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC
DTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7
qwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD
VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g
K4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI
KwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n
ZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB
BQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY
/iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/
zG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza
HFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto
WHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6
yuGnBXj8ytqU0CwIPX4WecigUCAkVDNx
-----END CERTIFICATE-----`
X9GET/qAyZkAJBl/RK+1XX75NxONgdUfZDw7PIYi/g+Efh3Z5zH5kh/dx9lxH5ZG
HGCqPAeMO/ofGDGtDULWW6iqDUFRu5gPgEVSCnnbqoHNU325WHhXdhejVAItwObC
IpL/zYfs2+gDHXct/n9FJ/9D/EGXZihwPqYaK8GQSfZAxz0QjLuh0wU1qpbm3y3N
q+o9FLv3b2Ys/tCJOUsYVQOYLSrZEI77y1ii3nWgQ8lXiTJbBUKzuq4f1YWeO8Ah
RZbdhTa57AF5lUaRtL7Nrm3HJUrK1alBbU7HHyjeW4Q4n/D3fiRDC1Mh2Bi4EOOn
wGctSx4kHsZGhJv5qwKqqPEFPhUzph8D2tm2TABk8HJa5KJFDbGrcfvk2uODAoZr
MbcpIxCfl8oB09bWfY6tDQjyvwSYYo2Phdwm7kT92xc=
-----END RSA PRIVATE KEY-----`
googleLeafFixture = `-----BEGIN CERTIFICATE-----
MIIEdjCCA16gAwIBAgIIcR5k4dkoe04wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMzEyMDkzODMwWhcNMTQwNjEwMDAwMDAw
WjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOd3d3
Lmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4zYCe
m0oUBhwE0EwBr65eBOcgcQO2PaSIAB2dEP/c1EMX2tOy0ov8rk83ePhJ+MWdT1z6
jge9X4zQQI8ZyA9qIiwrKBZOi8DNUvrqNZC7fJAVRrb9aX/99uYOJCypIbpmWG1q
fhbHjJewhwf8xYPj71eU4rLG80a+DapWmphtfq3h52lDQIBzLVf1yYbyrTaELaz4
NXF7HXb5YkId/gxIsSzM0aFUVu2o8sJcLYAsJqwfFKBKOMxUcn545nlspf0mTcWZ
0APlbwsKznNs4/xCDwIxxWjjqgHrYAFl6y07i1gzbAOqdNEyR24p+3JWI8WZBlBI
dk2KGj0W1fIfsvyxAgMBAAGjggFBMIIBPTAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20waAYIKwYBBQUHAQEE
XDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0
MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G
A1UdDgQWBBTXD5Bx6iqT+dmEhbFL4OUoHyZn8zAMBgNVHRMBAf8EAjAAMB8GA1Ud
IwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMBcGA1UdIAQQMA4wDAYKKwYBBAHW
eQIFATAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lB
RzIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCR3RJtHzgDh33b/MI1ugiki+nl8Ikj
5larbJRE/rcA5oite+QJyAr6SU1gJJ/rRrK3ItVEHr9L621BCM7GSdoNMjB9MMcf
tJAW0kYGJ+wqKm53wG/JaOADTnnq2Mt/j6F2uvjgN/ouns1nRHufIvd370N0LeH+
orKqTuAPzXK7imQk6+OycYABbqCtC/9qmwRd8wwn7sF97DtYfK8WuNHtFalCAwyi
8LxJJYJCLWoMhZ+V8GZm+FOex5qkQAjnZrtNlbQJ8ro4r+rpKXtmMFFhfa+7L+PA
Kom08eUK8skxAzfDDijZPh10VtJ66uBoiDPdT+uCBehcBIcmSTrKjFGX
-----END CERTIFICATE-----`
// knownHostsFixture is known_hosts fixture in the expected
// format.
knownHostsFixture string = `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`
// googleLeafWithInvalidHashFixture is the same as googleLeafFixture, but the signature
// algorithm in the certificate contains a nonsense OID.
googleLeafWithInvalidHashFixture = `-----BEGIN CERTIFICATE-----
MIIEdjCCA16gAwIBAgIIcR5k4dkoe04wDQYJKoZIhvcNAWAFBQAwSTELMAkGA1UE
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMzEyMDkzODMwWhcNMTQwNjEwMDAwMDAw
WjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOd3d3
Lmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4zYCe
m0oUBhwE0EwBr65eBOcgcQO2PaSIAB2dEP/c1EMX2tOy0ov8rk83ePhJ+MWdT1z6
jge9X4zQQI8ZyA9qIiwrKBZOi8DNUvrqNZC7fJAVRrb9aX/99uYOJCypIbpmWG1q
fhbHjJewhwf8xYPj71eU4rLG80a+DapWmphtfq3h52lDQIBzLVf1yYbyrTaELaz4
NXF7HXb5YkId/gxIsSzM0aFUVu2o8sJcLYAsJqwfFKBKOMxUcn545nlspf0mTcWZ
0APlbwsKznNs4/xCDwIxxWjjqgHrYAFl6y07i1gzbAOqdNEyR24p+3JWI8WZBlBI
dk2KGj0W1fIfsvyxAgMBAAGjggFBMIIBPTAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20waAYIKwYBBQUHAQEE
XDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0
MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G
A1UdDgQWBBTXD5Bx6iqT+dmEhbFL4OUoHyZn8zAMBgNVHRMBAf8EAjAAMB8GA1Ud
IwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMBcGA1UdIAQQMA4wDAYKKwYBBAHW
eQIFATAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lB
RzIuY3JsMA0GCSqGSIb3DQFgBQUAA4IBAQCR3RJtHzgDh33b/MI1ugiki+nl8Ikj
5larbJRE/rcA5oite+QJyAr6SU1gJJ/rRrK3ItVEHr9L621BCM7GSdoNMjB9MMcf
tJAW0kYGJ+wqKm53wG/JaOADTnnq2Mt/j6F2uvjgN/ouns1nRHufIvd370N0LeH+
orKqTuAPzXK7imQk6+OycYABbqCtC/9qmwRd8wwn7sF97DtYfK8WuNHtFalCAwyi
8LxJJYJCLWoMhZ+V8GZm+FOex5qkQAjnZrtNlbQJ8ro4r+rpKXtmMFFhfa+7L+PA
Kom08eUK8skxAzfDDijZPh10VtJ66uBoiDPdT+uCBehcBIcmSTrKjFGX
-----END CERTIFICATE-----`
knownHosts string = `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`
)
var (
basicAuthSecretFixture = corev1.Secret{
Data: map[string][]byte{
"username": []byte("git"),
"password": []byte("password"),
},
}
privateKeySecretFixture = corev1.Secret{
Data: map[string][]byte{
"identity": []byte(secretKeyFixture),
"known_hosts": []byte(knownHostsFixture),
},
}
privateKeySecretWithPassphraseFixture = corev1.Secret{
Data: map[string][]byte{
"identity": []byte(secretPassphraseFixture),
"known_hosts": []byte(knownHostsFixture),
"password": []byte("foobar"),
},
}
)
func Test_x509Callback(t *testing.T) {
now = func() time.Time { return time.Unix(1395785200, 0) }
func TestAuthSecretStrategyForURL(t *testing.T) {
tests := []struct {
name string
url string
want git.AuthSecretStrategy
wantErr bool
name string
certificate string
host string
caBundle []byte
want git2go.ErrorCode
}{
{"HTTP", "http://git.example.com/org/repo.git", &BasicAuth{}, false},
{"HTTPS", "https://git.example.com/org/repo.git", &BasicAuth{}, false},
{"SSH", "ssh://git.example.com:2222/org/repo.git", &PublicKeyAuth{host: "git.example.com:2222"}, false},
{"SSH with username", "ssh://example@git.example.com:2222/org/repo.git", &PublicKeyAuth{user: "example", host: "git.example.com:2222"}, false},
{"unsupported", "protocol://example.com", nil, true},
{
name: "Valid certificate authority bundle",
certificate: googleLeafFixture,
host: "www.google.com",
caBundle: []byte(giag2IntermediateFixture + "\n" + geoTrustRootFixture),
want: git2go.ErrorCodeOK,
},
{
name: "Invalid certificate",
certificate: googleLeafWithInvalidHashFixture,
host: "www.google.com",
caBundle: []byte(giag2IntermediateFixture + "\n" + geoTrustRootFixture),
want: git2go.ErrorCodeCertificate,
},
{
name: "Invalid certificate authority bundle",
certificate: googleLeafFixture,
host: "www.google.com",
caBundle: bytes.Trim([]byte(giag2IntermediateFixture+"\n"+geoTrustRootFixture), "-"),
want: git2go.ErrorCodeCertificate,
},
{
name: "Missing intermediate in bundle",
certificate: googleLeafFixture,
host: "www.google.com",
caBundle: []byte(geoTrustRootFixture),
want: git2go.ErrorCodeCertificate,
},
{
name: "Invalid host",
certificate: googleLeafFixture,
host: "www.google.co",
caBundle: []byte(giag2IntermediateFixture + "\n" + geoTrustRootFixture),
want: git2go.ErrorCodeCertificate,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := AuthSecretStrategyForURL(tt.url)
if (err != nil) != tt.wantErr {
t.Errorf("AuthSecretStrategyForURL() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AuthSecretStrategyForURL() got = %v, want %v", got, tt.want)
g := NewWithT(t)
cert := &git2go.Certificate{}
if tt.certificate != "" {
x509Cert, err := certificateFromPEM(tt.certificate)
g.Expect(err).ToNot(HaveOccurred())
cert.X509 = x509Cert
}
callback := x509Callback(tt.caBundle)
g.Expect(callback(cert, false, tt.host)).To(Equal(tt.want))
})
}
}
func TestBasicAuthStrategy_Method(t *testing.T) {
func Test_knownHostsCallback(t *testing.T) {
tests := []struct {
name string
secret corev1.Secret
modify func(secret *corev1.Secret)
wantErr bool
name string
host string
expectedHost string
knownHosts []byte
hostkey git2go.HostkeyCertificate
want git2go.ErrorCode
}{
{"with username and password", basicAuthSecretFixture, nil, false},
{
name: "Match",
host: "github.com",
knownHosts: []byte(knownHosts),
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
expectedHost: "github.com",
want: git2go.ErrorCodeOK,
},
{
name: "Match with port",
host: "github.com",
knownHosts: []byte(knownHosts),
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
expectedHost: "github.com:22",
want: git2go.ErrorCodeOK,
},
{
name: "Hostname mismatch",
host: "github.com",
knownHosts: []byte(knownHosts),
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
expectedHost: "example.com",
want: git2go.ErrorCodeUser,
},
{
name: "Hostkey mismatch",
host: "github.com",
knownHosts: []byte(knownHosts),
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeyMD5, HashMD5: md5Fingerprint("\xb6\x03\x0e\x39\x97\x9e\xd0\xe7\x24\xce\xa3\x77\x3e\x01\x42\x09")},
expectedHost: "github.com",
want: git2go.ErrorCodeCertificate,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := tt.secret.DeepCopy()
if tt.modify != nil {
tt.modify(secret)
}
s := &BasicAuth{}
_, err := s.Method(*secret)
if (err != nil) != tt.wantErr {
t.Errorf("Method() error = %v, wantErr %v", err, tt.wantErr)
return
}
g := NewWithT(t)
cert := &git2go.Certificate{Hostkey: tt.hostkey}
callback := knownHostsCallback(tt.expectedHost, tt.knownHosts)
g.Expect(callback(cert, false, tt.host)).To(Equal(tt.want))
})
}
}
func TestPublicKeyStrategy_Method(t *testing.T) {
tests := []struct {
name string
secret corev1.Secret
modify func(secret *corev1.Secret)
wantErr bool
}{
{"private key and known_hosts", privateKeySecretFixture, nil, false},
{"private key with passphrase and known_hosts", privateKeySecretWithPassphraseFixture, nil, false},
{"missing private key", privateKeySecretFixture, func(s *corev1.Secret) { delete(s.Data, "identity") }, true},
{"invalid private key", privateKeySecretFixture, func(s *corev1.Secret) { s.Data["identity"] = []byte(`-----BEGIN RSA PRIVATE KEY-----`) }, true},
{"missing known_hosts", privateKeySecretFixture, func(s *corev1.Secret) { delete(s.Data, "known_hosts") }, true},
{"invalid known_hosts", privateKeySecretFixture, func(s *corev1.Secret) { s.Data["known_hosts"] = []byte(`invalid`) }, true},
{"missing password", privateKeySecretWithPassphraseFixture, func(s *corev1.Secret) { delete(s.Data, "password") }, true},
{"invalid password", privateKeySecretWithPassphraseFixture, func(s *corev1.Secret) { s.Data["password"] = []byte("foo") }, true},
{"empty", corev1.Secret{}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := tt.secret.DeepCopy()
if tt.modify != nil {
tt.modify(secret)
}
s := &PublicKeyAuth{}
_, err := s.Method(*secret)
if (err != nil) != tt.wantErr {
t.Errorf("Method() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestKnownKeyHash(t *testing.T) {
func Test_parseKnownHosts(t *testing.T) {
tests := []struct {
name string
hostkey git2go.HostkeyCertificate
@ -189,24 +267,22 @@ func TestKnownKeyHash(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
knownKeys, err := parseKnownHosts(knownHostsFixture)
g := NewWithT(t)
knownKeys, err := parseKnownHosts(knownHosts)
if err != nil {
t.Error(err)
return
}
matches := knownKeys[0].matches("github.com", tt.hostkey)
if matches != tt.wantMatches {
t.Errorf("Method() matches = %v, wantMatches %v", matches, tt.wantMatches)
return
}
g.Expect(matches).To(Equal(tt.wantMatches))
})
}
}
func md5Fingerprint(in string) [16]byte {
var out [16]byte
copy(out[:], []byte(in))
copy(out[:], in)
return out
}
@ -229,3 +305,11 @@ func sha256Fingerprint(in string) [32]byte {
copy(out[:], d)
return out
}
func certificateFromPEM(pemBytes string) (*x509.Certificate, error) {
block, _ := pem.Decode([]byte(pemBytes))
if block == nil {
return nil, errors.New("failed to decode PEM")
}
return x509.ParseCertificate(block.Bytes)
}

121
pkg/git/options.go Normal file
View File

@ -0,0 +1,121 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package git
import (
"fmt"
"net/url"
"golang.org/x/crypto/ssh"
v1 "k8s.io/api/core/v1"
)
const (
DefaultOrigin = "origin"
DefaultBranch = "master"
DefaultPublicKeyAuthUser = "git"
)
// CheckoutOptions are the options used for a Git checkout.
type CheckoutOptions struct {
GitImplementation string
RecurseSubmodules bool
}
type TransportType string
const (
SSH TransportType = "ssh"
HTTPS TransportType = "https"
HTTP TransportType = "http"
)
// AuthOptions are the authentication options for the Transport of
// communication with a remote origin.
type AuthOptions struct {
Transport TransportType
Host string
Username string
Password string
Identity []byte
KnownHosts []byte
CAFile []byte
}
// Validate the AuthOptions against the defined Transport.
func (o AuthOptions) Validate() error {
switch o.Transport {
case HTTPS, HTTP:
if o.Username == "" && o.Password != "" {
return fmt.Errorf("invalid '%s' auth option: 'password' requires 'username' to be set", o.Transport)
}
case SSH:
if len(o.Identity) == 0 {
return fmt.Errorf("invalid '%s' auth option: 'identity' is required", o.Transport)
}
var err error
if o.Password != "" {
_, err = ssh.ParsePrivateKeyWithPassphrase(o.Identity, []byte(o.Password))
} else {
_, err = ssh.ParsePrivateKey(o.Identity)
}
if err != nil {
return fmt.Errorf("invalid '%s' auth option 'identity': %w", o.Transport, err)
}
if len(o.KnownHosts) == 0 {
return fmt.Errorf("invalid '%s' auth option: 'known_hosts' is required", o.Transport)
}
case "":
return fmt.Errorf("no transport type set")
}
return nil
}
// AuthOptionsFromSecret constructs an AuthOptions object from the given Secret,
// and then validates the result. It returns the AuthOptions, or an error.
func AuthOptionsFromSecret(URL string, secret *v1.Secret) (*AuthOptions, error) {
if secret == nil {
return nil, fmt.Errorf("no secret provided to construct auth strategy from")
}
u, err := url.Parse(URL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL to determine auth strategy: %w", err)
}
opts := &AuthOptions{
Transport: TransportType(u.Scheme),
Host: u.Host,
Username: string(secret.Data["username"]),
Password: string(secret.Data["password"]),
CAFile: secret.Data["caFile"],
Identity: secret.Data["identity"],
KnownHosts: secret.Data["known_hosts"],
}
if opts.Username == "" {
opts.Username = u.User.Username()
}
if opts.Username == "" {
opts.Username = DefaultPublicKeyAuthUser
}
if err = opts.Validate(); err != nil {
return nil, err
}
return opts, nil
}

236
pkg/git/options_test.go Normal file
View File

@ -0,0 +1,236 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package git
import (
"testing"
. "github.com/onsi/gomega"
v1 "k8s.io/api/core/v1"
)
const (
// privateKeyFixture is a randomly generated password less
// 512bit RSA private key.
privateKeyFixture = `-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCrakELAKxozvwJijQEggYlTvS1QTZx1DaBwOhW/4kRSuR21plu
xuQeyuUiztoWeb9jgW7wjzG4j1PIJjdbsgjPIcIZ4PBY7JeEW+QRopfwuN8MHXNp
uTLgIHbkmhoOg5qBEcjzO/lEOOPpV0EmbObgqv3+wRmLJrgfzWl/cTtRewIDAQAB
AoGAawKFImpEN5Xn78iwWpQVZBsbV0AjzgHuGSiloxIZrorzf2DPHkHZzYNaclVx
/o/4tBTsfg7WumH3qr541qyZJDgU7iRMABwmx0v1vm2wQiX7NJzLzH2E9vlMC3mw
d8S99g9EqRuNH98XX8su34B9WGRPqiKvEm0RW8Hideo2/KkCQQDbs6rHcriKQyPB
paidHZAfguu0eVbyHT2EgLgRboWE+tEAqFEW2ycqNL3VPz9fRvwexbB6rpOcPpQJ
DEL4XB2XAkEAx7xJz8YlCQ2H38xggK8R8EUXF9Zhb0fqMJHMNmao1HCHVMtbsa8I
jR2EGyQ4CaIqNG5tdWukXQSJrPYDRWNvvQJAZX3rP7XUYDLB2twvN12HzbbKMhX3
v2MYnxRjc9INpi/Dyzz2MMvOnOW+aDuOh/If2AtVCmeJUx1pf4CFk3viQwJBAKyC
t824+evjv+NQBlme3AOF6PgxtV4D4wWoJ5Uk/dTejER0j/Hbl6sqPxuiILRRV9qJ
Ngkgu4mLjc3RfenEhJECQAx8zjWUE6kHHPGAd9DfiAIQ4bChqnyS0Nwb9+Gd4hSE
P0Ah10mHiK/M0o3T8Eanwum0gbQHPnOwqZgsPkwXRqQ=
-----END RSA PRIVATE KEY-----`
// privateKeyPassphraseFixture is a randomly generated
// 512bit RSA private key with password foobar.
privateKeyPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,0B016973B2A761D31E6B388D0F327C35
X9GET/qAyZkAJBl/RK+1XX75NxONgdUfZDw7PIYi/g+Efh3Z5zH5kh/dx9lxH5ZG
HGCqPAeMO/ofGDGtDULWW6iqDUFRu5gPgEVSCnnbqoHNU325WHhXdhejVAItwObC
IpL/zYfs2+gDHXct/n9FJ/9D/EGXZihwPqYaK8GQSfZAxz0QjLuh0wU1qpbm3y3N
q+o9FLv3b2Ys/tCJOUsYVQOYLSrZEI77y1ii3nWgQ8lXiTJbBUKzuq4f1YWeO8Ah
RZbdhTa57AF5lUaRtL7Nrm3HJUrK1alBbU7HHyjeW4Q4n/D3fiRDC1Mh2Bi4EOOn
wGctSx4kHsZGhJv5qwKqqPEFPhUzph8D2tm2TABk8HJa5KJFDbGrcfvk2uODAoZr
MbcpIxCfl8oB09bWfY6tDQjyvwSYYo2Phdwm7kT92xc=
-----END RSA PRIVATE KEY-----`
// knownHostsFixture is known_hosts fixture in the expected
// format.
knownHostsFixture = `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`
)
func TestAuthOptions_Validate(t *testing.T) {
tests := []struct {
name string
opts AuthOptions
wantErr string
}{
{
name: "HTTP transport with password requires user",
opts: AuthOptions{
Transport: HTTP,
Password: "foo",
},
wantErr: "invalid 'http' auth option: 'password' requires 'username' to be set",
},
{
name: "HTTPS transport with password requires user",
opts: AuthOptions{
Transport: HTTPS,
Password: "foo",
},
wantErr: "invalid 'https' auth option: 'password' requires 'username' to be set",
},
{
name: "SSH transport requires identity",
opts: AuthOptions{
Transport: SSH,
},
wantErr: "invalid 'ssh' auth option: 'identity' is required",
},
{
name: "SSH transport requires valid identity",
opts: AuthOptions{
Transport: SSH,
Identity: []byte("malformed"),
},
wantErr: "invalid 'ssh' auth option 'identity': ssh: no key found",
},
{
name: "SSH transport requires valid identity password",
opts: AuthOptions{
Transport: SSH,
Identity: []byte(privateKeyPassphraseFixture),
Password: "invalid",
},
wantErr: "invalid 'ssh' auth option 'identity': x509: decryption password incorrect",
},
{
name: "SSH transport requires known_hosts",
opts: AuthOptions{
Transport: SSH,
Identity: []byte(privateKeyFixture),
},
wantErr: "invalid 'ssh' auth option: 'known_hosts' is required",
},
{
name: "Requires transport",
opts: AuthOptions{},
wantErr: "no transport type set",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
got := tt.opts.Validate()
if tt.wantErr != "" {
g.Expect(got.Error()).To(ContainSubstring(tt.wantErr))
return
}
g.Expect(got).ToNot(HaveOccurred())
})
}
}
func TestAuthOptionsFromSecret(t *testing.T) {
tests := []struct {
name string
URL string
secret *v1.Secret
wantFunc func(g *WithT, opts *AuthOptions, secret *v1.Secret)
wantErr string
}{
{
name: "Sets values from Secret",
URL: "https://git@example.com",
secret: &v1.Secret{
Data: map[string][]byte{
"username": []byte("example"), // This takes precedence over the one from the URL
"password": []byte("secret"),
"identity": []byte(privateKeyFixture),
"known_hosts": []byte(knownHostsFixture),
"caFile": []byte("mock"),
},
},
wantFunc: func(g *WithT, opts *AuthOptions, secret *v1.Secret) {
g.Expect(opts.Username).To(Equal("example"))
g.Expect(opts.Password).To(Equal("secret"))
g.Expect(opts.Identity).To(BeEquivalentTo(privateKeyFixture))
g.Expect(opts.KnownHosts).To(BeEquivalentTo(knownHostsFixture))
g.Expect(opts.CAFile).To(BeEquivalentTo("mock"))
},
},
{
name: "Sets default user",
URL: "http://example.com",
secret: &v1.Secret{},
wantFunc: func(g *WithT, opts *AuthOptions, secret *v1.Secret) {
g.Expect(opts.Username).To(Equal(DefaultPublicKeyAuthUser))
},
},
{
name: "Sets transport from URL",
URL: "http://git@example.com",
secret: &v1.Secret{},
wantFunc: func(g *WithT, opts *AuthOptions, secret *v1.Secret) {
g.Expect(opts.Transport).To(Equal(HTTP))
},
},
{
name: "Sets user from URL",
URL: "http://example@example.com",
secret: &v1.Secret{
Data: map[string][]byte{
"password": []byte("secret"),
},
},
wantFunc: func(g *WithT, opts *AuthOptions, secret *v1.Secret) {
g.Expect(opts.Username).To(Equal("example"))
g.Expect(opts.Password).To(Equal("secret"))
},
},
{
name: "Validates options",
URL: "ssh://example.com",
secret: &v1.Secret{
Data: map[string][]byte{
"identity": []byte(privateKeyFixture),
},
},
wantErr: "invalid 'ssh' auth option: 'known_hosts' is required",
},
{
name: "Errors without secret",
secret: nil,
wantErr: "no secret provided to construct auth strategy from",
},
{
name: "Errors on malformed URL",
URL: ":example",
secret: &v1.Secret{},
wantErr: "failed to parse URL to determine auth strategy",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
got, err := AuthOptionsFromSecret(tt.URL, tt.secret)
if tt.wantErr != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
g.Expect(got).To(BeNil())
return
}
g.Expect(err).To(BeNil())
if tt.wantFunc != nil {
tt.wantFunc(g, got, tt.secret)
}
})
}
}

View File

@ -32,17 +32,6 @@ func CheckoutStrategyForRef(ref *sourcev1.GitRepositoryRef, opt git.CheckoutOpti
case sourcev1.LibGit2Implementation:
return libgit2.CheckoutStrategyForRef(ref, opt), nil
default:
return nil, fmt.Errorf("invalid Git implementation %s", opt.GitImplementation)
}
}
func AuthSecretStrategyForURL(url string, opt git.CheckoutOptions) (git.AuthSecretStrategy, error) {
switch opt.GitImplementation {
case sourcev1.GoGitImplementation:
return gogit.AuthSecretStrategyForURL(url)
case sourcev1.LibGit2Implementation:
return libgit2.AuthSecretStrategyForURL(url)
default:
return nil, fmt.Errorf("invalid Git implementation %s", opt.GitImplementation)
return nil, fmt.Errorf("unsupported Git implementation %s", opt.GitImplementation)
}
}