From 4e895b863508b08cacb1201eeedaa2d60c48bf26 Mon Sep 17 00:00:00 2001 From: vimbaer Date: Wed, 10 Sep 2014 22:03:05 +0200 Subject: [PATCH 1/3] Added passwordeval as an option for the general config section. If no password is provided the command provided as passwordeval will be called with username and hostname as arguments. --- tests/utils/test_main.py | 33 ++++++++++++++++++++++++++++++++- vdirsyncer/cli.py | 2 +- vdirsyncer/utils/__init__.py | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/tests/utils/test_main.py b/tests/utils/test_main.py index e960a45..bd01019 100644 --- a/tests/utils/test_main.py +++ b/tests/utils/test_main.py @@ -10,6 +10,9 @@ import click from click.testing import CliRunner +from tempfile import mkstemp +import os +import stat import pytest import requests @@ -108,6 +111,35 @@ def test_get_password_from_system_keyring(monkeypatch): assert _password == password +def test_get_password_from_evalcmd(): + username = 'my_username' + resource = 'http://example.com' + password = 'testpassword' + + fd, temp_path = mkstemp() + os.close(fd) + fp = open(temp_path, 'w') + fp.write('#!/bin/sh\n' + '[ "$1" != "my_username" ] && exit 1\n' + '[ "$2" != "example.com" ] && exit 1\n' + 'echo "{}"'.format(password)) + fp.close() + st = os.stat(temp_path) + os.chmod(temp_path, st.st_mode | stat.S_IEXEC) + + @doubleclick.click.command() + @doubleclick.click.pass_context + def fake_app(ctx): + ctx.obj = {'config' : ({'passwordeval' : temp_path},{},{})} + _password = utils.get_password(username, resource) + assert _password == password + + runner = CliRunner() + result = runner.invoke(fake_app) + assert not result.exception + os.remove(temp_path) + + def test_get_password_from_prompt(): getpass_calls = [] @@ -187,7 +219,6 @@ def test_get_password_from_cache(monkeypatch): ] - def test_get_class_init_args(): class Foobar(object): def __init__(self, foo, bar, baz=None): diff --git a/vdirsyncer/cli.py b/vdirsyncer/cli.py index 6078495..22a10fd 100644 --- a/vdirsyncer/cli.py +++ b/vdirsyncer/cli.py @@ -30,7 +30,7 @@ cli_logger = log.get(__name__) PROJECT_HOME = 'https://github.com/untitaker/vdirsyncer' DOCS_HOME = 'https://vdirsyncer.readthedocs.org/en/latest' -GENERAL_ALL = set(['processes', 'status_path']) +GENERAL_ALL = set(['processes', 'status_path', 'passwordeval']) GENERAL_REQUIRED = set(['status_path']) diff --git a/vdirsyncer/utils/__init__.py b/vdirsyncer/utils/__init__.py index 0dd09cb..8c86d25 100644 --- a/vdirsyncer/utils/__init__.py +++ b/vdirsyncer/utils/__init__.py @@ -97,7 +97,9 @@ def get_password(username, resource, _lock=threading.Lock()): 1. read password from netrc (and only the password, username in netrc will be ignored) 2. read password from keyring (keyring needs to be installed) - 3a ask user for the password + 3. read password from the command passed as passwordeval in the + general config section with username and host as parameters + 4a ask user for the password b save in keyring if installed and user agrees :param username: user's name on the server @@ -118,7 +120,7 @@ def get_password(username, resource, _lock=threading.Lock()): with _lock: host = urlparse.urlsplit(resource).hostname for func in (_password_from_cache, _password_from_netrc, - _password_from_keyring): + _password_from_keyring, _password_from_evalcmd): password = func(username, host) if password is not None: logger.debug('Got password for {} from {}' @@ -166,6 +168,33 @@ def _password_from_keyring(username, host): return keyring.get_password(password_key_prefix + host, username) +def _password_from_evalcmd(username, host): + '''evalcmd''' + import subprocess + + try: + general, _, _ = ctx.obj['config'] + _evalcmd = general.get('passwordeval', '') + except (KeyError, IndexError): + return None + + if _evalcmd == '': + return None + + evalcmd = expand_path(_evalcmd) + + try: + proc = subprocess.Popen([evalcmd, username, host], + stdout=subprocess.PIPE) + password = proc.stdout.read().strip() + except OSError, e: + logger.debug('Failed to execute evalcmd: {}\n{}'. + format(evalcmd, str(e))) + return None + + return password + + class _FingerprintAdapter(requests.adapters.HTTPAdapter): def __init__(self, fingerprint=None, **kwargs): self.fingerprint = str(fingerprint) From 64e9ef7dc3bcbf4f900ce8ed48669b3bc93e1100 Mon Sep 17 00:00:00 2001 From: vimbaer Date: Thu, 11 Sep 2014 23:33:44 +0200 Subject: [PATCH 2/3] Renamed evalcmd -> command, use py.test's tmpdir, allow arguments in passwordeval option. --- tests/utils/test_main.py | 21 ++++++++++----------- vdirsyncer/utils/__init__.py | 29 ++++++++++++++--------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/tests/utils/test_main.py b/tests/utils/test_main.py index bd01019..0b0d7f7 100644 --- a/tests/utils/test_main.py +++ b/tests/utils/test_main.py @@ -10,7 +10,6 @@ import click from click.testing import CliRunner -from tempfile import mkstemp import os import stat import pytest @@ -111,33 +110,33 @@ def test_get_password_from_system_keyring(monkeypatch): assert _password == password -def test_get_password_from_evalcmd(): +def test_get_password_from_command(tmpdir): username = 'my_username' resource = 'http://example.com' password = 'testpassword' + filename = 'command.sh' - fd, temp_path = mkstemp() - os.close(fd) - fp = open(temp_path, 'w') - fp.write('#!/bin/sh\n' + filepath = str(tmpdir) + '/' + filename + f = open(filepath, 'w') + f.write('#!/bin/sh\n' '[ "$1" != "my_username" ] && exit 1\n' '[ "$2" != "example.com" ] && exit 1\n' 'echo "{}"'.format(password)) - fp.close() - st = os.stat(temp_path) - os.chmod(temp_path, st.st_mode | stat.S_IEXEC) + f.close() + + st = os.stat(filepath) + os.chmod(filepath, st.st_mode | stat.S_IEXEC) @doubleclick.click.command() @doubleclick.click.pass_context def fake_app(ctx): - ctx.obj = {'config' : ({'passwordeval' : temp_path},{},{})} + ctx.obj = {'config' : ({'passwordeval' : filepath},{},{})} _password = utils.get_password(username, resource) assert _password == password runner = CliRunner() result = runner.invoke(fake_app) assert not result.exception - os.remove(temp_path) def test_get_password_from_prompt(): diff --git a/vdirsyncer/utils/__init__.py b/vdirsyncer/utils/__init__.py index 8c86d25..57815b7 100644 --- a/vdirsyncer/utils/__init__.py +++ b/vdirsyncer/utils/__init__.py @@ -119,8 +119,8 @@ def get_password(username, resource, _lock=threading.Lock()): with _lock: host = urlparse.urlsplit(resource).hostname - for func in (_password_from_cache, _password_from_netrc, - _password_from_keyring, _password_from_evalcmd): + for func in (_password_from_command, _password_from_cache, + _password_from_netrc, _password_from_keyring): password = func(username, host) if password is not None: logger.debug('Got password for {} from {}' @@ -168,28 +168,27 @@ def _password_from_keyring(username, host): return keyring.get_password(password_key_prefix + host, username) -def _password_from_evalcmd(username, host): - '''evalcmd''' +def _password_from_command(username, host): + '''command''' import subprocess try: general, _, _ = ctx.obj['config'] - _evalcmd = general.get('passwordeval', '') - except (KeyError, IndexError): + _command = general['passwordeval'].split() + except (IndexError, KeyError): return None - if _evalcmd == '': - return None - - evalcmd = expand_path(_evalcmd) + command = [expand_path(_command[0])] + if len(_command) > 1: + command += _command[1:] try: - proc = subprocess.Popen([evalcmd, username, host], + proc = subprocess.Popen(command + [username, host], stdout=subprocess.PIPE) - password = proc.stdout.read().strip() - except OSError, e: - logger.debug('Failed to execute evalcmd: {}\n{}'. - format(evalcmd, str(e))) + password = proc.stdout.read().decode('utf-8').strip() + except OSError as e: + logger.debug('Failed to execute command: {}\n{}'. + format(" ".join(command), str(e))) return None return password From 23a4a96cb9369c6cdeed581b6e531a231ba060f2 Mon Sep 17 00:00:00 2001 From: vimbaer Date: Fri, 12 Sep 2014 00:02:11 +0200 Subject: [PATCH 3/3] Fixed style. --- vdirsyncer/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vdirsyncer/utils/__init__.py b/vdirsyncer/utils/__init__.py index 57815b7..2fdedcf 100644 --- a/vdirsyncer/utils/__init__.py +++ b/vdirsyncer/utils/__init__.py @@ -188,7 +188,7 @@ def _password_from_command(username, host): password = proc.stdout.read().decode('utf-8').strip() except OSError as e: logger.debug('Failed to execute command: {}\n{}'. - format(" ".join(command), str(e))) + format(" ".join(command), str(e))) return None return password