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:
parent
46a5b9c27d
commit
0cf0d4e756
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -25,7 +25,7 @@ import (
|
|||
)
|
||||
|
||||
func TestCheckoutTagSemVer_Checkout(t *testing.T) {
|
||||
auth := &git.Auth{}
|
||||
auth := &git.AuthOptions{}
|
||||
tag := CheckoutTag{
|
||||
tag: "v1.7.0",
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue