diff --git a/tests/utils/test_main.py b/tests/utils/test_main.py index e960a45..0b0d7f7 100644 --- a/tests/utils/test_main.py +++ b/tests/utils/test_main.py @@ -10,6 +10,8 @@ import click from click.testing import CliRunner +import os +import stat import pytest import requests @@ -108,6 +110,35 @@ def test_get_password_from_system_keyring(monkeypatch): assert _password == password +def test_get_password_from_command(tmpdir): + username = 'my_username' + resource = 'http://example.com' + password = 'testpassword' + filename = 'command.sh' + + 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)) + 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' : filepath},{},{})} + _password = utils.get_password(username, resource) + assert _password == password + + runner = CliRunner() + result = runner.invoke(fake_app) + assert not result.exception + + def test_get_password_from_prompt(): getpass_calls = [] @@ -187,7 +218,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..2fdedcf 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 @@ -117,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): + 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 {}' @@ -166,6 +168,32 @@ def _password_from_keyring(username, host): return keyring.get_password(password_key_prefix + host, username) +def _password_from_command(username, host): + '''command''' + import subprocess + + try: + general, _, _ = ctx.obj['config'] + _command = general['passwordeval'].split() + except (IndexError, KeyError): + return None + + command = [expand_path(_command[0])] + if len(_command) > 1: + command += _command[1:] + + try: + proc = subprocess.Popen(command + [username, host], + stdout=subprocess.PIPE) + 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 + + class _FingerprintAdapter(requests.adapters.HTTPAdapter): def __init__(self, fingerprint=None, **kwargs): self.fingerprint = str(fingerprint)