Skip to content

Commit 5fbdfb0

Browse files
author
Miguel Martínez Triviño
authored
feat: validate credentials (#146)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent 1b354c0 commit 5fbdfb0

File tree

5 files changed

+185
-34
lines changed

5 files changed

+185
-34
lines changed

cmd/chart.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,12 @@ func moveChart(cmd *cobra.Command, args []string) error {
113113
Chart: mover.ChartSpec{},
114114
ImageHintsFile: imagePatternsFile,
115115
// Use local keychain for authentication
116-
Containers: mover.Containers{UseDefaultLocalKeychain: true},
116+
ContainersAuth: &mover.ContainersAuth{UseDefaultLocalKeychain: true},
117117
},
118118
Target: mover.Target{
119-
Chart: mover.ChartSpec{},
120-
Rules: *targetRewriteRules,
121-
Containers: mover.Containers{UseDefaultLocalKeychain: true},
119+
Chart: mover.ChartSpec{},
120+
Rules: *targetRewriteRules,
121+
ContainersAuth: &mover.ContainersAuth{UseDefaultLocalKeychain: true},
122122
},
123123
}
124124

pkg/mover/auth.go

+39-8
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@
33

44
package mover
55

6-
import "github.com/google/go-containerregistry/pkg/authn"
6+
import (
7+
"errors"
8+
"fmt"
9+
"net/url"
10+
11+
"github.com/google/go-containerregistry/pkg/authn"
12+
)
713

814
// Resolve implements an authn.KeyChain
915
//
1016
// See https://pkg.go.dev/github.com/google/go-containerregistry/pkg/authn#Keychain
1117
//
1218
// Returns a custom credentials authn.Authenticator if the given resource
1319
// RegistryStr() matches the Repository, otherwise it returns annonymous access
14-
func (repo ContainerRepository) Resolve(resource authn.Resource) (authn.Authenticator, error) {
20+
func (repo *OCICredentials) Resolve(resource authn.Resource) (authn.Authenticator, error) {
1521
if repo.Server == resource.RegistryStr() {
1622
return repo, nil
1723
}
@@ -25,16 +31,41 @@ func (repo ContainerRepository) Resolve(resource authn.Resource) (authn.Authenti
2531
// See https://pkg.go.dev/github.com/google/go-containerregistry/pkg/authn#Authenticator
2632
//
2733
// Returns an authn.AuthConfig with a user / password pair to be used for authentication
28-
func (repo ContainerRepository) Authorization() (*authn.AuthConfig, error) {
34+
func (repo *OCICredentials) Authorization() (*authn.AuthConfig, error) {
2935
return &authn.AuthConfig{Username: repo.Username, Password: repo.Password}, nil
3036
}
3137

32-
// Define a container images keychain based on the settings provided via the containers struct
33-
// If useDefaultKeychain is set, use config/docker.json otherwise load the provided creds (if any)
34-
func getContainersKeychain(c Containers) authn.Keychain {
38+
// Define a container registry keychain based on the settings provided in containers Auth
39+
// If useDefaultKeychain is set, use config/docker.json otherwise it will load the provided credentials (if any)
40+
func getContainersKeychain(c *ContainersAuth) (authn.Keychain, error) {
41+
// No credentials provided
42+
if !c.UseDefaultLocalKeychain && c.Credentials == nil {
43+
return nil, errors.New("either local keychain or explicit credentials are required")
44+
}
45+
46+
if c.UseDefaultLocalKeychain && c.Credentials != nil {
47+
return nil, errors.New("you can use either local keychain or explicit credentials not both")
48+
}
49+
3550
if c.UseDefaultLocalKeychain {
36-
return authn.DefaultKeychain
51+
return authn.DefaultKeychain, nil
52+
}
53+
54+
return validateOCICredentials(c.Credentials)
55+
}
56+
57+
// validate if the provided OCI credentials are valid
58+
// They include a username, password and a valid (RFC 3986 URI authority) serverName
59+
func validateOCICredentials(c *OCICredentials) (authn.Keychain, error) {
60+
if c.Username == "" || c.Password == "" || c.Server == "" {
61+
return nil, errors.New("OCI credentials require an username, password and a server name")
62+
}
63+
64+
// See https://github.com/google/go-containerregistry/blob/main/pkg/name/registry.go#L97
65+
// Valid RFC 3986 URI authority
66+
if uri, err := url.Parse("//" + c.Server); err != nil || uri.Host != c.Server {
67+
return nil, fmt.Errorf("credentials server name %q must not contain a scheme (\"http://\") nor a path. Example of valid registry names are \"myregistry.io\" or \"myregistry.io:9999\"", c.Server)
3768
}
3869

39-
return c.ContainerRepository
70+
return c, nil
4071
}

pkg/mover/auth_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2022 VMware, Inc.
2+
// SPDX-License-Identifier: BSD-2-Clause
3+
4+
package mover
5+
6+
import (
7+
"testing"
8+
9+
"github.com/google/go-containerregistry/pkg/authn"
10+
)
11+
12+
func TestGetContainersKeychain(t *testing.T) {
13+
explicitCreds := &OCICredentials{Username: "user", Password: "pass", Server: "server.io"}
14+
15+
tests := []struct {
16+
name string
17+
auth *ContainersAuth
18+
want authn.Keychain
19+
wantError bool // if the function should return an error
20+
}{
21+
// Valid triplets
22+
{name: "neither local nor provided credentials", auth: &ContainersAuth{}, wantError: true},
23+
{name: "both local and provided credentials",
24+
auth: &ContainersAuth{
25+
UseDefaultLocalKeychain: true,
26+
Credentials: explicitCreds},
27+
wantError: true},
28+
{name: "localKeychain", auth: &ContainersAuth{UseDefaultLocalKeychain: true}, want: authn.DefaultKeychain},
29+
{
30+
name: "valid explicit creds",
31+
auth: &ContainersAuth{
32+
Credentials: explicitCreds,
33+
},
34+
want: explicitCreds,
35+
},
36+
{name: "invalid provided credentials",
37+
auth: &ContainersAuth{
38+
Credentials: &OCICredentials{Username: "user", Password: "pass", Server: "https://server.io"}},
39+
wantError: true},
40+
}
41+
42+
for _, test := range tests {
43+
t.Run(test.name, func(t *testing.T) {
44+
got, gotError := getContainersKeychain(test.auth)
45+
if gotError == nil == test.wantError {
46+
t.Errorf("expected error %t, got error %q. auth=%v", test.wantError, gotError, test.auth)
47+
}
48+
49+
if got != test.want {
50+
t.Errorf("got=%v, want=%v", got, test.want)
51+
}
52+
})
53+
}
54+
}
55+
56+
func TestValidateOCICredentials(t *testing.T) {
57+
var tests = []struct {
58+
username string
59+
password string
60+
server string
61+
wantError bool // if the function should return an error
62+
}{
63+
// Valid triplets
64+
{"username", "password", "server.io", false},
65+
{"username", "password", "server.io:9999", false},
66+
67+
// Missing one of the three items
68+
{"", "password", "server.io", true},
69+
{"username", "", "server.io", true},
70+
{"username", "password", "", true},
71+
72+
// Invalid serverName
73+
{"username", "password", "http://server.io", true},
74+
{"username", "password", "//server.io", true},
75+
{"username", "password", "server.io/baz", true},
76+
}
77+
78+
for _, test := range tests {
79+
_, gotError := validateOCICredentials(&OCICredentials{test.server, test.username, test.password})
80+
if gotError == nil == test.wantError {
81+
t.Errorf("expected error %t, got error %q. username=%q, pass=%q, server=%q", test.wantError, gotError, test.username, test.password, test.server)
82+
}
83+
}
84+
}

pkg/mover/chart.go

+45-13
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"path/filepath"
1212
"regexp"
1313

14+
"github.com/google/go-containerregistry/pkg/authn"
15+
1416
"github.com/avast/retry-go"
1517
"github.com/google/go-containerregistry/pkg/name"
1618
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -94,15 +96,15 @@ type LocalChart struct {
9496
// the hints file and container images
9597
type IntermediateBundle LocalChart
9698

97-
// ContainerRepository defines a private repo name and credentials
98-
type ContainerRepository struct {
99+
// OCICredentials defines a private repo name and credentials
100+
type OCICredentials struct {
99101
Server string
100102
Username, Password string
101103
}
102104

103-
// Containers is the section for private repository definition
104-
type Containers struct {
105-
ContainerRepository
105+
// ContainersAuth is the section for private repository credentials definition
106+
type ContainersAuth struct {
107+
Credentials *OCICredentials
106108
// Use local keychain in the system (config/docker.json)
107109
// This is useful to offer a CLI experience similar to docker
108110
UseDefaultLocalKeychain bool
@@ -118,14 +120,14 @@ type ChartSpec struct {
118120
type Source struct {
119121
Chart ChartSpec
120122
ImageHintsFile string
121-
Containers Containers
123+
ContainersAuth *ContainersAuth
122124
}
123125

124126
// Target of the chart move
125127
type Target struct {
126-
Chart ChartSpec
127-
Rules RewriteRules
128-
Containers Containers
128+
Chart ChartSpec
129+
Rules RewriteRules
130+
ContainersAuth *ContainersAuth
129131
}
130132

131133
// ChartMoveRequest defines a chart move
@@ -155,10 +157,12 @@ type ChartMover struct {
155157
// imagePatters and rules.
156158
func NewChartMover(req *ChartMoveRequest, opts ...Option) (*ChartMover, error) {
157159
cm := &ChartMover{
158-
logger: defaultLogger{},
159-
retries: DefaultRetries,
160-
sourceContainerRegistry: internal.NewContainerRegistryClient(getContainersKeychain(req.Source.Containers)),
161-
targetContainerRegistry: internal.NewContainerRegistryClient(getContainersKeychain(req.Target.Containers)),
160+
logger: defaultLogger{},
161+
retries: DefaultRetries,
162+
}
163+
164+
if err := initializeContainersAuth(req, cm); err != nil {
165+
return nil, err
162166
}
163167

164168
if err := validateTarget(&req.Target); err != nil {
@@ -727,3 +731,31 @@ func notNilData(data []byte, err error) ([]byte, error) {
727731
}
728732
return data, err
729733
}
734+
735+
// Initialize the ChartMover OCI credentials based on the provided request
736+
func initializeContainersAuth(req *ChartMoveRequest, cm *ChartMover) error {
737+
var err error
738+
if cm.sourceContainerRegistry, err = newContainerRegistryClient(req.Source.ContainersAuth); err != nil {
739+
return err
740+
}
741+
742+
if cm.targetContainerRegistry, err = newContainerRegistryClient(req.Target.ContainersAuth); err != nil {
743+
return err
744+
}
745+
746+
return nil
747+
}
748+
749+
// Return a private registry keychain or one for anonymous access
750+
func newContainerRegistryClient(auth *ContainersAuth) (*internal.ContainerRegistryClient, error) {
751+
var keychain authn.Keychain
752+
var err error
753+
754+
if auth != nil {
755+
if keychain, err = getContainersKeychain(auth); err != nil {
756+
return nil, err
757+
}
758+
}
759+
760+
return internal.NewContainerRegistryClient(keychain), nil
761+
}

pkg/mover/registry_test.go

+13-9
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ func NewMoveRequest(chartPath, hints, target, targetRegistry, targetPrefix strin
7878
}
7979

8080
if useLocalKeychain {
81-
req.Source.Containers = mover.Containers{UseDefaultLocalKeychain: true}
82-
req.Target.Containers = mover.Containers{UseDefaultLocalKeychain: true}
81+
req.Source.ContainersAuth = &mover.ContainersAuth{UseDefaultLocalKeychain: true}
82+
req.Target.ContainersAuth = &mover.ContainersAuth{UseDefaultLocalKeychain: true}
8383
}
8484

8585
return req
@@ -92,7 +92,7 @@ func NewSaveRequest(chartPath, hints, bundle string) *mover.ChartMoveRequest {
9292
Chart: mover.ChartSpec{Local: &mover.LocalChart{Path: chartPath}},
9393
// path to file containing rules such as // {{.image.registry}}:{{.image.tag}}
9494
ImageHintsFile: hints,
95-
Containers: mover.Containers{UseDefaultLocalKeychain: true},
95+
ContainersAuth: &mover.ContainersAuth{UseDefaultLocalKeychain: true},
9696
},
9797
Target: mover.Target{
9898
Chart: mover.ChartSpec{IntermediateBundle: &mover.IntermediateBundle{Path: bundle}},
@@ -107,8 +107,8 @@ func NewLoadRequest(bundle, target, targetRegistry, targetPrefix string) *mover.
107107
Chart: mover.ChartSpec{IntermediateBundle: &mover.IntermediateBundle{Path: bundle}},
108108
},
109109
Target: mover.Target{
110-
Chart: mover.ChartSpec{Local: &mover.LocalChart{Path: target}},
111-
Containers: mover.Containers{UseDefaultLocalKeychain: true},
110+
Chart: mover.ChartSpec{Local: &mover.LocalChart{Path: target}},
111+
ContainersAuth: &mover.ContainersAuth{UseDefaultLocalKeychain: true},
112112
// Where to push and how to rewrite the found images
113113
// i.e docker.io/bitnami/mariadb => myregistry.com/myteam/mariadb
114114
Rules: mover.RewriteRules{
@@ -119,8 +119,8 @@ func NewLoadRequest(bundle, target, targetRegistry, targetPrefix string) *mover.
119119
}
120120
}
121121

122-
func repo(domain, user, passwd string) mover.ContainerRepository {
123-
return mover.ContainerRepository{
122+
func repo(domain, user, passwd string) *mover.OCICredentials {
123+
return &mover.OCICredentials{
124124
Server: domain,
125125
Username: user,
126126
Password: passwd,
@@ -166,7 +166,9 @@ func TestRegistryCustomCredentials(t *testing.T) {
166166
prepareDockerCA(t, params.certFile)
167167
dockerLogout(t, params.domain)
168168
req := NewMoveRequest(TestChart, Hints, Target, params.domain, Prefix, false)
169-
req.Target.Containers.ContainerRepository = repo(params.domain, params.user, params.passwd)
169+
req.Target.ContainersAuth = &mover.ContainersAuth{
170+
Credentials: repo(params.domain, params.user, params.passwd),
171+
}
170172
got := relok8s(t, req)
171173
var want error
172174
if got != want {
@@ -193,7 +195,9 @@ func TestRegistryBadCustomCredentials(t *testing.T) {
193195
prepareDockerCA(t, params.certFile)
194196
dockerLogout(t, params.domain)
195197
req := NewMoveRequest(TestChart, Hints, Target, params.domain, Prefix, false)
196-
req.Target.Containers.ContainerRepository = repo(params.domain, BadUser, BadPasswd)
198+
req.Target.ContainersAuth = &mover.ContainersAuth{
199+
Credentials: repo(params.domain, BadUser, BadPasswd),
200+
}
197201
got := relok8s(t, req)
198202
// retry.Error is incompatible with errors package, it cannot be unwrapped
199203
_, ok := got.(retry.Error)

0 commit comments

Comments
 (0)