summaryrefslogtreecommitdiff
path: root/docker/auth.py
diff options
context:
space:
mode:
Diffstat (limited to 'docker/auth.py')
-rw-r--r--docker/auth.py303
1 files changed, 303 insertions, 0 deletions
diff --git a/docker/auth.py b/docker/auth.py
new file mode 100644
index 0000000..0a2eda1
--- /dev/null
+++ b/docker/auth.py
@@ -0,0 +1,303 @@
+import base64
+import json
+import logging
+import os
+
+import dockerpycreds
+import six
+
+from . import errors
+
+INDEX_NAME = 'docker.io'
+INDEX_URL = 'https://{0}/v1/'.format(INDEX_NAME)
+DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json')
+LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg'
+TOKEN_USERNAME = '<token>'
+
+log = logging.getLogger(__name__)
+
+
+def resolve_repository_name(repo_name):
+ if '://' in repo_name:
+ raise errors.InvalidRepository(
+ 'Repository name cannot contain a scheme ({0})'.format(repo_name)
+ )
+
+ index_name, remote_name = split_repo_name(repo_name)
+ if index_name[0] == '-' or index_name[-1] == '-':
+ raise errors.InvalidRepository(
+ 'Invalid index name ({0}). Cannot begin or end with a'
+ ' hyphen.'.format(index_name)
+ )
+ return resolve_index_name(index_name), remote_name
+
+
+def resolve_index_name(index_name):
+ index_name = convert_to_hostname(index_name)
+ if index_name == 'index.' + INDEX_NAME:
+ index_name = INDEX_NAME
+ return index_name
+
+
+def get_config_header(client, registry):
+ log.debug('Looking for auth config')
+ if not client._auth_configs:
+ log.debug(
+ "No auth config in memory - loading from filesystem"
+ )
+ client._auth_configs = load_config()
+ authcfg = resolve_authconfig(client._auth_configs, registry)
+ # Do not fail here if no authentication exists for this
+ # specific registry as we can have a readonly pull. Just
+ # put the header if we can.
+ if authcfg:
+ log.debug('Found auth config')
+ # auth_config needs to be a dict in the format used by
+ # auth.py username , password, serveraddress, email
+ return encode_header(authcfg)
+ log.debug('No auth config found')
+ return None
+
+
+def split_repo_name(repo_name):
+ parts = repo_name.split('/', 1)
+ if len(parts) == 1 or (
+ '.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost'
+ ):
+ # This is a docker index repo (ex: username/foobar or ubuntu)
+ return INDEX_NAME, repo_name
+ return tuple(parts)
+
+
+def resolve_authconfig(authconfig, registry=None):
+ """
+ Returns the authentication data from the given auth configuration for a
+ specific registry. As with the Docker client, legacy entries in the config
+ with full URLs are stripped down to hostnames before checking for a match.
+ Returns None if no match was found.
+ """
+ if 'credsStore' in authconfig:
+ log.debug(
+ 'Using credentials store "{0}"'.format(authconfig['credsStore'])
+ )
+ return _resolve_authconfig_credstore(
+ authconfig, registry, authconfig['credsStore']
+ )
+ # Default to the public index server
+ registry = resolve_index_name(registry) if registry else INDEX_NAME
+ log.debug("Looking for auth entry for {0}".format(repr(registry)))
+
+ if registry in authconfig:
+ log.debug("Found {0}".format(repr(registry)))
+ return authconfig[registry]
+
+ for key, config in six.iteritems(authconfig):
+ if resolve_index_name(key) == registry:
+ log.debug("Found {0}".format(repr(key)))
+ return config
+
+ log.debug("No entry found")
+ return None
+
+
+def _resolve_authconfig_credstore(authconfig, registry, credstore_name):
+ if not registry or registry == INDEX_NAME:
+ # The ecosystem is a little schizophrenic with index.docker.io VS
+ # docker.io - in that case, it seems the full URL is necessary.
+ registry = 'https://index.docker.io/v1/'
+ log.debug("Looking for auth entry for {0}".format(repr(registry)))
+ store = dockerpycreds.Store(credstore_name)
+ try:
+ data = store.get(registry)
+ res = {
+ 'ServerAddress': registry,
+ }
+ if data['Username'] == TOKEN_USERNAME:
+ res['IdentityToken'] = data['Secret']
+ else:
+ res.update({
+ 'Username': data['Username'],
+ 'Password': data['Secret'],
+ })
+ return res
+ except dockerpycreds.CredentialsNotFound as e:
+ log.debug('No entry found')
+ return None
+ except dockerpycreds.StoreError as e:
+ raise errors.DockerException(
+ 'Credentials store error: {0}'.format(repr(e))
+ )
+
+
+def convert_to_hostname(url):
+ return url.replace('http://', '').replace('https://', '').split('/', 1)[0]
+
+
+def decode_auth(auth):
+ if isinstance(auth, six.string_types):
+ auth = auth.encode('ascii')
+ s = base64.b64decode(auth)
+ login, pwd = s.split(b':', 1)
+ return login.decode('utf8'), pwd.decode('utf8')
+
+
+def encode_header(auth):
+ auth_json = json.dumps(auth).encode('ascii')
+ return base64.urlsafe_b64encode(auth_json)
+
+
+def parse_auth(entries, raise_on_error=False):
+ """
+ Parses authentication entries
+
+ Args:
+ entries: Dict of authentication entries.
+ raise_on_error: If set to true, an invalid format will raise
+ InvalidConfigFile
+
+ Returns:
+ Authentication registry.
+ """
+
+ conf = {}
+ for registry, entry in six.iteritems(entries):
+ if not isinstance(entry, dict):
+ log.debug(
+ 'Config entry for key {0} is not auth config'.format(registry)
+ )
+ # We sometimes fall back to parsing the whole config as if it was
+ # the auth config by itself, for legacy purposes. In that case, we
+ # fail silently and return an empty conf if any of the keys is not
+ # formatted properly.
+ if raise_on_error:
+ raise errors.InvalidConfigFile(
+ 'Invalid configuration for registry {0}'.format(registry)
+ )
+ return {}
+ if 'identitytoken' in entry:
+ log.debug('Found an IdentityToken entry for registry {0}'.format(
+ registry
+ ))
+ conf[registry] = {
+ 'IdentityToken': entry['identitytoken']
+ }
+ continue # Other values are irrelevant if we have a token, skip.
+
+ if 'auth' not in entry:
+ # Starting with engine v1.11 (API 1.23), an empty dictionary is
+ # a valid value in the auths config.
+ # https://github.com/docker/compose/issues/3265
+ log.debug(
+ 'Auth data for {0} is absent. Client might be using a '
+ 'credentials store instead.'
+ )
+ conf[registry] = {}
+ continue
+
+ username, password = decode_auth(entry['auth'])
+ log.debug(
+ 'Found entry (registry={0}, username={1})'
+ .format(repr(registry), repr(username))
+ )
+
+ conf[registry] = {
+ 'username': username,
+ 'password': password,
+ 'email': entry.get('email'),
+ 'serveraddress': registry,
+ }
+ return conf
+
+
+def find_config_file(config_path=None):
+ environment_path = os.path.join(
+ os.environ.get('DOCKER_CONFIG'),
+ os.path.basename(DOCKER_CONFIG_FILENAME)
+ ) if os.environ.get('DOCKER_CONFIG') else None
+
+ paths = filter(None, [
+ config_path, # 1
+ environment_path, # 2
+ os.path.join(os.path.expanduser('~'), DOCKER_CONFIG_FILENAME), # 3
+ os.path.join(
+ os.path.expanduser('~'), LEGACY_DOCKER_CONFIG_FILENAME
+ ) # 4
+ ])
+
+ log.debug("Trying paths: {0}".format(repr(paths)))
+
+ for path in paths:
+ if os.path.exists(path):
+ log.debug("Found file at path: {0}".format(path))
+ return path
+
+ log.debug("No config file found")
+
+ return None
+
+
+def load_config(config_path=None):
+ """
+ Loads authentication data from a Docker configuration file in the given
+ root directory or if config_path is passed use given path.
+ Lookup priority:
+ explicit config_path parameter > DOCKER_CONFIG environment variable >
+ ~/.docker/config.json > ~/.dockercfg
+ """
+ config_file = find_config_file(config_path)
+
+ if not config_file:
+ return {}
+
+ try:
+ with open(config_file) as f:
+ data = json.load(f)
+ res = {}
+ if data.get('auths'):
+ log.debug("Found 'auths' section")
+ res.update(parse_auth(data['auths'], raise_on_error=True))
+ if data.get('HttpHeaders'):
+ log.debug("Found 'HttpHeaders' section")
+ res.update({'HttpHeaders': data['HttpHeaders']})
+ if data.get('credsStore'):
+ log.debug("Found 'credsStore' section")
+ res.update({'credsStore': data['credsStore']})
+ if res:
+ return res
+ else:
+ log.debug("Couldn't find 'auths' or 'HttpHeaders' sections")
+ f.seek(0)
+ return parse_auth(json.load(f))
+ except (IOError, KeyError, ValueError) as e:
+ # Likely missing new Docker config file or it's in an
+ # unknown format, continue to attempt to read old location
+ # and format.
+ log.debug(e)
+
+ log.debug("Attempting to parse legacy auth file format")
+ try:
+ data = []
+ with open(config_file) as f:
+ for line in f.readlines():
+ data.append(line.strip().split(' = ')[1])
+ if len(data) < 2:
+ # Not enough data
+ raise errors.InvalidConfigFile(
+ 'Invalid or empty configuration file!'
+ )
+
+ username, password = decode_auth(data[0])
+ return {
+ INDEX_NAME: {
+ 'username': username,
+ 'password': password,
+ 'email': data[1],
+ 'serveraddress': INDEX_URL,
+ }
+ }
+ except Exception as e:
+ log.debug(e)
+ pass
+
+ log.debug("All parsing attempts failed - returning empty config")
+ return {}