-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for custom registries, registry auth #72
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package oci | ||
|
||
import ( | ||
"context" | ||
"encoding/base64" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
) | ||
|
||
type Authorizer interface { | ||
AuthorizeOCIRequest(context.Context, Reference, *http.Request) error | ||
} | ||
|
||
type AuthorizeFunc func(context.Context, Reference, *http.Request) error | ||
|
||
func (f AuthorizeFunc) AuthorizeOCIRequest(ctx context.Context, image Reference, req *http.Request) error { | ||
return f(ctx, image, req) | ||
} | ||
|
||
type AuthorizerToken string | ||
|
||
func (s AuthorizerToken) AuthorizeOCIRequest(ctx context.Context, image Reference, req *http.Request) error { | ||
req.Header.Set("Authorization", "Bearer "+string(s)) | ||
return nil | ||
} | ||
|
||
// See: https://github.com/docker/docker/pull/12009. | ||
// See: https://kubernetes.io/docs/concepts/containers/images/#config-json. | ||
// See: https://github.com/kubernetes/kubernetes/blob/1a9feed0cd89f3299ddb6f5eaa5663496c59342c/pkg/credentialprovider/config.go. | ||
// See: https://github.com/kubernetes/kubernetes/blob/1a9feed0cd89f3299ddb6f5eaa5663496c59342c/pkg/credentialprovider/keyring_test.go#L26. | ||
type DockerConfig struct { | ||
Auths map[string]DockerConfigEntry `json:"auths"` | ||
HttpHeaders map[string]string `json:"HttpHeaders,omitempty"` | ||
} | ||
|
||
type DockerConfigEntry struct { | ||
Username string `json:"username"` | ||
Password string `json:"password"` | ||
Email string `json:"email"` | ||
} | ||
|
||
type DockerConfigAuthorizer struct { | ||
Config *DockerConfig | ||
Fallback Authorizer | ||
} | ||
|
||
func (a *DockerConfigAuthorizer) AuthorizeOCIRequest(ctx context.Context, image Reference, req *http.Request) error { | ||
if a.Config != nil { | ||
for k, v := range a.Config.HttpHeaders { | ||
req.Header.Set(k, v) | ||
} | ||
|
||
matchedPattern := a.Match(req.URL) | ||
if matchedPattern != "" { | ||
config := a.Config.Auths[matchedPattern] | ||
|
||
credentials := base64.StdEncoding.EncodeToString([]byte(config.Username + ":" + config.Password)) | ||
req.Header.Set("Authorization", "Basic "+credentials) | ||
return nil | ||
} | ||
} | ||
|
||
if a.Fallback != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bit awkward, have a generic keychain like implementation instead, supporting multiple ways of authentication simultaneously? |
||
return a.Fallback.AuthorizeOCIRequest(ctx, image, req) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (a *DockerConfigAuthorizer) Match(url *url.URL) string { | ||
// TODO: This code is not especially nice or efficient. For example, it parses | ||
// the URLs every iteration | ||
for pattern := range a.Config.Auths { | ||
// Compare scheme, if set in pattern - otherwise allow any scheme | ||
if !strings.HasPrefix(pattern, "https://") && !strings.HasPrefix(pattern, "http://") { | ||
pattern = url.Scheme + "://" + pattern | ||
} | ||
|
||
u, err := url.Parse(pattern) | ||
if err != nil { | ||
continue | ||
} | ||
|
||
if url.Scheme != u.Scheme { | ||
continue | ||
} | ||
|
||
// The Docker client matches either the image or hostname, where the API can | ||
// have a /v2/ or /v1/ prefix | ||
if strings.HasPrefix(u.Path, "/v2/") || strings.HasPrefix(u.Path, "/v1/") { | ||
u.Path = u.Path[3:] | ||
} | ||
|
||
// If the pattern has a path specified, match it | ||
if u.Path != "" && u.Path != "/" { | ||
p := url.Path | ||
if !strings.HasSuffix(p, "/") { | ||
p += "/" | ||
} | ||
|
||
// Make sure paths in patterns match full segments | ||
if !strings.HasSuffix(u.Path, "/") { | ||
u.Path += "/" | ||
} | ||
|
||
if !strings.HasPrefix(p, u.Path) { | ||
continue | ||
} | ||
} | ||
|
||
urlParts := strings.Split(url.Host, ".") | ||
patternParts := strings.Split(u.Host, ".") | ||
|
||
// The pattern has more parts of its hostname than the URL | ||
if len(urlParts) < len(patternParts) { | ||
continue | ||
} | ||
|
||
matched := true | ||
for i := 0; i < len(patternParts); i++ { | ||
if strings.HasPrefix(patternParts[i], "*") { | ||
if !strings.HasSuffix(urlParts[i], patternParts[i][1:]) { | ||
matched = false | ||
break | ||
} | ||
} else if strings.HasSuffix(patternParts[i], "*") { | ||
// TODO: | ||
} else if urlParts[i] != patternParts[i] { | ||
matched = false | ||
break | ||
} | ||
} | ||
|
||
if matched { | ||
return pattern | ||
} | ||
} | ||
|
||
return "" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
package oci | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add tests for the headers being set as well. |
||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestDockerConfigAuthorizer_MatchPattern(t *testing.T) { | ||
testCases := []struct { | ||
Pattern string | ||
URL string | ||
Expected bool | ||
}{ | ||
// From: https://kubernetes.io/docs/concepts/containers/images/#config-json | ||
{ | ||
Pattern: "*.kubernetes.io", | ||
URL: "abc.kubernetes.io", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "*.kubernetes.io", | ||
URL: "kubernetes.io", | ||
Expected: false, | ||
}, | ||
{ | ||
Pattern: "*.*.kubernetes.io", | ||
URL: "abc.kubernetes.io", | ||
Expected: false, | ||
}, | ||
{ | ||
Pattern: "*.*.kubernetes.io", | ||
URL: "abc.def.kubernetes.io", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "prefix.*.io", | ||
URL: "prefix.kubernetes.io", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "*-good.kubernetes.io", | ||
URL: "prefix-good.kubernetes.io", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "my-registry.io/images", | ||
URL: "my-registry.io/images", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "my-registry.io/images", | ||
URL: "my-registry.io/images/my-image", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "my-registry.io/images", | ||
URL: "my-registry.io/images/another-image", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "*.my-registry.io/images", | ||
URL: "sub.my-registry.io/images/my-image", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "*.my-registry.io/images", | ||
URL: "a.sub.my-registry.io/images/my-image", | ||
Expected: false, | ||
}, | ||
{ | ||
Pattern: "*.my-registry.io/images", | ||
URL: "a.b.sub.my-registry.io/images/my-image", | ||
Expected: false, | ||
}, | ||
{ | ||
Pattern: "my-registry.io/images", | ||
URL: "a.sub.my-registry.io/images/my-image", | ||
Expected: false, | ||
}, | ||
{ | ||
Pattern: "my-registry.io/images", | ||
URL: "a.b.sub.my-registry.io/images/my-image", | ||
Expected: false, | ||
}, | ||
// HTTP / HTTPS | ||
{ | ||
Pattern: "https://example.com", | ||
URL: "https://example.com/images", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "https://example.com", | ||
URL: "http://example.com/images", | ||
Expected: false, | ||
}, | ||
{ | ||
Pattern: "example.com", | ||
URL: "https://example.com/images", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "example.com", | ||
URL: "http://example.com/images", | ||
Expected: true, | ||
}, | ||
// IP / port | ||
{ | ||
Pattern: "example.com:8080", | ||
URL: "example.com:8080/alpine", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "192.168.1.100:8080", | ||
URL: "192.168.1.100:8080/alpine", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "192.168.1.100", | ||
URL: "192.168.1.100/alpine", | ||
Expected: true, | ||
}, | ||
{ | ||
Pattern: "example.com", | ||
URL: "example.com:8080/alpine", | ||
Expected: false, | ||
}, | ||
{ | ||
Pattern: "192.168.1.100", | ||
URL: "192.168.1.100:8080/alpine", | ||
Expected: false, | ||
}, | ||
} | ||
|
||
for _, testCase := range testCases { | ||
t.Run(fmt.Sprintf("%s->%s", testCase.Pattern, testCase.URL), func(t *testing.T) { | ||
a := &DockerConfigAuthorizer{ | ||
Config: &DockerConfig{ | ||
Auths: map[string]DockerConfigEntry{ | ||
testCase.Pattern: {}, | ||
}, | ||
}, | ||
} | ||
|
||
if !strings.HasPrefix(testCase.URL, "https://") && !strings.HasPrefix(testCase.URL, "http://") { | ||
testCase.URL = "https://" + testCase.URL | ||
} | ||
|
||
u, err := url.Parse(testCase.URL) | ||
require.NoError(t, err) | ||
|
||
actual := a.Match(u) | ||
assert.Equal(t, testCase.Expected, actual != "") | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rewrite? Take inspiration from Kubernetes' "keychain" abstraction, which keeps tokens around as well - solving #32?