Skip to content

Commit eb55926

Browse files
authored
Merge pull request #132 from fluxcd/custom-ca-option
Add support for self-signed certificates
2 parents cda93bf + 9312425 commit eb55926

File tree

6 files changed

+95
-4
lines changed

6 files changed

+95
-4
lines changed

gitprovider/client_options.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package gitprovider
1818

1919
import (
20+
"crypto/tls"
21+
"crypto/x509"
2022
"fmt"
2123
"net/http"
2224

@@ -59,6 +61,9 @@ type CommonClientOptions struct {
5961

6062
// Logger allows the caller to pass a logger for use by the provider
6163
Logger *logr.Logger
64+
65+
// CABundle is a []byte containing the CA bundle to use for the client.
66+
CABundle []byte
6267
}
6368

6469
// ApplyToCommonClientOptions applies the currently set fields in opts to target. If both opts and
@@ -106,6 +111,14 @@ func (opts *CommonClientOptions) ApplyToCommonClientOptions(target *CommonClient
106111
}
107112
target.Logger = opts.Logger
108113
}
114+
115+
if opts.CABundle != nil {
116+
if target.CABundle != nil {
117+
return fmt.Errorf("option CABundle already configured: %w", ErrInvalidClientOptions)
118+
}
119+
target.CABundle = opts.CABundle
120+
}
121+
109122
return nil
110123
}
111124

@@ -292,3 +305,32 @@ func MakeClientOptions(opts ...ClientOption) (*ClientOptions, error) {
292305
}
293306
return o, nil
294307
}
308+
309+
// WithCustomCAPostChainTransportHook registers a ChainableRoundTripperFunc "after" the cache and authentication
310+
// transports in the chain.
311+
func WithCustomCAPostChainTransportHook(caBundle []byte) ClientOption {
312+
// Don't allow an empty value
313+
if len(caBundle) == 0 {
314+
return optionError(fmt.Errorf("caBundle cannot be empty: %w", ErrInvalidClientOptions))
315+
}
316+
317+
return buildCommonOption(CommonClientOptions{CABundle: caBundle, PostChainTransportHook: caCustomTransport(caBundle)})
318+
}
319+
320+
func caCustomTransport(caBundle []byte) ChainableRoundTripperFunc {
321+
return func(_ http.RoundTripper) http.RoundTripper {
322+
// discard error, as we're only using it to check if rootCA is empty
323+
rootCAs, _ := x509.SystemCertPool()
324+
if rootCAs == nil {
325+
rootCAs = x509.NewCertPool()
326+
}
327+
328+
rootCAs.AppendCertsFromPEM(caBundle)
329+
330+
return &http.Transport{
331+
TLSClientConfig: &tls.Config{
332+
RootCAs: rootCAs,
333+
},
334+
}
335+
}
336+
}

gitprovider/client_options_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package gitprovider
1818

1919
import (
2020
"net/http"
21+
"os"
2122
"reflect"
2223
"testing"
2324

@@ -212,6 +213,10 @@ func Test_clientOptions_getTransportChain(t *testing.T) {
212213
}
213214

214215
func Test_makeCientOptions(t *testing.T) {
216+
ca, err := os.ReadFile("./testdata/ca.pem")
217+
if err != nil {
218+
t.Fatal(err)
219+
}
215220
tests := []struct {
216221
name string
217222
opts []ClientOption
@@ -257,6 +262,16 @@ func Test_makeCientOptions(t *testing.T) {
257262
opts: []ClientOption{WithPostChainTransportHook(nil)},
258263
expectedErrs: []error{ErrInvalidClientOptions},
259264
},
265+
{
266+
name: "WithCustomCAPostChainTransportHook",
267+
opts: []ClientOption{WithCustomCAPostChainTransportHook(ca)},
268+
want: buildCommonOption(CommonClientOptions{CABundle: ca, PostChainTransportHook: caCustomTransport(ca)}),
269+
},
270+
{
271+
name: "WithCustomCAPostChainTransportHook, nil",
272+
opts: []ClientOption{WithCustomCAPostChainTransportHook(nil)},
273+
expectedErrs: []error{ErrInvalidClientOptions},
274+
},
260275
{
261276
name: "WithOAuth2Token",
262277
opts: []ClientOption{WithOAuth2Token("foo")},

gitprovider/testdata/ca.pem

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIBhzCCAS2gAwIBAgIUdsAtiX3gN0uk7ddxASWYE/tdv0wwCgYIKoZIzj0EAwIw
3+
GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMjUw
4+
NDE2MDgxODAwWjAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49
5+
AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr
6+
RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxijUzBRMA4GA1UdDwEB/wQE
7+
AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGyUiU1QEZiMAqjsnIYTwZ
8+
4yp5wzAPBgNVHREECDAGhwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIQDzdtvKdE8O
9+
1+WRTZ9MuSiFYcrEz7Zne7VXouDEKqKEigIgM4WlbDeuNCKbqhqj+xZV0pa3rweb
10+
OD8EjjCMY69RMO0=
11+
-----END CERTIFICATE-----

stash/auth.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@ func NewStashClient(username, token string, optFns ...gitprovider.ClientOption)
5858
return nil, err
5959
}
6060

61-
st, err := NewClient(client, host, nil, logger, WithAuth(username, token))
61+
var stashClient *Client
62+
if len(opts.CABundle) != 0 {
63+
stashClient, err = NewClient(client, host, nil, logger, WithAuth(username, token), WithCABundle(opts.CABundle))
64+
} else {
65+
stashClient, err = NewClient(client, host, nil, logger, WithAuth(username, token))
66+
}
67+
6268
if err != nil {
6369
return nil, err
6470
}
@@ -69,5 +75,5 @@ func NewStashClient(username, token string, optFns ...gitprovider.ClientOption)
6975
destructiveActions = *opts.EnableDestructiveAPICalls
7076
}
7177

72-
return newClient(st, host, token, destructiveActions, logger), nil
78+
return newClient(stashClient, host, token, destructiveActions, logger), nil
7379
}

stash/client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ type Client struct {
100100
username string
101101
// Token used to make authenticated API calls.
102102
token string
103+
// caBundle is the CA bundle used to authenticate the server.
104+
caBundle []byte
103105

104106
// Services are used to communicate with the different stash endpoints.
105107
Users Users
@@ -119,6 +121,18 @@ type RateLimiter interface {
119121
Wait(context.Context) error
120122
}
121123

124+
// WithCABundle is used to setup the client authentication.
125+
func WithCABundle(caBundle []byte) ClientOptionsFunc {
126+
return func(c *Client) error {
127+
if len(caBundle) == 0 {
128+
return errors.New("caBundle cannot be empty")
129+
}
130+
131+
c.caBundle = caBundle
132+
return nil
133+
}
134+
}
135+
122136
// WithAuth is used to setup the client authentication.
123137
func WithAuth(username string, token string) ClientOptionsFunc {
124138
return func(c *Client) error {

stash/git.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,9 @@ func (s *GitService) CloneRepository(ctx context.Context, URL string) (r *git.Re
317317
}
318318

319319
r, err = git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
320-
URL: URL,
321-
Auth: &githttp.BasicAuth{Username: s.Client.username, Password: s.Client.token},
320+
URL: URL,
321+
Auth: &githttp.BasicAuth{Username: s.Client.username, Password: s.Client.token},
322+
CABundle: s.Client.caBundle,
322323
})
323324
if err != nil {
324325
return nil, "", fmt.Errorf("failed to clone repository: %v", err)
@@ -327,6 +328,7 @@ func (s *GitService) CloneRepository(ctx context.Context, URL string) (r *git.Re
327328
err = r.Fetch(&git.FetchOptions{
328329
RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"},
329330
Auth: &githttp.BasicAuth{Username: s.Client.username, Password: s.Client.token},
331+
CABundle: s.Client.caBundle,
330332
})
331333

332334
if err != nil {
@@ -538,6 +540,7 @@ func (s *GitService) Push(ctx context.Context, r *git.Repository) error {
538540
options := &git.PushOptions{
539541
RemoteName: "origin",
540542
Auth: &githttp.BasicAuth{Username: s.Client.username, Password: s.Client.token},
543+
CABundle: s.Client.caBundle,
541544
}
542545

543546
err := r.PushContext(ctx, options)

0 commit comments

Comments
 (0)