forked from docker/docker-py
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make dockerpycreds part of the SDK under docker.credentials
Signed-off-by: Joffrey F <[email protected]>
- Loading branch information
Showing
18 changed files
with
341 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# flake8: noqa | ||
from .store import Store | ||
from .errors import StoreError, CredentialsNotFound | ||
from .constants import * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
PROGRAM_PREFIX = 'docker-credential-' | ||
DEFAULT_LINUX_STORE = 'secretservice' | ||
DEFAULT_OSX_STORE = 'osxkeychain' | ||
DEFAULT_WIN32_STORE = 'wincred' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
ARG PYTHON_VERSION=3.6 | ||
FROM python:$PYTHON_VERSION-jessie | ||
RUN apt-get update && apt-get -y install \ | ||
gnupg2 \ | ||
pass \ | ||
curl | ||
|
||
COPY ./tests/gpg-keys /gpg-keys | ||
RUN gpg2 --import gpg-keys/secret | ||
RUN gpg2 --import-ownertrust gpg-keys/ownertrust | ||
RUN yes | pass init $(gpg2 --no-auto-check-trustdb --list-secret-keys | grep ^sec | cut -d/ -f2 | cut -d" " -f1) | ||
RUN gpg2 --check-trustdb | ||
ARG CREDSTORE_VERSION=v0.6.0 | ||
RUN curl -sSL -o /opt/docker-credential-pass.tar.gz \ | ||
https://github.com/docker/docker-credential-helpers/releases/download/$CREDSTORE_VERSION/docker-credential-pass-$CREDSTORE_VERSION-amd64.tar.gz && \ | ||
tar -xf /opt/docker-credential-pass.tar.gz -O > /usr/local/bin/docker-credential-pass && \ | ||
rm -rf /opt/docker-credential-pass.tar.gz && \ | ||
chmod +x /usr/local/bin/docker-credential-pass | ||
|
||
WORKDIR /src | ||
COPY requirements.txt /src/requirements.txt | ||
RUN pip install -r requirements.txt | ||
|
||
COPY test-requirements.txt /src/test-requirements.txt | ||
RUN pip install -r test-requirements.txt | ||
|
||
COPY . /src | ||
RUN pip install . |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# List of assigned trustvalues, created Wed 25 Apr 2018 01:28:17 PM PDT | ||
# (Use "gpg --import-ownertrust" to restore them) | ||
9781B87DAB042E6FD51388A5464ED987A7B21401:6: |
Binary file not shown.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
#!/usr/bin/sh | ||
haveged | ||
gpg --batch --gen-key <<-EOF | ||
%echo Generating a standard key | ||
Key-Type: DSA | ||
Key-Length: 1024 | ||
Subkey-Type: ELG-E | ||
Subkey-Length: 1024 | ||
Name-Real: Sakuya Izayoi | ||
Name-Email: [email protected] | ||
Expire-Date: 0 | ||
EOF |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import os | ||
import random | ||
import sys | ||
|
||
import pytest | ||
import six | ||
from distutils.spawn import find_executable | ||
|
||
from docker.credentials import ( | ||
CredentialsNotFound, Store, StoreError, DEFAULT_LINUX_STORE, | ||
DEFAULT_OSX_STORE | ||
) | ||
|
||
|
||
class TestStore(object): | ||
def teardown_method(self): | ||
for server in self.tmp_keys: | ||
try: | ||
self.store.erase(server) | ||
except StoreError: | ||
pass | ||
|
||
def setup_method(self): | ||
self.tmp_keys = [] | ||
if sys.platform.startswith('linux'): | ||
if find_executable('docker-credential-' + DEFAULT_LINUX_STORE): | ||
self.store = Store(DEFAULT_LINUX_STORE) | ||
elif find_executable('docker-credential-pass'): | ||
self.store = Store('pass') | ||
else: | ||
raise Exception('No supported docker-credential store in PATH') | ||
elif sys.platform.startswith('darwin'): | ||
self.store = Store(DEFAULT_OSX_STORE) | ||
|
||
def get_random_servername(self): | ||
res = 'pycreds_test_{:x}'.format(random.getrandbits(32)) | ||
self.tmp_keys.append(res) | ||
return res | ||
|
||
def test_store_and_get(self): | ||
key = self.get_random_servername() | ||
self.store.store(server=key, username='user', secret='pass') | ||
data = self.store.get(key) | ||
assert data == { | ||
'ServerURL': key, | ||
'Username': 'user', | ||
'Secret': 'pass' | ||
} | ||
|
||
def test_get_nonexistent(self): | ||
key = self.get_random_servername() | ||
with pytest.raises(CredentialsNotFound): | ||
self.store.get(key) | ||
|
||
def test_store_and_erase(self): | ||
key = self.get_random_servername() | ||
self.store.store(server=key, username='user', secret='pass') | ||
self.store.erase(key) | ||
with pytest.raises(CredentialsNotFound): | ||
self.store.get(key) | ||
|
||
def test_unicode_strings(self): | ||
key = self.get_random_servername() | ||
key = six.u(key) | ||
self.store.store(server=key, username='user', secret='pass') | ||
data = self.store.get(key) | ||
assert data | ||
self.store.erase(key) | ||
with pytest.raises(CredentialsNotFound): | ||
self.store.get(key) | ||
|
||
def test_list(self): | ||
names = (self.get_random_servername(), self.get_random_servername()) | ||
self.store.store(names[0], username='sakuya', secret='izayoi') | ||
self.store.store(names[1], username='reimu', secret='hakurei') | ||
data = self.store.list() | ||
assert names[0] in data | ||
assert data[names[0]] == 'sakuya' | ||
assert names[1] in data | ||
assert data[names[1]] == 'reimu' | ||
|
||
def test_execute_with_env_override(self): | ||
self.store.exe = 'env' | ||
self.store.environment = {'FOO': 'bar'} | ||
data = self.store._execute('--null', '') | ||
assert b'\0FOO=bar\0' in data | ||
assert 'FOO' not in os.environ |
Oops, something went wrong.