-
Notifications
You must be signed in to change notification settings - Fork 395
Add authentication package to containers/image/pkg #333
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
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 |
|---|---|---|
|
|
@@ -3,7 +3,6 @@ package docker | |
| import ( | ||
| "context" | ||
| "crypto/tls" | ||
| "encoding/base64" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
|
|
@@ -15,25 +14,19 @@ import ( | |
| "time" | ||
|
|
||
| "github.com/containers/image/docker/reference" | ||
| "github.com/containers/image/pkg/docker/config" | ||
| "github.com/containers/image/pkg/tlsclientconfig" | ||
| "github.com/containers/image/types" | ||
| "github.com/containers/storage/pkg/homedir" | ||
| "github.com/docker/distribution/registry/client" | ||
| helperclient "github.com/docker/docker-credential-helpers/client" | ||
| "github.com/docker/go-connections/tlsconfig" | ||
| "github.com/opencontainers/go-digest" | ||
| "github.com/pkg/errors" | ||
| "github.com/sirupsen/logrus" | ||
| ) | ||
|
|
||
| const ( | ||
| dockerHostname = "docker.io" | ||
| dockerRegistry = "registry-1.docker.io" | ||
| dockerAuthRegistry = "https://index.docker.io/v1/" | ||
|
|
||
| dockerCfg = ".docker" | ||
| dockerCfgFileName = "config.json" | ||
| dockerCfgObsolete = ".dockercfg" | ||
| dockerHostname = "docker.io" | ||
| dockerRegistry = "registry-1.docker.io" | ||
|
|
||
| systemPerHostCertDirPath = "/etc/docker/certs.d" | ||
|
|
||
|
|
@@ -51,9 +44,13 @@ const ( | |
| extensionSignatureTypeAtomic = "atomic" // extensionSignature.Type | ||
| ) | ||
|
|
||
| // ErrV1NotSupported is returned when we're trying to talk to a | ||
| // docker V1 registry. | ||
| var ErrV1NotSupported = errors.New("can't talk to a V1 docker registry") | ||
| var ( | ||
| // ErrV1NotSupported is returned when we're trying to talk to a | ||
| // docker V1 registry. | ||
| ErrV1NotSupported = errors.New("can't talk to a V1 docker registry") | ||
| // ErrUnauthorizedForCredentials is returned when the status code returned is 401 | ||
| ErrUnauthorizedForCredentials = errors.New("unable to retrieve auth token: invalid username/password") | ||
| ) | ||
|
|
||
| // extensionSignature and extensionSignatureList come from github.com/openshift/origin/pkg/dockerregistry/server/signaturedispatcher.go: | ||
| // signature represents a Docker image signature. | ||
|
|
@@ -128,52 +125,147 @@ func dockerCertDir(ctx *types.SystemContext, hostPort string) string { | |
| return filepath.Join(hostCertDir, hostPort) | ||
| } | ||
|
|
||
| // newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry) | ||
| func setupCertificates(dir string, tlsc *tls.Config) error { | ||
| logrus.Debugf("Looking for TLS certificates and private keys in %s", dir) | ||
| fs, err := ioutil.ReadDir(dir) | ||
| if err != nil { | ||
| if os.IsNotExist(err) { | ||
| return nil | ||
| } | ||
| if os.IsPermission(err) { | ||
| logrus.Debugf("Skipping scan of %s due to permission error: %v", dir, err) | ||
| return nil | ||
| } | ||
| return err | ||
| } | ||
|
|
||
| for _, f := range fs { | ||
| fullPath := filepath.Join(dir, f.Name()) | ||
| if strings.HasSuffix(f.Name(), ".crt") { | ||
| systemPool, err := tlsconfig.SystemCertPool() | ||
| if err != nil { | ||
| return errors.Wrap(err, "unable to get system cert pool") | ||
| } | ||
| tlsc.RootCAs = systemPool | ||
| logrus.Debugf(" crt: %s", fullPath) | ||
| data, err := ioutil.ReadFile(fullPath) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| tlsc.RootCAs.AppendCertsFromPEM(data) | ||
| } | ||
| if strings.HasSuffix(f.Name(), ".cert") { | ||
| certName := f.Name() | ||
| keyName := certName[:len(certName)-5] + ".key" | ||
| logrus.Debugf(" cert: %s", fullPath) | ||
| if !hasFile(fs, keyName) { | ||
| return errors.Errorf("missing key %s for client certificate %s. Note that CA certificates should use the extension .crt", keyName, certName) | ||
| } | ||
| cert, err := tls.LoadX509KeyPair(filepath.Join(dir, certName), filepath.Join(dir, keyName)) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| tlsc.Certificates = append(tlsc.Certificates, cert) | ||
| } | ||
| if strings.HasSuffix(f.Name(), ".key") { | ||
| keyName := f.Name() | ||
| certName := keyName[:len(keyName)-4] + ".cert" | ||
| logrus.Debugf(" key: %s", fullPath) | ||
| if !hasFile(fs, certName) { | ||
| return errors.Errorf("missing client certificate %s for key %s", certName, keyName) | ||
| } | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func hasFile(files []os.FileInfo, name string) bool { | ||
| for _, f := range files { | ||
| if f.Name() == name { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // newDockerClientFromRef returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry) | ||
| // “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection) | ||
| func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) { | ||
| func newDockerClientFromRef(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) { | ||
| registry := reference.Domain(ref.ref) | ||
| if registry == dockerHostname { | ||
| registry = dockerRegistry | ||
| username, password, err := config.GetAuthentication(ctx, reference.Domain(ref.ref)) | ||
| if err != nil { | ||
| return nil, errors.Wrapf(err, "error getting username and password") | ||
| } | ||
| username, password, err := getAuth(ctx, reference.Domain(ref.ref)) | ||
| sigBase, err := configuredSignatureStorageBase(ctx, ref, write) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| remoteName := reference.Path(ref.ref) | ||
|
|
||
| return newDockerClientWithDetails(ctx, registry, username, password, actions, sigBase, remoteName) | ||
| } | ||
|
|
||
| // newDockerClientWithDetails returns a new dockerClient instance for the given parameters | ||
| func newDockerClientWithDetails(ctx *types.SystemContext, registry, username, password, actions string, sigBase signatureStorageBase, remoteName string) (*dockerClient, error) { | ||
| hostName := registry | ||
| if registry == dockerHostname { | ||
| registry = dockerRegistry | ||
| } | ||
| tr := tlsclientconfig.NewTransport() | ||
| tr.TLSClientConfig = serverDefault() | ||
|
|
||
| // It is undefined whether the host[:port] string for dockerHostname should be dockerHostname or dockerRegistry, | ||
| // because docker/docker does not read the certs.d subdirectory at all in that case. We use the user-visible | ||
| // dockerHostname here, because it is more symmetrical to read the configuration in that case as well, and because | ||
| // generally the UI hides the existence of the different dockerRegistry. But note that this behavior is | ||
| // undocumented and may change if docker/docker changes. | ||
| certDir := dockerCertDir(ctx, reference.Domain(ref.ref)) | ||
| certDir := dockerCertDir(ctx, hostName) | ||
| if err := tlsclientconfig.SetupCertificates(certDir, tr.TLSClientConfig); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if ctx != nil && ctx.DockerInsecureSkipTLSVerify { | ||
| tr.TLSClientConfig.InsecureSkipVerify = true | ||
| } | ||
| client := &http.Client{Transport: tr} | ||
|
|
||
| sigBase, err := configuredSignatureStorageBase(ctx, ref, write) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return &dockerClient{ | ||
| ctx: ctx, | ||
| registry: registry, | ||
| username: username, | ||
| password: password, | ||
| client: client, | ||
| client: &http.Client{Transport: tr}, | ||
| signatureBase: sigBase, | ||
| scope: authScope{ | ||
| actions: actions, | ||
| remoteName: reference.Path(ref.ref), | ||
| remoteName: remoteName, | ||
| }, | ||
| }, nil | ||
| } | ||
|
|
||
| // CheckAuth validates the credentials by attempting to log into the registry | ||
| // returns an error if an error occcured while making the http request or the status code received was 401 | ||
| func CheckAuth(ctx context.Context, sCtx *types.SystemContext, username, password, registry string) error { | ||
| newLoginClient, err := newDockerClientWithDetails(sCtx, registry, username, password, "", nil, "") | ||
| if err != nil { | ||
| return errors.Wrapf(err, "error creating new docker client") | ||
| } | ||
|
|
||
| resp, err := newLoginClient.makeRequest(ctx, "GET", "/v2/", nil, nil) | ||
|
Collaborator
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.
Collaborator
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. Never mind, the above is correct ( |
||
| if err != nil { | ||
| return err | ||
| } | ||
|
Member
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. you forgot a
Member
Author
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. fixed. |
||
| defer resp.Body.Close() | ||
|
|
||
| switch resp.StatusCode { | ||
| case http.StatusOK: | ||
| return nil | ||
| case http.StatusUnauthorized: | ||
| return ErrUnauthorizedForCredentials | ||
| default: | ||
| return errors.Errorf("error occured with status code %q", resp.StatusCode) | ||
| } | ||
| } | ||
|
|
||
| // makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client. | ||
| // The host name and schema is taken from the client or autodetected, and the path is relative to it, i.e. the path usually starts with /v2/. | ||
| func (c *dockerClient) makeRequest(ctx context.Context, method, path string, headers map[string][]string, stream io.Reader) (*http.Response, error) { | ||
|
|
@@ -245,7 +337,10 @@ func (c *dockerClient) setupRequestAuth(req *http.Request) error { | |
| return errors.Errorf("missing realm in bearer auth challenge") | ||
| } | ||
| service, _ := challenge.Parameters["service"] // Will be "" if not present | ||
| scope := fmt.Sprintf("repository:%s:%s", c.scope.remoteName, c.scope.actions) | ||
| var scope string | ||
| if c.scope.remoteName != "" && c.scope.actions != "" { | ||
|
Member
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. why this
Collaborator
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. Because a “log in” action (i.e. only verify that the username/password are valid) don’t really have a scope, and Docker seems to do the same thing.
Member
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. awesome, thx
Member
Author
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. thanks @mtrmac :) |
||
| scope = fmt.Sprintf("repository:%s:%s", c.scope.remoteName, c.scope.actions) | ||
| } | ||
| token, err := c.getBearerToken(req.Context(), realm, service, scope) | ||
| if err != nil { | ||
| return err | ||
|
|
@@ -291,7 +386,7 @@ func (c *dockerClient) getBearerToken(ctx context.Context, realm, service, scope | |
| defer res.Body.Close() | ||
| switch res.StatusCode { | ||
| case http.StatusUnauthorized: | ||
| return nil, errors.Errorf("unable to retrieve auth token: 401 unauthorized") | ||
| return nil, ErrUnauthorizedForCredentials | ||
| case http.StatusOK: | ||
| break | ||
| default: | ||
|
|
@@ -315,65 +410,6 @@ func (c *dockerClient) getBearerToken(ctx context.Context, realm, service, scope | |
| return &token, nil | ||
| } | ||
|
|
||
| func getAuth(ctx *types.SystemContext, registry string) (string, string, error) { | ||
| if ctx != nil && ctx.DockerAuthConfig != nil { | ||
| return ctx.DockerAuthConfig.Username, ctx.DockerAuthConfig.Password, nil | ||
| } | ||
| var dockerAuth dockerConfigFile | ||
| dockerCfgPath := filepath.Join(getDefaultConfigDir(".docker"), dockerCfgFileName) | ||
| if _, err := os.Stat(dockerCfgPath); err == nil { | ||
| j, err := ioutil.ReadFile(dockerCfgPath) | ||
| if err != nil { | ||
| return "", "", err | ||
| } | ||
| if err := json.Unmarshal(j, &dockerAuth); err != nil { | ||
| return "", "", err | ||
| } | ||
|
|
||
| } else if os.IsNotExist(err) { | ||
| // try old config path | ||
| oldDockerCfgPath := filepath.Join(getDefaultConfigDir(dockerCfgObsolete)) | ||
| if _, err := os.Stat(oldDockerCfgPath); err != nil { | ||
| if os.IsNotExist(err) { | ||
| return "", "", nil | ||
| } | ||
| return "", "", errors.Wrap(err, oldDockerCfgPath) | ||
| } | ||
|
|
||
| j, err := ioutil.ReadFile(oldDockerCfgPath) | ||
| if err != nil { | ||
| return "", "", err | ||
| } | ||
| if err := json.Unmarshal(j, &dockerAuth.AuthConfigs); err != nil { | ||
| return "", "", err | ||
| } | ||
|
|
||
| } else if err != nil { | ||
| return "", "", errors.Wrap(err, dockerCfgPath) | ||
| } | ||
|
|
||
| // First try cred helpers. They should always be normalized. | ||
| if ch, exists := dockerAuth.CredHelpers[registry]; exists { | ||
| return getAuthFromCredHelper(ch, registry) | ||
| } | ||
|
|
||
| // I'm feeling lucky. | ||
| if c, exists := dockerAuth.AuthConfigs[registry]; exists { | ||
| return decodeDockerAuth(c.Auth) | ||
| } | ||
|
|
||
| // bad luck; let's normalize the entries first | ||
| registry = normalizeRegistry(registry) | ||
| normalizedAuths := map[string]dockerAuthConfig{} | ||
| for k, v := range dockerAuth.AuthConfigs { | ||
| normalizedAuths[normalizeRegistry(k)] = v | ||
| } | ||
| if c, exists := normalizedAuths[registry]; exists { | ||
| return decodeDockerAuth(c.Auth) | ||
| } | ||
| return "", "", nil | ||
| } | ||
|
|
||
| // detectProperties detects various properties of the registry. | ||
| // See the dockerClient documentation for members which are affected by this. | ||
| func (c *dockerClient) detectProperties(ctx context.Context) error { | ||
|
|
@@ -456,67 +492,3 @@ func (c *dockerClient) getExtensionsSignatures(ctx context.Context, ref dockerRe | |
| } | ||
| return &parsedBody, nil | ||
| } | ||
|
|
||
| func getDefaultConfigDir(confPath string) string { | ||
| return filepath.Join(homedir.Get(), confPath) | ||
| } | ||
|
|
||
| type dockerAuthConfig struct { | ||
| Auth string `json:"auth,omitempty"` | ||
| } | ||
|
|
||
| type dockerConfigFile struct { | ||
| AuthConfigs map[string]dockerAuthConfig `json:"auths"` | ||
| CredHelpers map[string]string `json:"credHelpers,omitempty"` | ||
| } | ||
|
|
||
| func getAuthFromCredHelper(credHelper, registry string) (string, string, error) { | ||
| helperName := fmt.Sprintf("docker-credential-%s", credHelper) | ||
| p := helperclient.NewShellProgramFunc(helperName) | ||
| creds, err := helperclient.Get(p, registry) | ||
| if err != nil { | ||
| return "", "", err | ||
| } | ||
|
|
||
| return creds.Username, creds.Secret, nil | ||
| } | ||
|
|
||
| func decodeDockerAuth(s string) (string, string, error) { | ||
| decoded, err := base64.StdEncoding.DecodeString(s) | ||
| if err != nil { | ||
| return "", "", err | ||
| } | ||
| parts := strings.SplitN(string(decoded), ":", 2) | ||
| if len(parts) != 2 { | ||
| // if it's invalid just skip, as docker does | ||
| return "", "", nil | ||
| } | ||
| user := parts[0] | ||
| password := strings.Trim(parts[1], "\x00") | ||
| return user, password, nil | ||
| } | ||
|
|
||
| // convertToHostname converts a registry url which has http|https prepended | ||
| // to just an hostname. | ||
| // Copied from github.com/docker/docker/registry/auth.go | ||
| func convertToHostname(url string) string { | ||
| stripped := url | ||
| if strings.HasPrefix(url, "http://") { | ||
| stripped = strings.TrimPrefix(url, "http://") | ||
| } else if strings.HasPrefix(url, "https://") { | ||
| stripped = strings.TrimPrefix(url, "https://") | ||
| } | ||
|
|
||
| nameParts := strings.SplitN(stripped, "/", 2) | ||
|
|
||
| return nameParts[0] | ||
| } | ||
|
|
||
| func normalizeRegistry(registry string) string { | ||
| normalized := convertToHostname(registry) | ||
| switch normalized { | ||
| case "registry-1.docker.io", "docker.io": | ||
| return "index.docker.io" | ||
| } | ||
| return normalized | ||
| } | ||
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.
This comment belongs to the
dockerCertDircode, not toconfiguredSignatureStorageBasecode. Please keep it closer.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.
fixed