Merge pull request #120 from mpetazzoni/improve-auth

Rework auth config loading and use of auth data for login/pull/push
This commit is contained in:
Joffrey F 2014-01-09 11:00:55 -08:00
commit 2ac5cca84d
4 changed files with 140 additions and 97 deletions

View File

@ -13,6 +13,7 @@
# limitations under the License. # limitations under the License.
import base64 import base64
import fileinput
import json import json
import os import os
@ -21,6 +22,7 @@ import six
import docker.utils as utils import docker.utils as utils
INDEX_URL = 'https://index.docker.io/v1/' INDEX_URL = 'https://index.docker.io/v1/'
DOCKER_CONFIG_FILENAME = '.dockercfg'
def swap_protocol(url): def swap_protocol(url):
@ -59,12 +61,15 @@ def resolve_repository_name(repo_name):
return expand_registry_url(parts[0]), parts[1] return expand_registry_url(parts[0]), parts[1]
def resolve_authconfig(authconfig, registry): def resolve_authconfig(authconfig, registry=None):
default = {} """Return the authentication data from the given auth configuration for a
if registry == INDEX_URL or registry == '': specific registry. We'll do our best to infer the correct URL for the
# default to the index server registry, trying both http and https schemes. Returns an empty dictionnary
return authconfig['Configs'].get(INDEX_URL, default) if no data exists."""
# if its not the index server there are three cases: # Default to the public index server
registry = registry or INDEX_URL
# Ff its not the index server there are three cases:
# #
# 1. this is a full config url -> it should be used as is # 1. this is a full config url -> it should be used as is
# 2. it could be a full url, but with the wrong protocol # 2. it could be a full url, but with the wrong protocol
@ -77,11 +82,9 @@ def resolve_authconfig(authconfig, registry):
if not registry.startswith('http:') and not registry.startswith('https:'): if not registry.startswith('http:') and not registry.startswith('https:'):
registry = 'https://' + registry registry = 'https://' + registry
if registry in authconfig['Configs']: if registry in authconfig:
return authconfig['Configs'][registry] return authconfig[registry]
elif swap_protocol(registry) in authconfig['Configs']: return authconfig.get(swap_protocol(registry), None)
return authconfig['Configs'][swap_protocol(registry)]
return default
def decode_auth(auth): def decode_auth(auth):
@ -98,38 +101,53 @@ def encode_header(auth):
def load_config(root=None): def load_config(root=None):
root = root or os.environ['HOME'] """Loads authentication data from a Docker configuration file in the given
config = { root directory."""
'Configs': {}, conf = {}
'rootPath': root data = None
}
config_file = os.path.join(root, '.dockercfg') config_file = os.path.join(root or os.environ.get('HOME', '.'),
if not os.path.exists(config_file): DOCKER_CONFIG_FILENAME)
return config
f = open(config_file) # First try as JSON
try: try:
config['Configs'] = json.load(f) with open(config_file) as f:
for k, conf in six.iteritems(config['Configs']): conf = {}
conf['Username'], conf['Password'] = decode_auth(conf['auth']) for registry, entry in six.iteritems(json.load(f)):
del conf['auth'] username, password = decode_auth(entry['auth'])
config['Configs'][k] = conf conf[registry] = {
except Exception: 'username': username,
f.seek(0) 'password': password,
buf = [] 'email': entry['email'],
for line in f: 'serveraddress': registry,
k, v = line.split(' = ')
buf.append(v)
if len(buf) < 2:
raise Exception("The Auth config file is empty")
user, pwd = decode_auth(buf[0])
config['Configs'][INDEX_URL] = {
'Username': user,
'Password': pwd,
'Email': buf[1]
} }
finally: return conf
f.close() except:
pass
return config # If that fails, we assume the configuration file contains a single
# authentication token for the public registry in the following format:
#
# auth = AUTH_TOKEN
# email = email@domain.com
try:
data = []
for line in fileinput.input(config_file):
data.append(line.strip().split(' = ')[1])
if len(data) < 2:
# Not enough data
raise Exception('Invalid or empty configuration file!')
username, password = decode_auth(data[0])
conf[INDEX_URL] = {
'username': username,
'password': password,
'email': data[1],
'serveraddress': INDEX_URL,
}
return conf
except:
pass
# If all fails, return an empty config
return {}

View File

@ -75,12 +75,9 @@ class Client(requests.Session):
self.base_url = base_url self.base_url = base_url
self._version = version self._version = version
self._timeout = timeout self._timeout = timeout
self._auth_configs = auth.load_config()
self.mount('unix://', unixconn.UnixAdapter(base_url, timeout)) self.mount('unix://', unixconn.UnixAdapter(base_url, timeout))
try:
self._cfg = auth.load_config()
except Exception:
pass
def _set_request_timeout(self, kwargs): def _set_request_timeout(self, kwargs):
"""Prepare the kwargs for an HTTP request by inserting the timeout """Prepare the kwargs for an HTTP request by inserting the timeout
@ -516,24 +513,33 @@ class Client(requests.Session):
self._raise_for_status(res) self._raise_for_status(res)
def login(self, username, password=None, email=None, registry=None): def login(self, username, password=None, email=None, registry=None,
url = self._url("/auth") reauth=False):
if registry is None: # If we don't have any auth data so far, try reloading the config file
registry = auth.INDEX_URL # one more time in case anything showed up in there.
if getattr(self, '_cfg', None) is None: if not self._auth_configs:
self._cfg = auth.load_config() self._auth_configs = auth.load_config()
authcfg = auth.resolve_authconfig(self._cfg, registry)
if 'username' in authcfg and authcfg['username'] == username: registry = registry or auth.INDEX_URL
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
# If we found an existing auth config for this registry and username
# combination, we can return it immediately unless reauth is requested.
if authcfg and authcfg.get('username', None) == username \
and not reauth:
return authcfg return authcfg
req_data = { req_data = {
'username': username, 'username': username,
'password': password, 'password': password,
'email': email 'email': email,
'serveraddress': registry,
} }
res = self._result(self._post_json(url, data=req_data), True)
if res['Status'] == 'Login Succeeded': response = self._post_json(self._url('/auth'), data=req_data)
self._cfg['Configs'][registry] = req_data if response.status_code == 200:
return res self._auth_configs[registry] = req_data
return self._result(response, json=True)
def logs(self, container, stdout=True, stderr=True, stream=False): def logs(self, container, stdout=True, stderr=True, stream=False):
if isinstance(container, dict): if isinstance(container, dict):
@ -582,16 +588,20 @@ class Client(requests.Session):
headers = {} headers = {}
if utils.compare_version('1.5', self._version) >= 0: if utils.compare_version('1.5', self._version) >= 0:
if getattr(self, '_cfg', None) is None: # If we don't have any auth data so far, try reloading the config
self._cfg = auth.load_config() # file one more time in case anything showed up in there.
authcfg = auth.resolve_authconfig(self._cfg, registry) if not self._auth_configs:
# do not fail if no atuhentication exists self._auth_configs = auth.load_config()
# for this specific registry as we can have a readonly pull authcfg = auth.resolve_authconfig(self._auth_configs, registry)
# Do not fail here if no atuhentication exists for this specific
# registry as we can have a readonly pull. Just put the header if
# we can.
if authcfg: if authcfg:
headers['X-Registry-Auth'] = auth.encode_header(authcfg) headers['X-Registry-Auth'] = auth.encode_header(authcfg)
u = self._url("/images/create")
response = self._post(u, params=params, headers=headers, stream=stream, response = self._post(self._url('/images/create'), params=params,
timeout=None) headers=headers, stream=stream, timeout=None)
if stream: if stream:
return self._stream_helper(response) return self._stream_helper(response)
@ -602,26 +612,26 @@ class Client(requests.Session):
registry, repo_name = auth.resolve_repository_name(repository) registry, repo_name = auth.resolve_repository_name(repository)
u = self._url("/images/{0}/push".format(repository)) u = self._url("/images/{0}/push".format(repository))
headers = {} headers = {}
if getattr(self, '_cfg', None) is None:
self._cfg = auth.load_config()
authcfg = auth.resolve_authconfig(self._cfg, registry)
if utils.compare_version('1.5', self._version) >= 0: if utils.compare_version('1.5', self._version) >= 0:
# do not fail if no atuhentication exists # If we don't have any auth data so far, try reloading the config
# for this specific registry as we can have an anon push # file one more time in case anything showed up in there.
if not self._auth_configs:
self._auth_configs = auth.load_config()
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
# Do not fail here if no atuhentication exists for this specific
# registry as we can have a readonly pull. Just put the header if
# we can.
if authcfg: if authcfg:
headers['X-Registry-Auth'] = auth.encode_header(authcfg) headers['X-Registry-Auth'] = auth.encode_header(authcfg)
if stream: response = self._post_json(u, None, headers=headers, stream=stream)
return self._stream_helper(
self._post_json(u, None, headers=headers, stream=True))
else: else:
return self._result( response = self._post_json(u, authcfg, stream=stream)
self._post_json(u, None, headers=headers, stream=False))
if stream: return stream and self._stream_helper(response) \
return self._stream_helper( or self._result(response)
self._post_json(u, authcfg, stream=True))
else:
return self._result(self._post_json(u, authcfg, stream=False))
def remove_container(self, container, v=False, link=False): def remove_container(self, container, v=False, link=False):
if isinstance(container, dict): if isinstance(container, dict):

View File

@ -775,11 +775,29 @@ class TestLoadConfig(BaseTestCase):
f.write('email = sakuya@scarlet.net') f.write('email = sakuya@scarlet.net')
f.close() f.close()
cfg = docker.auth.load_config(folder) cfg = docker.auth.load_config(folder)
self.assertNotEqual(cfg['Configs'][docker.auth.INDEX_URL], None) self.assertNotEqual(cfg[docker.auth.INDEX_URL], None)
cfg = cfg['Configs'][docker.auth.INDEX_URL] cfg = cfg[docker.auth.INDEX_URL]
self.assertEqual(cfg['Username'], b'sakuya') self.assertEqual(cfg['username'], b'sakuya')
self.assertEqual(cfg['Password'], b'izayoi') self.assertEqual(cfg['password'], b'izayoi')
self.assertEqual(cfg['Email'], 'sakuya@scarlet.net') self.assertEqual(cfg['email'], 'sakuya@scarlet.net')
self.assertEqual(cfg.get('Auth'), None)
class TestLoadJSONConfig(BaseTestCase):
def runTest(self):
folder = tempfile.mkdtemp()
f = open(os.path.join(folder, '.dockercfg'), 'w')
auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
email_ = 'sakuya@scarlet.net'
f.write('{{"{}": {{"auth": "{}", "email": "{}"}}}}\n'.format(
docker.auth.INDEX_URL, auth_, email_))
f.close()
cfg = docker.auth.load_config(folder)
self.assertNotEqual(cfg[docker.auth.INDEX_URL], None)
cfg = cfg[docker.auth.INDEX_URL]
self.assertEqual(cfg['username'], b'sakuya')
self.assertEqual(cfg['password'], b'izayoi')
self.assertEqual(cfg['email'], 'sakuya@scarlet.net')
self.assertEqual(cfg.get('Auth'), None) self.assertEqual(cfg.get('Auth'), None)

View File

@ -1018,10 +1018,6 @@ class DockerClientTest(unittest.TestCase):
folder = tempfile.mkdtemp() folder = tempfile.mkdtemp()
cfg = docker.auth.load_config(folder) cfg = docker.auth.load_config(folder)
self.assertTrue(cfg is not None) self.assertTrue(cfg is not None)
self.assertTrue('Configs' in cfg)
self.assertEqual(cfg['Configs'], {})
self.assertTrue('rootPath' in cfg)
self.assertEqual(cfg['rootPath'], folder)
def test_load_config(self): def test_load_config(self):
folder = tempfile.mkdtemp() folder = tempfile.mkdtemp()
@ -1031,12 +1027,13 @@ class DockerClientTest(unittest.TestCase):
f.write('email = sakuya@scarlet.net') f.write('email = sakuya@scarlet.net')
f.close() f.close()
cfg = docker.auth.load_config(folder) cfg = docker.auth.load_config(folder)
self.assertNotEqual(cfg['Configs'][docker.auth.INDEX_URL], None) self.assertTrue(docker.auth.INDEX_URL in cfg)
cfg = cfg['Configs'][docker.auth.INDEX_URL] self.assertNotEqual(cfg[docker.auth.INDEX_URL], None)
self.assertEqual(cfg['Username'], 'sakuya') cfg = cfg[docker.auth.INDEX_URL]
self.assertEqual(cfg['Password'], 'izayoi') self.assertEqual(cfg['username'], 'sakuya')
self.assertEqual(cfg['Email'], 'sakuya@scarlet.net') self.assertEqual(cfg['password'], 'izayoi')
self.assertEqual(cfg.get('Auth'), None) self.assertEqual(cfg['email'], 'sakuya@scarlet.net')
self.assertEqual(cfg.get('auth'), None)
if __name__ == '__main__': if __name__ == '__main__':