From 4e895b863508b08cacb1201eeedaa2d60c48bf26 Mon Sep 17 00:00:00 2001 From: vimbaer Date: Wed, 10 Sep 2014 22:03:05 +0200 Subject: [PATCH] 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)