Module: Docker::API::Auth

Defined in:
lib/docker/api/auth.rb

Overview

Registry credentials, resolved per call.

There is no credential store on this module and no way to set one globally. Credentials are looked up for the registry a particular request is talking to and travel on that request as X-Registry-Auth, so pulling from two registries in one process needs no coordination.

Constant Summary collapse

DOCKER_HUB_KEY =

Docker Hub's canonical auth key, which is not the hostname you pull from.

"https://index.docker.io/v1/"

Class Method Summary collapse

Class Method Details

.encode(credentials) ⇒ String

Encode a credential hash the way the daemon expects it: base64url of a JSON document, in a header.

Encoded with pack rather than with the base64 library, deliberately. base64 stopped being a default gem in Ruby 3.4, so requiring it makes this a gem with a runtime dependency it does not declare -- which fails at require time for anyone whose bundle does not happen to carry it for another reason. pack("m0") is the same transform with no newlines, and tr maps the standard alphabet onto the URL-safe one the daemon expects.

Parameters:

  • credentials (Hash)

    with :username, :password, :serveraddress

Returns:

  • (String)


56
57
58
# File 'lib/docker/api/auth.rb', line 56

def encode(credentials)
  [JSON.generate(credentials)].pack("m0").tr("+/", "-_")
end

.from_auths(config, key) ⇒ Hash?

Parameters:

  • config (Hash)
  • key (String)

Returns:

  • (Hash, nil)


86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/docker/api/auth.rb', line 86

def from_auths(config, key)
  entry = config.dig("auths", key)
  return nil if entry.nil?

  if entry["auth"] && !entry["auth"].empty?
    # unpack1("m") is Base64.decode64: lenient, ignoring newlines and
    # any character outside the alphabet, which is what these values
    # arrive as when a config.json has been hand-edited or line-wrapped.
    username, _, password = entry["auth"].unpack1("m").partition(":")
    return { username: username, password: password, serveraddress: key }
  end

  return nil if entry["username"].nil?

  { username: entry["username"], password: entry["password"], serveraddress: key }
end

.from_helper(config, key) ⇒ Hash?

A helper named for this specific registry wins over the catch-all store, which is how credHelpers is specified to behave.

Parameters:

  • config (Hash)
  • key (String)

Returns:

  • (Hash, nil)


109
110
111
112
113
114
115
116
117
118
# File 'lib/docker/api/auth.rb', line 109

def from_helper(config, key)
  helper = config.dig("credHelpers", key) ||
    config.dig("credHelpers", URI(key).host.to_s) ||
    config["credsStore"]
  return nil if helper.nil? || helper.empty?

  run_helper(helper, key)
rescue URI::InvalidURIError
  nil
end

.read_config(path) ⇒ Hash?

Parameters:

  • path (String, nil)

Returns:

  • (Hash, nil)


74
75
76
77
78
79
80
81
# File 'lib/docker/api/auth.rb', line 74

def read_config(path)
  path ||= File.join(Dir.home, ".docker", "config.json")
  return nil unless File.readable?(path)

  JSON.parse(File.read(path))
rescue JSON::ParserError, SystemCallError, ArgumentError
  nil
end

.registry_key(registry) ⇒ String

Docker Hub is stored under a URL key rather than its hostname, and an empty registry means Hub. Everything else is keyed by hostname.

Parameters:

  • registry (String, nil)

Returns:

  • (String)


65
66
67
68
69
70
# File 'lib/docker/api/auth.rb', line 65

def registry_key(registry)
  return DOCKER_HUB_KEY if registry.nil? || registry.empty?
  return DOCKER_HUB_KEY if ["docker.io", "index.docker.io", "registry-1.docker.io"].include?(registry)

  registry
end

.resolve(registry = nil, config_path: nil) ⇒ String?

Find credentials for a registry and encode them for the daemon.

Every failure mode here is soft. A missing config file, an unreadable one, or a credential helper that is not installed all mean "no credentials", because anonymous pulls of public images must keep working on a machine that has never run docker login.

Parameters:

  • registry (String, nil) (defaults to: nil)

    a registry hostname, or nil for Docker Hub

  • config_path (String, nil) (defaults to: nil)

    path to config.json, for tests

Returns:

  • (String, nil)

    a base64 X-Registry-Auth value, or nil



32
33
34
35
36
37
38
39
40
41
# File 'lib/docker/api/auth.rb', line 32

def resolve(registry = nil, config_path: nil)
  key = registry_key(registry)
  config = read_config(config_path)
  return nil if config.nil?

  credentials = from_helper(config, key) || from_auths(config, key)
  return nil if credentials.nil?

  encode(credentials)
end

.run_helper(helper, key) ⇒ Hash?

Invoke docker-credential-<helper> get, which reads the registry on stdin and writes JSON on stdout.

The command is passed as an argument array, never a shell string, so a helper name from a config file cannot become a shell injection.

Parameters:

  • helper (String)

    the helper's short name

  • key (String)

    the registry to ask about

Returns:

  • (Hash, nil)


129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/docker/api/auth.rb', line 129

def run_helper(helper, key)
  output = IO.popen(["docker-credential-#{helper}", "get"], "r+") do |io|
    io.write(key)
    io.close_write
    io.read
  end
  return nil unless $CHILD_STATUS.nil? || $CHILD_STATUS.success?

  parsed = JSON.parse(output.to_s)
  secret = parsed["Secret"]
  return nil if secret.nil? || secret.empty?

  # A username of <token> is the helper's way of saying the secret is an
  # identity token rather than a password.
  if parsed["Username"] == "<token>"
    { identitytoken: secret, serveraddress: parsed["ServerURL"] || key }
  else
    { username: parsed["Username"], password: secret,
      serveraddress: parsed["ServerURL"] || key }
  end
rescue Errno::ENOENT, Errno::EACCES, JSON::ParserError, IOError
  nil
end