Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions docker/api/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,14 +300,12 @@ def _set_auth_headers(self, headers):
# Matches CLI behavior: https://github.com/docker/docker/blob/
# 67b85f9d26f1b0b2b240f2d794748fac0f45243c/cliconfig/
# credentials/native_store.go#L68-L83
for registry in self._auth_configs.keys():
if registry == 'credsStore' or registry == 'HttpHeaders':
continue
for registry in self._auth_configs.get('auths', {}).keys():
auth_data[registry] = auth.resolve_authconfig(
self._auth_configs, registry
)
else:
auth_data = self._auth_configs.copy()
auth_data = self._auth_configs.get('auths', {}).copy()
# See https://github.com/docker/docker-py/issues/1683
if auth.INDEX_NAME in auth_data:
auth_data[auth.INDEX_URL] = auth_data[auth.INDEX_NAME]
Expand Down
5 changes: 4 additions & 1 deletion docker/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class APIClient(
"""

__attrs__ = requests.Session.__attrs__ + ['_auth_configs',
'_general_configs',
'_version',
'base_url',
'timeout']
Expand All @@ -105,8 +106,10 @@ def __init__(self, base_url=None, version=None,
self.timeout = timeout
self.headers['User-Agent'] = user_agent

self._auth_configs = auth.load_config()
self._general_configs = config.load_general_config()
self._auth_configs = auth.load_config(
config_dict=self._general_configs
)

base_url = utils.parse_host(
base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)
Expand Down
2 changes: 2 additions & 0 deletions docker/api/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ def login(self, username, password=None, email=None, registry=None,

response = self._post_json(self._url('/auth'), data=req_data)
if response.status_code == 200:
if 'auths' not in self._auth_configs:
self._auth_configs['auths'] = {}
self._auth_configs[registry or auth.INDEX_NAME] = req_data
return self._result(response, json=True)

Expand Down
75 changes: 41 additions & 34 deletions docker/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,12 @@ def resolve_authconfig(authconfig, registry=None):
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:
authdict = authconfig.get('auths', {})
if registry in authdict:
log.debug("Found {0}".format(repr(registry)))
return authconfig[registry]
return authdict[registry]

for key, conf in six.iteritems(authconfig):
for key, conf in six.iteritems(authdict):
if resolve_index_name(key) == registry:
log.debug("Found {0}".format(repr(key)))
return conf
Expand Down Expand Up @@ -220,47 +221,53 @@ def parse_auth(entries, raise_on_error=False):
return conf


def load_config(config_path=None):
def load_config(config_path=None, config_dict=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 = config.find_config_file(config_path)

if not config_file:
return {}
if not config_dict:
config_file = config.find_config_file(config_path)

if not config_file:
return {}
try:
with open(config_file) as f:
config_dict = 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)
return _load_legacy_config(config_file)

res = {}
if config_dict.get('auths'):
log.debug("Found 'auths' section")
res.update({
'auths': parse_auth(config_dict.pop('auths'), raise_on_error=True)
})
if config_dict.get('credsStore'):
log.debug("Found 'credsStore' section")
res.update({'credsStore': config_dict.pop('credsStore')})
if config_dict.get('credHelpers'):
log.debug("Found 'credHelpers' section")
res.update({'credHelpers': config_dict.pop('credHelpers')})
if res:
return res

log.debug(
"Couldn't find auth-related section ; attempting to interpret"
"as auth-only file"
)
return parse_auth(config_dict)

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 data.get('credHelpers'):
log.debug("Found 'credHelpers' section")
res.update({'credHelpers': data['credHelpers']})
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)

def _load_legacy_config(config_file):
log.debug("Attempting to parse legacy auth file format")
try:
data = []
Expand Down
5 changes: 3 additions & 2 deletions docker/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ def load_general_config(config_path=None):
try:
with open(config_file) as f:
return json.load(f)
except Exception as e:
except (IOError, ValueError) as e:
# In the case of a legacy `.dockercfg` file, we won't
# be able to load any JSON data.
log.debug(e)
pass

log.debug("All parsing attempts failed - returning empty config")
return {}
6 changes: 3 additions & 3 deletions docker/utils/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ def wrapper(self, *args, **kwargs):

def update_headers(f):
def inner(self, *args, **kwargs):
if 'HttpHeaders' in self._auth_configs:
if 'HttpHeaders' in self._general_configs:
if not kwargs.get('headers'):
kwargs['headers'] = self._auth_configs['HttpHeaders']
kwargs['headers'] = self._general_configs['HttpHeaders']
else:
kwargs['headers'].update(self._auth_configs['HttpHeaders'])
kwargs['headers'].update(self._general_configs['HttpHeaders'])
return f(self, *args, **kwargs)
return inner
2 changes: 1 addition & 1 deletion tests/integration/api_container_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def test_create_container_with_volumes_from(self):
self.client.start(container3_id)

info = self.client.inspect_container(res2['Id'])
self.assertCountEqual(info['HostConfig']['VolumesFrom'], vol_names)
assert len(info['HostConfig']['VolumesFrom']) == len(vol_names)

def create_container_readonly_fs(self):
ctnr = self.client.create_container(
Expand Down
4 changes: 0 additions & 4 deletions tests/integration/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import docker
from docker.utils import kwargs_from_env
import six

from .. import helpers

Expand All @@ -19,9 +18,6 @@ class BaseIntegrationTest(unittest.TestCase):
"""

def setUp(self):
if six.PY2:
self.assertRegex = self.assertRegexpMatches
self.assertCountEqual = self.assertItemsEqual
self.tmp_imgs = []
self.tmp_containers = []
self.tmp_folders = []
Expand Down
48 changes: 32 additions & 16 deletions tests/unit/api_build_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ def test_build_container_custom_context_gzip(self):

def test_build_remote_with_registry_auth(self):
self.client._auth_configs = {
'https://example.com': {
'user': 'example',
'password': 'example',
'email': 'example@example.com'
'auths': {
'https://example.com': {
'user': 'example',
'password': 'example',
'email': 'example@example.com'
}
}
}

Expand All @@ -85,7 +87,10 @@ def test_build_remote_with_registry_auth(self):
'forcerm': False,
'remote': 'https://github.com/docker-library/mongo'}
expected_headers = {
'X-Registry-Config': auth.encode_header(self.client._auth_configs)}
'X-Registry-Config': auth.encode_header(
self.client._auth_configs['auths']
)
}

self.client.build(path='https://github.com/docker-library/mongo')

Expand Down Expand Up @@ -118,32 +123,43 @@ def test_build_container_invalid_container_limits(self):

def test_set_auth_headers_with_empty_dict_and_auth_configs(self):
self.client._auth_configs = {
'https://example.com': {
'user': 'example',
'password': 'example',
'email': 'example@example.com'
'auths': {
'https://example.com': {
'user': 'example',
'password': 'example',
'email': 'example@example.com'
}
}
}

headers = {}
expected_headers = {
'X-Registry-Config': auth.encode_header(self.client._auth_configs)}
'X-Registry-Config': auth.encode_header(
self.client._auth_configs['auths']
)
}

self.client._set_auth_headers(headers)
assert headers == expected_headers

def test_set_auth_headers_with_dict_and_auth_configs(self):
self.client._auth_configs = {
'https://example.com': {
'user': 'example',
'password': 'example',
'email': 'example@example.com'
'auths': {
'https://example.com': {
'user': 'example',
'password': 'example',
'email': 'example@example.com'
}
}
}

headers = {'foo': 'bar'}
expected_headers = {
'foo': 'bar',
'X-Registry-Config': auth.encode_header(self.client._auth_configs)}
'X-Registry-Config': auth.encode_header(
self.client._auth_configs['auths']
),
'foo': 'bar'
}

self.client._set_auth_headers(headers)
assert headers == expected_headers
Expand Down
52 changes: 14 additions & 38 deletions tests/unit/auth_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,13 @@ class ResolveAuthTest(unittest.TestCase):
private_config = {'auth': encode_auth({'username': 'privateuser'})}
legacy_config = {'auth': encode_auth({'username': 'legacyauth'})}

auth_config = auth.parse_auth({
'https://index.docker.io/v1/': index_config,
'my.registry.net': private_config,
'http://legacy.registry.url/v1/': legacy_config,
})
auth_config = {
'auths': auth.parse_auth({
'https://index.docker.io/v1/': index_config,
'my.registry.net': private_config,
'http://legacy.registry.url/v1/': legacy_config,
})
}

def test_resolve_authconfig_hostname_only(self):
assert auth.resolve_authconfig(
Expand Down Expand Up @@ -360,9 +362,8 @@ def test_load_config_custom_config_env_with_auths(self):

with mock.patch.dict(os.environ, {'DOCKER_CONFIG': folder}):
cfg = auth.load_config(None)
assert registry in cfg
assert cfg[registry] is not None
cfg = cfg[registry]
assert registry in cfg['auths']
cfg = cfg['auths'][registry]
assert cfg['username'] == 'sakuya'
assert cfg['password'] == 'izayoi'
assert cfg['email'] == 'sakuya@scarlet.net'
Expand Down Expand Up @@ -390,38 +391,13 @@ def test_load_config_custom_config_env_utf8(self):

with mock.patch.dict(os.environ, {'DOCKER_CONFIG': folder}):
cfg = auth.load_config(None)
assert registry in cfg
assert cfg[registry] is not None
cfg = cfg[registry]
assert registry in cfg['auths']
cfg = cfg['auths'][registry]
assert cfg['username'] == b'sakuya\xc3\xa6'.decode('utf8')
assert cfg['password'] == b'izayoi\xc3\xa6'.decode('utf8')
assert cfg['email'] == 'sakuya@scarlet.net'
assert cfg.get('auth') is None

def test_load_config_custom_config_env_with_headers(self):
folder = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, folder)

dockercfg_path = os.path.join(folder, 'config.json')
config = {
'HttpHeaders': {
'Name': 'Spike',
'Surname': 'Spiegel'
},
}

with open(dockercfg_path, 'w') as f:
json.dump(config, f)

with mock.patch.dict(os.environ, {'DOCKER_CONFIG': folder}):
cfg = auth.load_config(None)
assert 'HttpHeaders' in cfg
assert cfg['HttpHeaders'] is not None
cfg = cfg['HttpHeaders']

assert cfg['Name'] == 'Spike'
assert cfg['Surname'] == 'Spiegel'

def test_load_config_unknown_keys(self):
folder = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, folder)
Expand All @@ -448,7 +424,7 @@ def test_load_config_invalid_auth_dict(self):
json.dump(config, f)

cfg = auth.load_config(dockercfg_path)
assert cfg == {'scarlet.net': {}}
assert cfg == {'auths': {'scarlet.net': {}}}

def test_load_config_identity_token(self):
folder = tempfile.mkdtemp()
Expand All @@ -469,7 +445,7 @@ def test_load_config_identity_token(self):
json.dump(config, f)

cfg = auth.load_config(dockercfg_path)
assert registry in cfg
cfg = cfg[registry]
assert registry in cfg['auths']
cfg = cfg['auths'][registry]
assert 'IdentityToken' in cfg
assert cfg['IdentityToken'] == token
Loading