diff --git a/homeassistant/scripts/credstash.py b/homeassistant/scripts/credstash.py new file mode 100644 index 0000000000000000000000000000000000000000..9ba945626e2e15ef55735789a247787cb561e4f1 --- /dev/null +++ b/homeassistant/scripts/credstash.py @@ -0,0 +1,71 @@ +"""Script to get, put and delete secrets stored in credstash.""" +import argparse +import getpass + +from homeassistant.util.yaml import _SECRET_NAMESPACE + +REQUIREMENTS = ['credstash==1.13.2', 'botocore==1.4.93'] + + +def run(args): + """Handle credstash script.""" + parser = argparse.ArgumentParser( + description=("Modify Home-Assistant secrets in credstash." + "Use the secrets in configuration files with: " + "!secret <name>")) + parser.add_argument( + '--script', choices=['credstash']) + parser.add_argument( + 'action', choices=['get', 'put', 'del', 'list'], + help="Get, put or delete a secret, or list all available secrets") + parser.add_argument( + 'name', help="Name of the secret", nargs='?', default=None) + parser.add_argument( + 'value', help="The value to save when putting a secret", + nargs='?', default=None) + + import credstash + import botocore + + args = parser.parse_args(args) + table = _SECRET_NAMESPACE + + try: + credstash.listSecrets(table=table) + except botocore.errorfactory.ClientError: + credstash.createDdbTable(table=table) + + if args.action == 'list': + secrets = [i['name'] for i in credstash.listSecrets(table=table)] + deduped_secrets = sorted(set(secrets)) + + print('Saved secrets:') + for secret in deduped_secrets: + print(secret) + return 0 + + if args.name is None: + parser.print_help() + return 1 + + if args.action == 'put': + if args.value: + the_secret = args.value + else: + the_secret = getpass.getpass('Please enter the secret for {}: ' + .format(args.name)) + current_version = credstash.getHighestVersion(args.name, table=table) + credstash.putSecret(args.name, + the_secret, + version=int(current_version) + 1, + table=table) + print('Secret {} put successfully'.format(args.name)) + elif args.action == 'get': + the_secret = credstash.getSecret(args.name, table=table) + if the_secret is None: + print('Secret {} not found'.format(args.name)) + else: + print('Secret {}={}'.format(args.name, the_secret)) + elif args.action == 'del': + credstash.deleteSecrets(args.name, table=table) + print('Deleted secret {}'.format(args.name)) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 7827f484fdf2d6eca66050077baa1ab9deaff9fa..fb682ac6f197dd240cd587b58ef5b0ff1df13d6f 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -12,6 +12,11 @@ try: except ImportError: keyring = None +try: + import credstash +except ImportError: + credstash = None + from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) @@ -257,6 +262,15 @@ def _secret_yaml(loader: SafeLineLoader, _LOGGER.debug("Secret %s retrieved from keyring", node.value) return pwd + if credstash: + try: + pwd = credstash.getSecret(node.value, table=_SECRET_NAMESPACE) + if pwd: + _LOGGER.debug("Secret %s retrieved from credstash", node.value) + return pwd + except credstash.ItemNotFound: + pass + _LOGGER.error("Secret %s not defined", node.value) raise HomeAssistantError(node.value) diff --git a/pylintrc b/pylintrc index e94cbffe9f9369377742b4c5995dc86a0223a7f2..1ed8d2af3363ec7f462441152a84008345055615 100644 --- a/pylintrc +++ b/pylintrc @@ -14,6 +14,8 @@ reports=no # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing +generated-members=botocore.errorfactory + disable= abstract-class-little-used, abstract-class-not-used, diff --git a/requirements_all.txt b/requirements_all.txt index 10036a0361570a296a39157726ad5c0e0a7fa0a5..aa99cd03701836adc6a1184660a18991fa810977 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,6 +115,9 @@ blockchain==1.3.3 # homeassistant.components.tts.amazon_polly boto3==1.4.3 +# homeassistant.scripts.credstash +botocore==1.4.93 + # homeassistant.components.sensor.broadlink # homeassistant.components.switch.broadlink broadlink==0.5 @@ -136,6 +139,9 @@ colorlog>2.1,<3 # homeassistant.components.binary_sensor.concord232 concord232==0.14 +# homeassistant.scripts.credstash +credstash==1.13.2 + # homeassistant.components.sensor.crimereports crimereports==1.0.0 diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 0ccb6f5d6d08dec197cfb9e52337408527b1c7f2..c2eda2401f50f64e3ad76e434eb708a0d973771f 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -2,6 +2,7 @@ import io import os import unittest +import logging from unittest.mock import patch from homeassistant.exceptions import HomeAssistantError @@ -372,6 +373,16 @@ class TestSecrets(unittest.TestCase): _yaml = load_yaml(self._yaml_path, yaml_str) self.assertEqual({'http': {'api_password': 'yeah'}}, _yaml) + @patch.object(yaml, 'credstash') + def test_secrets_credstash(self, mock_credstash): + """Test credstash fallback & get_password.""" + mock_credstash.getSecret.return_value = 'yeah' + yaml_str = 'http:\n api_password: !secret http_pw_credstash' + _yaml = load_yaml(self._yaml_path, yaml_str) + log = logging.getLogger() + log.error(_yaml['http']) + self.assertEqual({'api_password': 'yeah'}, _yaml['http']) + def test_secrets_logger_removed(self): """Ensure logger: debug was removed.""" with self.assertRaises(yaml.HomeAssistantError):