Kaydet (Commit) ccbde11c authored tarafından Joffrey F's avatar Joffrey F

Improve separation between auth_configs and general_configs

Signed-off-by: 's avatarJoffrey F <joffrey@docker.com>
üst 75e2e8ad
...@@ -300,14 +300,12 @@ class BuildApiMixin(object): ...@@ -300,14 +300,12 @@ class BuildApiMixin(object):
# Matches CLI behavior: https://github.com/docker/docker/blob/ # Matches CLI behavior: https://github.com/docker/docker/blob/
# 67b85f9d26f1b0b2b240f2d794748fac0f45243c/cliconfig/ # 67b85f9d26f1b0b2b240f2d794748fac0f45243c/cliconfig/
# credentials/native_store.go#L68-L83 # credentials/native_store.go#L68-L83
for registry in self._auth_configs.keys(): for registry in self._auth_configs.get('auths', {}).keys():
if registry == 'credsStore' or registry == 'HttpHeaders':
continue
auth_data[registry] = auth.resolve_authconfig( auth_data[registry] = auth.resolve_authconfig(
self._auth_configs, registry self._auth_configs, registry
) )
else: else:
auth_data = self._auth_configs.copy() auth_data = self._auth_configs.get('auths', {}).copy()
# See https://github.com/docker/docker-py/issues/1683 # See https://github.com/docker/docker-py/issues/1683
if auth.INDEX_NAME in auth_data: if auth.INDEX_NAME in auth_data:
auth_data[auth.INDEX_URL] = auth_data[auth.INDEX_NAME] auth_data[auth.INDEX_URL] = auth_data[auth.INDEX_NAME]
......
...@@ -87,6 +87,7 @@ class APIClient( ...@@ -87,6 +87,7 @@ class APIClient(
""" """
__attrs__ = requests.Session.__attrs__ + ['_auth_configs', __attrs__ = requests.Session.__attrs__ + ['_auth_configs',
'_general_configs',
'_version', '_version',
'base_url', 'base_url',
'timeout'] 'timeout']
...@@ -105,8 +106,10 @@ class APIClient( ...@@ -105,8 +106,10 @@ class APIClient(
self.timeout = timeout self.timeout = timeout
self.headers['User-Agent'] = user_agent self.headers['User-Agent'] = user_agent
self._auth_configs = auth.load_config()
self._general_configs = config.load_general_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 = utils.parse_host(
base_url, IS_WINDOWS_PLATFORM, tls=bool(tls) base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)
......
...@@ -144,6 +144,8 @@ class DaemonApiMixin(object): ...@@ -144,6 +144,8 @@ class DaemonApiMixin(object):
response = self._post_json(self._url('/auth'), data=req_data) response = self._post_json(self._url('/auth'), data=req_data)
if response.status_code == 200: 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 self._auth_configs[registry or auth.INDEX_NAME] = req_data
return self._result(response, json=True) return self._result(response, json=True)
......
...@@ -98,11 +98,12 @@ def resolve_authconfig(authconfig, registry=None): ...@@ -98,11 +98,12 @@ def resolve_authconfig(authconfig, registry=None):
registry = resolve_index_name(registry) if registry else INDEX_NAME registry = resolve_index_name(registry) if registry else INDEX_NAME
log.debug("Looking for auth entry for {0}".format(repr(registry))) 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))) 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: if resolve_index_name(key) == registry:
log.debug("Found {0}".format(repr(key))) log.debug("Found {0}".format(repr(key)))
return conf return conf
...@@ -220,7 +221,7 @@ def parse_auth(entries, raise_on_error=False): ...@@ -220,7 +221,7 @@ def parse_auth(entries, raise_on_error=False):
return conf 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 Loads authentication data from a Docker configuration file in the given
root directory or if config_path is passed use given path. root directory or if config_path is passed use given path.
...@@ -228,39 +229,45 @@ def load_config(config_path=None): ...@@ -228,39 +229,45 @@ def load_config(config_path=None):
explicit config_path parameter > DOCKER_CONFIG environment variable > explicit config_path parameter > DOCKER_CONFIG environment variable >
~/.docker/config.json > ~/.dockercfg ~/.docker/config.json > ~/.dockercfg
""" """
config_file = config.find_config_file(config_path)
if not config_file: if not config_dict:
return {} 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") log.debug("Attempting to parse legacy auth file format")
try: try:
data = [] data = []
......
...@@ -57,9 +57,10 @@ def load_general_config(config_path=None): ...@@ -57,9 +57,10 @@ def load_general_config(config_path=None):
try: try:
with open(config_file) as f: with open(config_file) as f:
return json.load(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) log.debug(e)
pass
log.debug("All parsing attempts failed - returning empty config") log.debug("All parsing attempts failed - returning empty config")
return {} return {}
...@@ -38,10 +38,10 @@ def minimum_version(version): ...@@ -38,10 +38,10 @@ def minimum_version(version):
def update_headers(f): def update_headers(f):
def inner(self, *args, **kwargs): def inner(self, *args, **kwargs):
if 'HttpHeaders' in self._auth_configs: if 'HttpHeaders' in self._general_configs:
if not kwargs.get('headers'): if not kwargs.get('headers'):
kwargs['headers'] = self._auth_configs['HttpHeaders'] kwargs['headers'] = self._general_configs['HttpHeaders']
else: else:
kwargs['headers'].update(self._auth_configs['HttpHeaders']) kwargs['headers'].update(self._general_configs['HttpHeaders'])
return f(self, *args, **kwargs) return f(self, *args, **kwargs)
return inner return inner
...@@ -161,7 +161,7 @@ class CreateContainerTest(BaseAPIIntegrationTest): ...@@ -161,7 +161,7 @@ class CreateContainerTest(BaseAPIIntegrationTest):
self.client.start(container3_id) self.client.start(container3_id)
info = self.client.inspect_container(res2['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): def create_container_readonly_fs(self):
ctnr = self.client.create_container( ctnr = self.client.create_container(
......
...@@ -4,7 +4,6 @@ import unittest ...@@ -4,7 +4,6 @@ import unittest
import docker import docker
from docker.utils import kwargs_from_env from docker.utils import kwargs_from_env
import six
from .. import helpers from .. import helpers
...@@ -19,9 +18,6 @@ class BaseIntegrationTest(unittest.TestCase): ...@@ -19,9 +18,6 @@ class BaseIntegrationTest(unittest.TestCase):
""" """
def setUp(self): def setUp(self):
if six.PY2:
self.assertRegex = self.assertRegexpMatches
self.assertCountEqual = self.assertItemsEqual
self.tmp_imgs = [] self.tmp_imgs = []
self.tmp_containers = [] self.tmp_containers = []
self.tmp_folders = [] self.tmp_folders = []
......
...@@ -73,10 +73,12 @@ class BuildTest(BaseAPIClientTest): ...@@ -73,10 +73,12 @@ class BuildTest(BaseAPIClientTest):
def test_build_remote_with_registry_auth(self): def test_build_remote_with_registry_auth(self):
self.client._auth_configs = { self.client._auth_configs = {
'https://example.com': { 'auths': {
'user': 'example', 'https://example.com': {
'password': 'example', 'user': 'example',
'email': 'example@example.com' 'password': 'example',
'email': 'example@example.com'
}
} }
} }
...@@ -85,7 +87,10 @@ class BuildTest(BaseAPIClientTest): ...@@ -85,7 +87,10 @@ class BuildTest(BaseAPIClientTest):
'forcerm': False, 'forcerm': False,
'remote': 'https://github.com/docker-library/mongo'} 'remote': 'https://github.com/docker-library/mongo'}
expected_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.build(path='https://github.com/docker-library/mongo') self.client.build(path='https://github.com/docker-library/mongo')
...@@ -118,32 +123,43 @@ class BuildTest(BaseAPIClientTest): ...@@ -118,32 +123,43 @@ class BuildTest(BaseAPIClientTest):
def test_set_auth_headers_with_empty_dict_and_auth_configs(self): def test_set_auth_headers_with_empty_dict_and_auth_configs(self):
self.client._auth_configs = { self.client._auth_configs = {
'https://example.com': { 'auths': {
'user': 'example', 'https://example.com': {
'password': 'example', 'user': 'example',
'email': 'example@example.com' 'password': 'example',
'email': 'example@example.com'
}
} }
} }
headers = {} headers = {}
expected_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) self.client._set_auth_headers(headers)
assert headers == expected_headers assert headers == expected_headers
def test_set_auth_headers_with_dict_and_auth_configs(self): def test_set_auth_headers_with_dict_and_auth_configs(self):
self.client._auth_configs = { self.client._auth_configs = {
'https://example.com': { 'auths': {
'user': 'example', 'https://example.com': {
'password': 'example', 'user': 'example',
'email': 'example@example.com' 'password': 'example',
'email': 'example@example.com'
}
} }
} }
headers = {'foo': 'bar'} headers = {'foo': 'bar'}
expected_headers = { expected_headers = {
'foo': 'bar', 'X-Registry-Config': auth.encode_header(
'X-Registry-Config': auth.encode_header(self.client._auth_configs)} self.client._auth_configs['auths']
),
'foo': 'bar'
}
self.client._set_auth_headers(headers) self.client._set_auth_headers(headers)
assert headers == expected_headers assert headers == expected_headers
......
...@@ -106,11 +106,13 @@ class ResolveAuthTest(unittest.TestCase): ...@@ -106,11 +106,13 @@ class ResolveAuthTest(unittest.TestCase):
private_config = {'auth': encode_auth({'username': 'privateuser'})} private_config = {'auth': encode_auth({'username': 'privateuser'})}
legacy_config = {'auth': encode_auth({'username': 'legacyauth'})} legacy_config = {'auth': encode_auth({'username': 'legacyauth'})}
auth_config = auth.parse_auth({ auth_config = {
'https://index.docker.io/v1/': index_config, 'auths': auth.parse_auth({
'my.registry.net': private_config, 'https://index.docker.io/v1/': index_config,
'http://legacy.registry.url/v1/': legacy_config, 'my.registry.net': private_config,
}) 'http://legacy.registry.url/v1/': legacy_config,
})
}
def test_resolve_authconfig_hostname_only(self): def test_resolve_authconfig_hostname_only(self):
assert auth.resolve_authconfig( assert auth.resolve_authconfig(
...@@ -360,9 +362,8 @@ class LoadConfigTest(unittest.TestCase): ...@@ -360,9 +362,8 @@ class LoadConfigTest(unittest.TestCase):
with mock.patch.dict(os.environ, {'DOCKER_CONFIG': folder}): with mock.patch.dict(os.environ, {'DOCKER_CONFIG': folder}):
cfg = auth.load_config(None) cfg = auth.load_config(None)
assert registry in cfg assert registry in cfg['auths']
assert cfg[registry] is not None cfg = cfg['auths'][registry]
cfg = cfg[registry]
assert cfg['username'] == 'sakuya' assert cfg['username'] == 'sakuya'
assert cfg['password'] == 'izayoi' assert cfg['password'] == 'izayoi'
assert cfg['email'] == 'sakuya@scarlet.net' assert cfg['email'] == 'sakuya@scarlet.net'
...@@ -390,38 +391,13 @@ class LoadConfigTest(unittest.TestCase): ...@@ -390,38 +391,13 @@ class LoadConfigTest(unittest.TestCase):
with mock.patch.dict(os.environ, {'DOCKER_CONFIG': folder}): with mock.patch.dict(os.environ, {'DOCKER_CONFIG': folder}):
cfg = auth.load_config(None) cfg = auth.load_config(None)
assert registry in cfg assert registry in cfg['auths']
assert cfg[registry] is not None cfg = cfg['auths'][registry]
cfg = cfg[registry]
assert cfg['username'] == b'sakuya\xc3\xa6'.decode('utf8') assert cfg['username'] == b'sakuya\xc3\xa6'.decode('utf8')
assert cfg['password'] == b'izayoi\xc3\xa6'.decode('utf8') assert cfg['password'] == b'izayoi\xc3\xa6'.decode('utf8')
assert cfg['email'] == 'sakuya@scarlet.net' assert cfg['email'] == 'sakuya@scarlet.net'
assert cfg.get('auth') is None 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): def test_load_config_unknown_keys(self):
folder = tempfile.mkdtemp() folder = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, folder) self.addCleanup(shutil.rmtree, folder)
...@@ -448,7 +424,7 @@ class LoadConfigTest(unittest.TestCase): ...@@ -448,7 +424,7 @@ class LoadConfigTest(unittest.TestCase):
json.dump(config, f) json.dump(config, f)
cfg = auth.load_config(dockercfg_path) cfg = auth.load_config(dockercfg_path)
assert cfg == {'scarlet.net': {}} assert cfg == {'auths': {'scarlet.net': {}}}
def test_load_config_identity_token(self): def test_load_config_identity_token(self):
folder = tempfile.mkdtemp() folder = tempfile.mkdtemp()
...@@ -469,7 +445,7 @@ class LoadConfigTest(unittest.TestCase): ...@@ -469,7 +445,7 @@ class LoadConfigTest(unittest.TestCase):
json.dump(config, f) json.dump(config, f)
cfg = auth.load_config(dockercfg_path) cfg = auth.load_config(dockercfg_path)
assert registry in cfg assert registry in cfg['auths']
cfg = cfg[registry] cfg = cfg['auths'][registry]
assert 'IdentityToken' in cfg assert 'IdentityToken' in cfg
assert cfg['IdentityToken'] == token assert cfg['IdentityToken'] == token
...@@ -69,16 +69,55 @@ class LoadConfigTest(unittest.TestCase): ...@@ -69,16 +69,55 @@ class LoadConfigTest(unittest.TestCase):
folder = tempfile.mkdtemp() folder = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, folder) self.addCleanup(shutil.rmtree, folder)
cfg = config.load_general_config(folder) cfg = config.load_general_config(folder)
self.assertTrue(cfg is not None) assert cfg is not None
assert isinstance(cfg, dict)
assert not cfg
def test_load_config(self): def test_load_config_custom_headers(self):
folder = tempfile.mkdtemp() folder = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, folder) self.addCleanup(shutil.rmtree, folder)
dockercfg_path = os.path.join(folder, '.dockercfg')
cfg = { dockercfg_path = os.path.join(folder, 'config.json')
config_data = {
'HttpHeaders': {
'Name': 'Spike',
'Surname': 'Spiegel'
},
}
with open(dockercfg_path, 'w') as f:
json.dump(config_data, f)
cfg = config.load_general_config(dockercfg_path)
assert 'HttpHeaders' in cfg
assert cfg['HttpHeaders'] == {
'Name': 'Spike',
'Surname': 'Spiegel'
}
def test_load_config_detach_keys(self):
folder = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, folder)
dockercfg_path = os.path.join(folder, 'config.json')
config_data = {
'detachKeys': 'ctrl-q, ctrl-u, ctrl-i'
}
with open(dockercfg_path, 'w') as f:
json.dump(config_data, f)
cfg = config.load_general_config(dockercfg_path)
assert cfg == config_data
def test_load_config_from_env(self):
folder = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, folder)
dockercfg_path = os.path.join(folder, 'config.json')
config_data = {
'detachKeys': 'ctrl-q, ctrl-u, ctrl-i' 'detachKeys': 'ctrl-q, ctrl-u, ctrl-i'
} }
with open(dockercfg_path, 'w') as f: with open(dockercfg_path, 'w') as f:
json.dump(cfg, f) json.dump(config_data, f)
self.assertEqual(config.load_general_config(dockercfg_path), cfg) with mock.patch.dict(os.environ, {'DOCKER_CONFIG': folder}):
cfg = config.load_general_config(None)
assert cfg == config_data
...@@ -46,7 +46,7 @@ class DecoratorsTest(unittest.TestCase): ...@@ -46,7 +46,7 @@ class DecoratorsTest(unittest.TestCase):
return headers return headers
client = APIClient() client = APIClient()
client._auth_configs = {} client._general_configs = {}
g = update_headers(f) g = update_headers(f)
assert g(client, headers=None) is None assert g(client, headers=None) is None
...@@ -55,7 +55,7 @@ class DecoratorsTest(unittest.TestCase): ...@@ -55,7 +55,7 @@ class DecoratorsTest(unittest.TestCase):
'Content-type': 'application/json', 'Content-type': 'application/json',
} }
client._auth_configs = { client._general_configs = {
'HttpHeaders': sample_headers 'HttpHeaders': sample_headers
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment