Modernize auth management

Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
Joffrey F 2018-11-28 19:32:01 -08:00
parent 9a67e2032e
commit bc5d7c8cb6
8 changed files with 301 additions and 244 deletions

View File

@ -293,31 +293,11 @@ class BuildApiMixin(object):
# Send the full auth configuration (if any exists), since the build # Send the full auth configuration (if any exists), since the build
# could use any (or all) of the registries. # could use any (or all) of the registries.
if self._auth_configs: if self._auth_configs:
auth_cfgs = self._auth_configs auth_data = self._auth_configs.get_all_credentials()
auth_data = {}
if auth_cfgs.get('credsStore'):
# Using a credentials store, we need to retrieve the
# credentials for each registry listed in the config.json file
# Matches CLI behavior: https://github.com/docker/docker/blob/
# 67b85f9d26f1b0b2b240f2d794748fac0f45243c/cliconfig/
# credentials/native_store.go#L68-L83
for registry in auth_cfgs.get('auths', {}).keys():
auth_data[registry] = auth.resolve_authconfig(
auth_cfgs, registry,
credstore_env=self.credstore_env,
)
else:
for registry in auth_cfgs.get('credHelpers', {}).keys():
auth_data[registry] = auth.resolve_authconfig(
auth_cfgs, registry,
credstore_env=self.credstore_env
)
for registry, creds in auth_cfgs.get('auths', {}).items():
if registry not in auth_data:
auth_data[registry] = creds
# 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_URL not in auth_data and auth.INDEX_URL in auth_data:
auth_data[auth.INDEX_URL] = auth_data[auth.INDEX_NAME] auth_data[auth.INDEX_URL] = auth_data.get(auth.INDEX_NAME, {})
log.debug( log.debug(
'Sending auth config ({0})'.format( 'Sending auth config ({0})'.format(
@ -325,6 +305,7 @@ class BuildApiMixin(object):
) )
) )
if auth_data:
headers['X-Registry-Config'] = auth.encode_header( headers['X-Registry-Config'] = auth.encode_header(
auth_data auth_data
) )

View File

@ -124,13 +124,15 @@ class DaemonApiMixin(object):
# If dockercfg_path is passed check to see if the config file exists, # If dockercfg_path is passed check to see if the config file exists,
# if so load that config. # if so load that config.
if dockercfg_path and os.path.exists(dockercfg_path): if dockercfg_path and os.path.exists(dockercfg_path):
self._auth_configs = auth.load_config(dockercfg_path) self._auth_configs = auth.load_config(
elif not self._auth_configs: dockercfg_path, credstore_env=self.credstore_env
self._auth_configs = auth.load_config()
authcfg = auth.resolve_authconfig(
self._auth_configs, registry, credstore_env=self.credstore_env,
) )
elif not self._auth_configs:
self._auth_configs = auth.load_config(
credstore_env=self.credstore_env
)
authcfg = self._auth_configs.resolve_authconfig(registry)
# If we found an existing auth config for this registry and username # If we found an existing auth config for this registry and username
# combination, we can return it immediately unless reauth is requested. # combination, we can return it immediately unless reauth is requested.
if authcfg and authcfg.get('username', None) == username \ if authcfg and authcfg.get('username', None) == username \
@ -146,9 +148,7 @@ 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.add_auth(registry or auth.INDEX_NAME, req_data)
self._auth_configs['auths'] = {}
self._auth_configs['auths'][registry or auth.INDEX_NAME] = req_data
return self._result(response, json=True) return self._result(response, json=True)
def ping(self): def ping(self):

View File

@ -70,31 +70,161 @@ def split_repo_name(repo_name):
def get_credential_store(authconfig, registry): def get_credential_store(authconfig, registry):
if not registry or registry == INDEX_NAME: return authconfig.get_credential_store(registry)
registry = 'https://index.docker.io/v1/'
return authconfig.get('credHelpers', {}).get(registry) or authconfig.get(
'credsStore' class AuthConfig(object):
def __init__(self, dct, credstore_env=None):
if 'auths' not in dct:
dct['auths'] = {}
self._dct = dct
self._credstore_env = credstore_env
self._stores = {}
@classmethod
def parse_auth(cls, 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
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.'.format(registry)
)
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 resolve_authconfig(authconfig, registry=None, credstore_env=None): @classmethod
def load_config(cls, config_path, config_dict, credstore_env=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
"""
if not config_dict:
config_file = config.find_config_file(config_path)
if not config_file:
return cls({}, credstore_env)
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 cls(_load_legacy_config(config_file), credstore_env)
res = {}
if config_dict.get('auths'):
log.debug("Found 'auths' section")
res.update({
'auths': cls.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 cls(res, credstore_env)
log.debug(
"Couldn't find auth-related section ; attempting to interpret "
"as auth-only file"
)
return cls({'auths': cls.parse_auth(config_dict)}, credstore_env)
@property
def auths(self):
return self._dct.get('auths', {})
@property
def creds_store(self):
return self._dct.get('credsStore', None)
@property
def cred_helpers(self):
return self._dct.get('credHelpers', {})
def resolve_authconfig(self, registry=None):
""" """
Returns the authentication data from the given auth configuration for a Returns the authentication data from the given auth configuration for a
specific registry. As with the Docker client, legacy entries in the config specific registry. As with the Docker client, legacy entries in the
with full URLs are stripped down to hostnames before checking for a match. config with full URLs are stripped down to hostnames before checking
Returns None if no match was found. for a match. Returns None if no match was found.
""" """
if 'credHelpers' in authconfig or 'credsStore' in authconfig: if self.creds_store or self.cred_helpers:
store_name = get_credential_store(authconfig, registry) store_name = self.get_credential_store(registry)
if store_name is not None: if store_name is not None:
log.debug( log.debug(
'Using credentials store "{0}"'.format(store_name) 'Using credentials store "{0}"'.format(store_name)
) )
cfg = _resolve_authconfig_credstore( cfg = self._resolve_authconfig_credstore(registry, store_name)
authconfig, registry, store_name, env=credstore_env
)
if cfg is not None: if cfg is not None:
return cfg return cfg
log.debug('No entry in credstore - fetching from auth dict') log.debug('No entry in credstore - fetching from auth dict')
@ -103,12 +233,11 @@ def resolve_authconfig(authconfig, registry=None, credstore_env=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)))
authdict = authconfig.get('auths', {}) if registry in self.auths:
if registry in authdict:
log.debug("Found {0}".format(repr(registry))) log.debug("Found {0}".format(repr(registry)))
return authdict[registry] return self.auths[registry]
for key, conf in six.iteritems(authdict): for key, conf in six.iteritems(self.auths):
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
@ -116,15 +245,13 @@ def resolve_authconfig(authconfig, registry=None, credstore_env=None):
log.debug("No entry found") log.debug("No entry found")
return None return None
def _resolve_authconfig_credstore(self, registry, credstore_name):
def _resolve_authconfig_credstore(authconfig, registry, credstore_name,
env=None):
if not registry or registry == INDEX_NAME: if not registry or registry == INDEX_NAME:
# The ecosystem is a little schizophrenic with index.docker.io VS # The ecosystem is a little schizophrenic with index.docker.io VS
# docker.io - in that case, it seems the full URL is necessary. # docker.io - in that case, it seems the full URL is necessary.
registry = INDEX_URL registry = INDEX_URL
log.debug("Looking for auth entry for {0}".format(repr(registry))) log.debug("Looking for auth entry for {0}".format(repr(registry)))
store = dockerpycreds.Store(credstore_name, environment=env) store = self._get_store_instance(credstore_name)
try: try:
data = store.get(registry) data = store.get(registry)
res = { res = {
@ -146,6 +273,44 @@ def _resolve_authconfig_credstore(authconfig, registry, credstore_name,
'Credentials store error: {0}'.format(repr(e)) 'Credentials store error: {0}'.format(repr(e))
) )
def _get_store_instance(self, name):
if name not in self._stores:
self._stores[name] = dockerpycreds.Store(
name, environment=self._credstore_env
)
return self._stores[name]
def get_credential_store(self, registry):
if not registry or registry == INDEX_NAME:
registry = 'https://index.docker.io/v1/'
return self.cred_helpers.get(registry) or self.creds_store
def get_all_credentials(self):
auth_data = self.auths.copy()
if self.creds_store:
# Retrieve all credentials from the default store
store = self._get_store_instance(self.creds_store)
for k in store.list().keys():
auth_data[k] = self._resolve_authconfig_credstore(
k, self.creds_store
)
# credHelpers entries take priority over all others
for reg, store_name in self.cred_helpers.items():
auth_data[reg] = self._resolve_authconfig_credstore(
reg, store_name
)
return auth_data
def add_auth(self, reg, data):
self._dct['auths'][reg] = data
def resolve_authconfig(authconfig, registry=None, credstore_env=None):
return authconfig.resolve_authconfig(registry)
def convert_to_hostname(url): def convert_to_hostname(url):
return url.replace('http://', '').replace('https://', '').split('/', 1)[0] return url.replace('http://', '').replace('https://', '').split('/', 1)[0]
@ -177,100 +342,11 @@ def parse_auth(entries, raise_on_error=False):
Authentication registry. Authentication registry.
""" """
conf = {} return AuthConfig.parse_auth(entries, raise_on_error)
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.'.format(registry)
)
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 load_config(config_path=None, config_dict=None): def load_config(config_path=None, config_dict=None, credstore_env=None):
""" return AuthConfig.load_config(config_path, config_dict, credstore_env)
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
"""
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 {'auths': parse_auth(config_dict)}
def _load_legacy_config(config_file): def _load_legacy_config(config_file):

View File

@ -4,7 +4,7 @@ backports.ssl-match-hostname==3.5.0.1
cffi==1.10.0 cffi==1.10.0
cryptography==1.9; python_version == '3.3' cryptography==1.9; python_version == '3.3'
cryptography==2.3; python_version > '3.3' cryptography==2.3; python_version > '3.3'
docker-pycreds==0.3.0 docker-pycreds==0.4.0
enum34==1.1.6 enum34==1.1.6
idna==2.5 idna==2.5
ipaddress==1.0.18 ipaddress==1.0.18

View File

@ -12,7 +12,7 @@ SOURCE_DIR = os.path.join(ROOT_DIR)
requirements = [ requirements = [
'six >= 1.4.0', 'six >= 1.4.0',
'websocket-client >= 0.32.0', 'websocket-client >= 0.32.0',
'docker-pycreds >= 0.3.0', 'docker-pycreds >= 0.4.0',
'requests >= 2.14.2, != 2.18.0', 'requests >= 2.14.2, != 2.18.0',
] ]

View File

@ -65,7 +65,7 @@ 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 = auth.AuthConfig({
'auths': { 'auths': {
'https://example.com': { 'https://example.com': {
'user': 'example', 'user': 'example',
@ -73,7 +73,7 @@ class BuildTest(BaseAPIClientTest):
'email': 'example@example.com' 'email': 'example@example.com'
} }
} }
} })
expected_params = {'t': None, 'q': False, 'dockerfile': None, expected_params = {'t': None, 'q': False, 'dockerfile': None,
'rm': False, 'nocache': False, 'pull': False, 'rm': False, 'nocache': False, 'pull': False,
@ -81,7 +81,7 @@ class BuildTest(BaseAPIClientTest):
'remote': 'https://github.com/docker-library/mongo'} 'remote': 'https://github.com/docker-library/mongo'}
expected_headers = { expected_headers = {
'X-Registry-Config': auth.encode_header( 'X-Registry-Config': auth.encode_header(
self.client._auth_configs['auths'] self.client._auth_configs.auths
) )
} }
@ -115,7 +115,7 @@ 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 = auth.AuthConfig({
'auths': { 'auths': {
'https://example.com': { 'https://example.com': {
'user': 'example', 'user': 'example',
@ -123,12 +123,12 @@ class BuildTest(BaseAPIClientTest):
'email': 'example@example.com' 'email': 'example@example.com'
} }
} }
} })
headers = {} headers = {}
expected_headers = { expected_headers = {
'X-Registry-Config': auth.encode_header( 'X-Registry-Config': auth.encode_header(
self.client._auth_configs['auths'] self.client._auth_configs.auths
) )
} }
@ -136,7 +136,7 @@ class BuildTest(BaseAPIClientTest):
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 = auth.AuthConfig({
'auths': { 'auths': {
'https://example.com': { 'https://example.com': {
'user': 'example', 'user': 'example',
@ -144,12 +144,12 @@ class BuildTest(BaseAPIClientTest):
'email': 'example@example.com' 'email': 'example@example.com'
} }
} }
} })
headers = {'foo': 'bar'} headers = {'foo': 'bar'}
expected_headers = { expected_headers = {
'X-Registry-Config': auth.encode_header( 'X-Registry-Config': auth.encode_header(
self.client._auth_configs['auths'] self.client._auth_configs.auths
), ),
'foo': 'bar' 'foo': 'bar'
} }

View File

@ -221,14 +221,12 @@ class DockerApiTest(BaseAPIClientTest):
'username': 'sakuya', 'password': 'izayoi' 'username': 'sakuya', 'password': 'izayoi'
} }
assert args[1]['headers'] == {'Content-Type': 'application/json'} assert args[1]['headers'] == {'Content-Type': 'application/json'}
assert self.client._auth_configs['auths'] == { assert self.client._auth_configs.auths['docker.io'] == {
'docker.io': {
'email': None, 'email': None,
'password': 'izayoi', 'password': 'izayoi',
'username': 'sakuya', 'username': 'sakuya',
'serveraddress': None, 'serveraddress': None,
} }
}
def test_events(self): def test_events(self):
self.client.events() self.client.events()

View File

@ -106,13 +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_config = auth.AuthConfig({
'auths': auth.parse_auth({ 'auths': auth.parse_auth({
'https://index.docker.io/v1/': index_config, 'https://index.docker.io/v1/': index_config,
'my.registry.net': private_config, 'my.registry.net': private_config,
'http://legacy.registry.url/v1/': legacy_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(
@ -211,13 +211,15 @@ class ResolveAuthTest(unittest.TestCase):
) is None ) is None
def test_resolve_auth_with_empty_credstore_and_auth_dict(self): def test_resolve_auth_with_empty_credstore_and_auth_dict(self):
auth_config = { auth_config = auth.AuthConfig({
'auths': auth.parse_auth({ 'auths': auth.parse_auth({
'https://index.docker.io/v1/': self.index_config, 'https://index.docker.io/v1/': self.index_config,
}), }),
'credsStore': 'blackbox' 'credsStore': 'blackbox'
} })
with mock.patch('docker.auth._resolve_authconfig_credstore') as m: with mock.patch(
'docker.auth.AuthConfig._resolve_authconfig_credstore'
) as m:
m.return_value = None m.return_value = None
assert 'indexuser' == auth.resolve_authconfig( assert 'indexuser' == auth.resolve_authconfig(
auth_config, None auth_config, None
@ -226,13 +228,13 @@ class ResolveAuthTest(unittest.TestCase):
class CredStoreTest(unittest.TestCase): class CredStoreTest(unittest.TestCase):
def test_get_credential_store(self): def test_get_credential_store(self):
auth_config = { auth_config = auth.AuthConfig({
'credHelpers': { 'credHelpers': {
'registry1.io': 'truesecret', 'registry1.io': 'truesecret',
'registry2.io': 'powerlock' 'registry2.io': 'powerlock'
}, },
'credsStore': 'blackbox', 'credsStore': 'blackbox',
} })
assert auth.get_credential_store( assert auth.get_credential_store(
auth_config, 'registry1.io' auth_config, 'registry1.io'
@ -245,12 +247,12 @@ class CredStoreTest(unittest.TestCase):
) == 'blackbox' ) == 'blackbox'
def test_get_credential_store_no_default(self): def test_get_credential_store_no_default(self):
auth_config = { auth_config = auth.AuthConfig({
'credHelpers': { 'credHelpers': {
'registry1.io': 'truesecret', 'registry1.io': 'truesecret',
'registry2.io': 'powerlock' 'registry2.io': 'powerlock'
}, },
} })
assert auth.get_credential_store( assert auth.get_credential_store(
auth_config, 'registry2.io' auth_config, 'registry2.io'
) == 'powerlock' ) == 'powerlock'
@ -259,12 +261,12 @@ class CredStoreTest(unittest.TestCase):
) is None ) is None
def test_get_credential_store_default_index(self): def test_get_credential_store_default_index(self):
auth_config = { auth_config = auth.AuthConfig({
'credHelpers': { 'credHelpers': {
'https://index.docker.io/v1/': 'powerlock' 'https://index.docker.io/v1/': 'powerlock'
}, },
'credsStore': 'truesecret' 'credsStore': 'truesecret'
} })
assert auth.get_credential_store(auth_config, None) == 'powerlock' assert auth.get_credential_store(auth_config, None) == 'powerlock'
assert auth.get_credential_store( assert auth.get_credential_store(
@ -293,8 +295,8 @@ class LoadConfigTest(unittest.TestCase):
cfg = auth.load_config(cfg_path) cfg = auth.load_config(cfg_path)
assert auth.resolve_authconfig(cfg) is not None assert auth.resolve_authconfig(cfg) is not None
assert cfg['auths'][auth.INDEX_NAME] is not None assert cfg.auths[auth.INDEX_NAME] is not None
cfg = cfg['auths'][auth.INDEX_NAME] cfg = cfg.auths[auth.INDEX_NAME]
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'
@ -312,8 +314,8 @@ class LoadConfigTest(unittest.TestCase):
) )
cfg = auth.load_config(cfg_path) cfg = auth.load_config(cfg_path)
assert auth.resolve_authconfig(cfg) is not None assert auth.resolve_authconfig(cfg) is not None
assert cfg['auths'][auth.INDEX_URL] is not None assert cfg.auths[auth.INDEX_URL] is not None
cfg = cfg['auths'][auth.INDEX_URL] cfg = cfg.auths[auth.INDEX_URL]
assert cfg['username'] == 'sakuya' assert cfg['username'] == 'sakuya'
assert cfg['password'] == 'izayoi' assert cfg['password'] == 'izayoi'
assert cfg['email'] == email assert cfg['email'] == email
@ -335,8 +337,8 @@ class LoadConfigTest(unittest.TestCase):
}, f) }, f)
cfg = auth.load_config(cfg_path) cfg = auth.load_config(cfg_path)
assert auth.resolve_authconfig(cfg) is not None assert auth.resolve_authconfig(cfg) is not None
assert cfg['auths'][auth.INDEX_URL] is not None assert cfg.auths[auth.INDEX_URL] is not None
cfg = cfg['auths'][auth.INDEX_URL] cfg = cfg.auths[auth.INDEX_URL]
assert cfg['username'] == 'sakuya' assert cfg['username'] == 'sakuya'
assert cfg['password'] == 'izayoi' assert cfg['password'] == 'izayoi'
assert cfg['email'] == email assert cfg['email'] == email
@ -360,7 +362,7 @@ class LoadConfigTest(unittest.TestCase):
with open(dockercfg_path, 'w') as f: with open(dockercfg_path, 'w') as f:
json.dump(config, f) json.dump(config, f)
cfg = auth.load_config(dockercfg_path)['auths'] cfg = auth.load_config(dockercfg_path).auths
assert registry in cfg assert registry in cfg
assert cfg[registry] is not None assert cfg[registry] is not None
cfg = cfg[registry] cfg = cfg[registry]
@ -387,7 +389,7 @@ class LoadConfigTest(unittest.TestCase):
json.dump(config, f) json.dump(config, f)
with mock.patch.dict(os.environ, {'DOCKER_CONFIG': folder}): with mock.patch.dict(os.environ, {'DOCKER_CONFIG': folder}):
cfg = auth.load_config(None)['auths'] cfg = auth.load_config(None).auths
assert registry in cfg assert registry in cfg
assert cfg[registry] is not None assert cfg[registry] is not None
cfg = cfg[registry] cfg = cfg[registry]
@ -417,8 +419,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['auths'] assert registry in cfg.auths
cfg = cfg['auths'][registry] cfg = cfg.auths[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'
@ -446,8 +448,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['auths'] assert registry in cfg.auths
cfg = cfg['auths'][registry] cfg = cfg.auths[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'
@ -464,7 +466,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 == {'auths': {}} assert cfg._dct == {'auths': {}}
def test_load_config_invalid_auth_dict(self): def test_load_config_invalid_auth_dict(self):
folder = tempfile.mkdtemp() folder = tempfile.mkdtemp()
@ -479,7 +481,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 == {'auths': {'scarlet.net': {}}} assert cfg._dct == {'auths': {'scarlet.net': {}}}
def test_load_config_identity_token(self): def test_load_config_identity_token(self):
folder = tempfile.mkdtemp() folder = tempfile.mkdtemp()
@ -500,7 +502,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['auths'] assert registry in cfg.auths
cfg = cfg['auths'][registry] cfg = cfg.auths[registry]
assert 'IdentityToken' in cfg assert 'IdentityToken' in cfg
assert cfg['IdentityToken'] == token assert cfg['IdentityToken'] == token