diff --git a/go.mod b/go.mod index 5ec05b08..6c1f6272 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 github.com/cyphar/filepath-securejoin v0.2.2 github.com/fluxcd/pkg/apis/meta v0.10.0 - github.com/fluxcd/pkg/gittestserver v0.3.0 + github.com/fluxcd/pkg/gittestserver v0.4.0 github.com/fluxcd/pkg/gitutil v0.1.0 github.com/fluxcd/pkg/helmtestserver v0.2.0 github.com/fluxcd/pkg/lockedfile v0.1.0 diff --git a/go.sum b/go.sum index 34c48c50..27e597a9 100644 --- a/go.sum +++ b/go.sum @@ -266,8 +266,8 @@ github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fluxcd/pkg/apis/meta v0.10.0 h1:N7wVGHC1cyPdT87hrDC7UwCwRwnZdQM46PBSLjG2rlE= github.com/fluxcd/pkg/apis/meta v0.10.0/go.mod h1:CW9X9ijMTpNe7BwnokiUOrLl/h13miwVr/3abEQLbKE= -github.com/fluxcd/pkg/gittestserver v0.3.0 h1:6aa30mybecBwBWaJ2IEk7pQzefWnjWjxkTSrHMHawvg= -github.com/fluxcd/pkg/gittestserver v0.3.0/go.mod h1:8j36Z6B0BuKNZZ6exAWoyDEpyQoFcjz1IX3WBT7PZNg= +github.com/fluxcd/pkg/gittestserver v0.4.0 h1:VQzQ5TcHzohxbYGWpnQ/79w7/rnS2SQGC7FSDtbIsCA= +github.com/fluxcd/pkg/gittestserver v0.4.0/go.mod h1:hUPx21fe/6oox336Wih/XF1fnmzLmptNMOvATbTZXNY= github.com/fluxcd/pkg/gitutil v0.1.0 h1:VO3kJY/CKOCO4ysDNqfdpTg04icAKBOSb3lbR5uE/IE= github.com/fluxcd/pkg/gitutil v0.1.0/go.mod h1:Ybz50Ck5gkcnvF0TagaMwtlRy3X3wXuiri1HVsK5id4= github.com/fluxcd/pkg/helmtestserver v0.2.0 h1:cE7YHDmrWI0hr9QpaaeQ0vQ16Z0IiqZKiINDpqdY610= @@ -984,7 +984,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= diff --git a/pkg/git/strategy/strategy_test.go b/pkg/git/strategy/strategy_test.go new file mode 100644 index 00000000..de176158 --- /dev/null +++ b/pkg/git/strategy/strategy_test.go @@ -0,0 +1,200 @@ +/* +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 strategy + +import ( + "context" + "net/url" + "os" + "strings" + "testing" + "time" + + "github.com/fluxcd/pkg/gittestserver" + "github.com/fluxcd/pkg/ssh" + . "github.com/onsi/gomega" + + "github.com/fluxcd/source-controller/pkg/git" + "github.com/fluxcd/source-controller/pkg/git/gogit" + "github.com/fluxcd/source-controller/pkg/git/libgit2" +) + +func TestCheckoutStrategyForImplementation_Auth(t *testing.T) { + gitImpls := []git.Implementation{gogit.Implementation, libgit2.Implementation} + + type testCase struct { + name string + transport git.TransportType + getRepoURL func(g *WithT, srv *gittestserver.GitServer, repoPath string) string + getAuthOpts func(g *WithT, u *url.URL, user string, pswd string, ca []byte) *git.AuthOptions + wantFunc func(g *WithT, cs git.CheckoutStrategy, dir string, repoURL string, authOpts *git.AuthOptions) + } + + cases := []testCase{ + { + name: "http cloning", + transport: git.HTTP, + getRepoURL: func(g *WithT, srv *gittestserver.GitServer, repoPath string) string { + return srv.HTTPAddressWithCredentials() + "/" + repoPath + }, + getAuthOpts: func(g *WithT, u *url.URL, user string, pswd string, ca []byte) *git.AuthOptions { + return &git.AuthOptions{ + Transport: git.HTTP, + Username: user, + Password: pswd, + } + }, + wantFunc: func(g *WithT, cs git.CheckoutStrategy, dir string, repoURL string, authOpts *git.AuthOptions) { + _, err := cs.Checkout(context.TODO(), dir, repoURL, authOpts) + g.Expect(err).ToNot(HaveOccurred()) + }, + }, + { + name: "https cloning", + transport: git.HTTPS, + getRepoURL: func(g *WithT, srv *gittestserver.GitServer, repoPath string) string { + return srv.HTTPAddress() + "/" + repoPath + }, + getAuthOpts: func(g *WithT, u *url.URL, user, pswd string, ca []byte) *git.AuthOptions { + return &git.AuthOptions{ + Transport: git.HTTPS, + Username: user, + Password: pswd, + CAFile: ca, + } + }, + wantFunc: func(g *WithT, cs git.CheckoutStrategy, dir, repoURL string, authOpts *git.AuthOptions) { + _, err := cs.Checkout(context.TODO(), dir, repoURL, authOpts) + g.Expect(err).ToNot(HaveOccurred()) + }, + }, + { + name: "ssh cloning", + transport: git.SSH, + getRepoURL: func(g *WithT, srv *gittestserver.GitServer, repoPath string) string { + return getSSHRepoURL(srv.SSHAddress(), repoPath) + }, + getAuthOpts: func(g *WithT, u *url.URL, user, pswd string, ca []byte) *git.AuthOptions { + knownhosts, err := ssh.ScanHostKey(u.Host, 5*time.Second) + g.Expect(err).ToNot(HaveOccurred()) + + keygen := ssh.NewRSAGenerator(2048) + pair, err := keygen.Generate() + g.Expect(err).ToNot(HaveOccurred()) + + return &git.AuthOptions{ + Host: u.Host, // Without this libgit2 returns error "user cancelled hostkey check". + Transport: git.SSH, + Username: "git", // Without this libgit2 returns error "username does not match previous request". + Identity: pair.PrivateKey, + KnownHosts: knownhosts, + } + }, + wantFunc: func(g *WithT, cs git.CheckoutStrategy, dir, repoURL string, authOpts *git.AuthOptions) { + _, err := cs.Checkout(context.TODO(), dir, repoURL, authOpts) + g.Expect(err).ToNot(HaveOccurred()) + }, + }, + } + + testFunc := func(tt testCase, impl git.Implementation) func(t *testing.T) { + return func(t *testing.T) { + g := NewWithT(t) + + var examplePublicKey, examplePrivateKey, exampleCA []byte + + gitServer, err := gittestserver.NewTempGitServer() + g.Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(gitServer.Root()) + + username := "test-user" + password := "test-password" + gitServer.Auth(username, password) + gitServer.KeyDir(gitServer.Root()) + + // Start the HTTP/HTTPS server. + if tt.transport == git.HTTPS { + var err error + examplePublicKey, err = os.ReadFile("testdata/certs/server.pem") + g.Expect(err).ToNot(HaveOccurred()) + examplePrivateKey, err = os.ReadFile("testdata/certs/server-key.pem") + g.Expect(err).ToNot(HaveOccurred()) + exampleCA, err = os.ReadFile("testdata/certs/ca.pem") + g.Expect(err).ToNot(HaveOccurred()) + err = gitServer.StartHTTPS(examplePublicKey, examplePrivateKey, exampleCA, "example.com") + g.Expect(err).ToNot(HaveOccurred()) + } else { + g.Expect(gitServer.StartHTTP()).ToNot(HaveOccurred()) + } + + defer gitServer.StopHTTP() + + // Start the SSH server. + if tt.transport == git.SSH { + g.Expect(gitServer.ListenSSH()).ToNot(HaveOccurred()) + go func() { + gitServer.StartSSH() + }() + defer func() { + g.Expect(gitServer.StopSSH()).To(Succeed()) + }() + } + + // Initialize a git repo. + // TODO: Fix pkg/gittestserver InitRepo() bug to enable creating + // custom branch. + // branch := "main" + branch := "master" + repoPath := "bar/test-reponame" + err = gitServer.InitRepo("testdata/repo1", branch, repoPath) + g.Expect(err).ToNot(HaveOccurred()) + + repoURL := tt.getRepoURL(g, gitServer, repoPath) + u, err := url.Parse(repoURL) + g.Expect(err).ToNot(HaveOccurred()) + authOpts := tt.getAuthOpts(g, u, username, password, exampleCA) + + // Get the checkout strategy. + checkoutOpts := git.CheckoutOptions{ + Branch: branch, + } + checkoutStrategy, err := CheckoutStrategyForImplementation(context.TODO(), impl, checkoutOpts) + g.Expect(err).ToNot(HaveOccurred()) + + tmpDir, err := os.MkdirTemp("", "test-checkout") + g.Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(tmpDir) + + tt.wantFunc(g, checkoutStrategy, tmpDir, repoURL, authOpts) + } + } + + // Run the test cases against the git implementations. + for _, gitImpl := range gitImpls { + for _, tt := range cases { + t.Run(string(gitImpl)+"_"+tt.name, testFunc(tt, gitImpl)) + } + } +} + +func getSSHRepoURL(sshAddress, repoPath string) string { + // This is expected to use 127.0.0.1, but host key + // checking usually wants a hostname, so use + // "localhost". + sshURL := strings.Replace(sshAddress, "127.0.0.1", "localhost", 1) + return sshURL + "/" + repoPath +} diff --git a/pkg/git/strategy/testdata/certs/Makefile b/pkg/git/strategy/testdata/certs/Makefile new file mode 100644 index 00000000..5ec8f26c --- /dev/null +++ b/pkg/git/strategy/testdata/certs/Makefile @@ -0,0 +1,30 @@ +# 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. + +all: server-key.pem + +ca-key.pem: ca-csr.json + cfssl gencert -initca ca-csr.json | cfssljson -bare ca – +ca.pem: ca-key.pem +ca.csr: ca-key.pem + +server-key.pem: server-csr.json ca-config.json ca-key.pem + cfssl gencert \ + -ca=ca.pem \ + -ca-key=ca-key.pem \ + -config=ca-config.json \ + -profile=web-servers \ + server-csr.json | cfssljson -bare server +sever.pem: server-key.pem +server.csr: server-key.pem diff --git a/pkg/git/strategy/testdata/certs/ca-config.json b/pkg/git/strategy/testdata/certs/ca-config.json new file mode 100644 index 00000000..91c0644c --- /dev/null +++ b/pkg/git/strategy/testdata/certs/ca-config.json @@ -0,0 +1,18 @@ +{ + "signing": { + "default": { + "expiry": "87600h" + }, + "profiles": { + "web-servers": { + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ], + "expiry": "87600h" + } + } + } +} diff --git a/pkg/git/strategy/testdata/certs/ca-csr.json b/pkg/git/strategy/testdata/certs/ca-csr.json new file mode 100644 index 00000000..941277bb --- /dev/null +++ b/pkg/git/strategy/testdata/certs/ca-csr.json @@ -0,0 +1,9 @@ +{ + "CN": "example.com CA", + "hosts": [ + "127.0.0.1", + "localhost", + "example.com", + "www.example.com" + ] +} diff --git a/pkg/git/strategy/testdata/certs/ca-key.pem b/pkg/git/strategy/testdata/certs/ca-key.pem new file mode 100644 index 00000000..b69de5ab --- /dev/null +++ b/pkg/git/strategy/testdata/certs/ca-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIOH/u9dMcpVcZ0+X9Fc78dCTj8SHuXawhLjhu/ej64WToAoGCCqGSM49 +AwEHoUQDQgAEruH/kPxtX3cyYR2G7TYmxLq6AHyzo/NGXc9XjGzdJutE2SQzn37H +dvSJbH+Lvqo9ik0uiJVRVdCYD1j7gNszGA== +-----END EC PRIVATE KEY----- diff --git a/pkg/git/strategy/testdata/certs/ca.csr b/pkg/git/strategy/testdata/certs/ca.csr new file mode 100644 index 00000000..baa8aeb2 --- /dev/null +++ b/pkg/git/strategy/testdata/certs/ca.csr @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBIDCBxgIBADAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr +RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxigSzBJBgkqhkiG9w0BCQ4x +PDA6MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFt +cGxlLmNvbYcEfwAAATAKBggqhkjOPQQDAgNJADBGAiEAkw85nyLhJssyCYsaFvRU +EErhu66xHPJug/nG50uV5OoCIQCUorrflOSxfChPeCe4xfwcPv7FpcCYbKVYtGzz +b34Wow== +-----END CERTIFICATE REQUEST----- diff --git a/pkg/git/strategy/testdata/certs/ca.pem b/pkg/git/strategy/testdata/certs/ca.pem new file mode 100644 index 00000000..080bd24e --- /dev/null +++ b/pkg/git/strategy/testdata/certs/ca.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBhzCCAS2gAwIBAgIUdsAtiX3gN0uk7ddxASWYE/tdv0wwCgYIKoZIzj0EAwIw +GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMjUw +NDE2MDgxODAwWjAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr +RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxijUzBRMA4GA1UdDwEB/wQE +AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGyUiU1QEZiMAqjsnIYTwZ +4yp5wzAPBgNVHREECDAGhwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIQDzdtvKdE8O +1+WRTZ9MuSiFYcrEz7Zne7VXouDEKqKEigIgM4WlbDeuNCKbqhqj+xZV0pa3rweb +OD8EjjCMY69RMO0= +-----END CERTIFICATE----- diff --git a/pkg/git/strategy/testdata/certs/server-csr.json b/pkg/git/strategy/testdata/certs/server-csr.json new file mode 100644 index 00000000..0baf1160 --- /dev/null +++ b/pkg/git/strategy/testdata/certs/server-csr.json @@ -0,0 +1,9 @@ +{ + "CN": "example.com", + "hosts": [ + "127.0.0.1", + "localhost", + "example.com", + "www.example.com" + ] +} diff --git a/pkg/git/strategy/testdata/certs/server-key.pem b/pkg/git/strategy/testdata/certs/server-key.pem new file mode 100644 index 00000000..5054ff39 --- /dev/null +++ b/pkg/git/strategy/testdata/certs/server-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKQbEXV6nljOHMmPrWVWQ+JrAE5wsbE9iMhfY7wlJgXOoAoGCCqGSM49 +AwEHoUQDQgAE+53oBGlrvVUTelSGYji8GNHVhVg8jOs1PeeLuXCIZjQmctHLFEq3 +fE+mGxCL93MtpYzlwIWBf0m7pEGQre6bzg== +-----END EC PRIVATE KEY----- diff --git a/pkg/git/strategy/testdata/certs/server.csr b/pkg/git/strategy/testdata/certs/server.csr new file mode 100644 index 00000000..5caf7b39 --- /dev/null +++ b/pkg/git/strategy/testdata/certs/server.csr @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBHDCBwwIBADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR +yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86gSzBJBgkqhkiG9w0BCQ4xPDA6 +MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFtcGxl +LmNvbYcEfwAAATAKBggqhkjOPQQDAgNIADBFAiB5A6wvQ5x6g/zhiyn+wLzXsOaB +Gb/F25p/zTHHQqZbkwIhAPUgWzy/2bs6eZEi97bSlaRdmrqHwqT842t5sEwGyXNV +-----END CERTIFICATE REQUEST----- diff --git a/pkg/git/strategy/testdata/certs/server.pem b/pkg/git/strategy/testdata/certs/server.pem new file mode 100644 index 00000000..11c655a0 --- /dev/null +++ b/pkg/git/strategy/testdata/certs/server.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB7TCCAZKgAwIBAgIUB+17B8PU05wVTzRHLeG+S+ybZK4wCgYIKoZIzj0EAwIw +GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMzAw +NDE1MDgxODAwWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR +yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86jgbowgbcwDgYDVR0PAQH/BAQD +AgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAA +MB0GA1UdDgQWBBTM8HS5EIlVMBYv/300jN8PEArUgDAfBgNVHSMEGDAWgBQGyUiU +1QEZiMAqjsnIYTwZ4yp5wzA4BgNVHREEMTAvgglsb2NhbGhvc3SCC2V4YW1wbGUu +Y29tgg93d3cuZXhhbXBsZS5jb22HBH8AAAEwCgYIKoZIzj0EAwIDSQAwRgIhAOgB +5W82FEgiTTOmsNRekkK5jUPbj4D4eHtb2/BI7ph4AiEA2AxHASIFBdv5b7Qf5prb +bdNmUCzAvVuCAKuMjg2OPrE= +-----END CERTIFICATE----- diff --git a/pkg/git/strategy/testdata/repo1/foo.txt b/pkg/git/strategy/testdata/repo1/foo.txt new file mode 100644 index 00000000..16b14f5d --- /dev/null +++ b/pkg/git/strategy/testdata/repo1/foo.txt @@ -0,0 +1 @@ +test file