diff options
author | Joffrey F <joffrey@docker.com> | 2019-01-09 16:18:21 -0800 |
---|---|---|
committer | Joffrey F <joffrey@docker.com> | 2019-04-30 23:37:55 -0700 |
commit | a823acc2cae10c4635db2fb963cc37d8a23cc0c4 (patch) | |
tree | 6792cf991f7837a62f2ff2ca9c0f1ca6e451dabe /docker/credentials | |
parent | 41e1c054268104df1412f413245dc4b12951143e (diff) | |
download | docker-py-a823acc2cae10c4635db2fb963cc37d8a23cc0c4.tar.gz |
Make dockerpycreds part of the SDK under docker.credentials
Signed-off-by: Joffrey F <joffrey@docker.com>
Diffstat (limited to 'docker/credentials')
-rw-r--r-- | docker/credentials/__init__.py | 4 | ||||
-rw-r--r-- | docker/credentials/constants.py | 4 | ||||
-rw-r--r-- | docker/credentials/errors.py | 25 | ||||
-rw-r--r-- | docker/credentials/store.py | 107 | ||||
-rw-r--r-- | docker/credentials/utils.py | 38 |
5 files changed, 178 insertions, 0 deletions
diff --git a/docker/credentials/__init__.py b/docker/credentials/__init__.py new file mode 100644 index 0000000..31ad28e --- /dev/null +++ b/docker/credentials/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa +from .store import Store +from .errors import StoreError, CredentialsNotFound +from .constants import * diff --git a/docker/credentials/constants.py b/docker/credentials/constants.py new file mode 100644 index 0000000..6a82d8d --- /dev/null +++ b/docker/credentials/constants.py @@ -0,0 +1,4 @@ +PROGRAM_PREFIX = 'docker-credential-' +DEFAULT_LINUX_STORE = 'secretservice' +DEFAULT_OSX_STORE = 'osxkeychain' +DEFAULT_WIN32_STORE = 'wincred' diff --git a/docker/credentials/errors.py b/docker/credentials/errors.py new file mode 100644 index 0000000..42a1bc1 --- /dev/null +++ b/docker/credentials/errors.py @@ -0,0 +1,25 @@ +class StoreError(RuntimeError): + pass + + +class CredentialsNotFound(StoreError): + pass + + +class InitializationError(StoreError): + pass + + +def process_store_error(cpe, program): + message = cpe.output.decode('utf-8') + if 'credentials not found in native keychain' in message: + return CredentialsNotFound( + 'No matching credentials in {}'.format( + program + ) + ) + return StoreError( + 'Credentials store {} exited with "{}".'.format( + program, cpe.output.decode('utf-8').strip() + ) + ) diff --git a/docker/credentials/store.py b/docker/credentials/store.py new file mode 100644 index 0000000..3f51e4a --- /dev/null +++ b/docker/credentials/store.py @@ -0,0 +1,107 @@ +import json +import os +import subprocess + +import six + +from . import constants +from . import errors +from .utils import create_environment_dict +from .utils import find_executable + + +class Store(object): + def __init__(self, program, environment=None): + """ Create a store object that acts as an interface to + perform the basic operations for storing, retrieving + and erasing credentials using `program`. + """ + self.program = constants.PROGRAM_PREFIX + program + self.exe = find_executable(self.program) + self.environment = environment + if self.exe is None: + raise errors.InitializationError( + '{} not installed or not available in PATH'.format( + self.program + ) + ) + + def get(self, server): + """ Retrieve credentials for `server`. If no credentials are found, + a `StoreError` will be raised. + """ + if not isinstance(server, six.binary_type): + server = server.encode('utf-8') + data = self._execute('get', server) + result = json.loads(data.decode('utf-8')) + + # docker-credential-pass will return an object for inexistent servers + # whereas other helpers will exit with returncode != 0. For + # consistency, if no significant data is returned, + # raise CredentialsNotFound + if result['Username'] == '' and result['Secret'] == '': + raise errors.CredentialsNotFound( + 'No matching credentials in {}'.format(self.program) + ) + + return result + + def store(self, server, username, secret): + """ Store credentials for `server`. Raises a `StoreError` if an error + occurs. + """ + data_input = json.dumps({ + 'ServerURL': server, + 'Username': username, + 'Secret': secret + }).encode('utf-8') + return self._execute('store', data_input) + + def erase(self, server): + """ Erase credentials for `server`. Raises a `StoreError` if an error + occurs. + """ + if not isinstance(server, six.binary_type): + server = server.encode('utf-8') + self._execute('erase', server) + + def list(self): + """ List stored credentials. Requires v0.4.0+ of the helper. + """ + data = self._execute('list', None) + return json.loads(data.decode('utf-8')) + + def _execute(self, subcmd, data_input): + output = None + env = create_environment_dict(self.environment) + try: + if six.PY3: + output = subprocess.check_output( + [self.exe, subcmd], input=data_input, env=env, + ) + else: + process = subprocess.Popen( + [self.exe, subcmd], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, env=env, + ) + output, err = process.communicate(data_input) + if process.returncode != 0: + raise subprocess.CalledProcessError( + returncode=process.returncode, cmd='', output=output + ) + except subprocess.CalledProcessError as e: + raise errors.process_store_error(e, self.program) + except OSError as e: + if e.errno == os.errno.ENOENT: + raise errors.StoreError( + '{} not installed or not available in PATH'.format( + self.program + ) + ) + else: + raise errors.StoreError( + 'Unexpected OS error "{}", errno={}'.format( + e.strerror, e.errno + ) + ) + return output diff --git a/docker/credentials/utils.py b/docker/credentials/utils.py new file mode 100644 index 0000000..3f720ef --- /dev/null +++ b/docker/credentials/utils.py @@ -0,0 +1,38 @@ +import distutils.spawn +import os +import sys + + +def find_executable(executable, path=None): + """ + As distutils.spawn.find_executable, but on Windows, look up + every extension declared in PATHEXT instead of just `.exe` + """ + if sys.platform != 'win32': + return distutils.spawn.find_executable(executable, path) + + if path is None: + path = os.environ['PATH'] + + paths = path.split(os.pathsep) + extensions = os.environ.get('PATHEXT', '.exe').split(os.pathsep) + base, ext = os.path.splitext(executable) + + if not os.path.isfile(executable): + for p in paths: + for ext in extensions: + f = os.path.join(p, base + ext) + if os.path.isfile(f): + return f + return None + else: + return executable + + +def create_environment_dict(overrides): + """ + Create and return a copy of os.environ with the specified overrides + """ + result = os.environ.copy() + result.update(overrides or {}) + return result |