Skip to content

Commit

Permalink
Add support for using credstash as a secret store (home-assistant#8494)
Browse files Browse the repository at this point in the history
  • Loading branch information
justin8 authored and balloob committed Jul 24, 2017
1 parent 9d9ca64 commit 98568b5
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 0 deletions.
71 changes: 71 additions & 0 deletions homeassistant/scripts/credstash.py
Original file line number Diff line number Diff line change
@@ -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))
14 changes: 14 additions & 0 deletions homeassistant/util/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
except ImportError:
keyring = None

try:
import credstash
except ImportError:
credstash = None

from homeassistant.exceptions import HomeAssistantError

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
11 changes: 11 additions & 0 deletions tests/util/test_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import io
import os
import unittest
import logging
from unittest.mock import patch

from homeassistant.exceptions import HomeAssistantError
Expand Down Expand Up @@ -372,6 +373,16 @@ def test_secrets_keyring(self):
_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):
Expand Down

0 comments on commit 98568b5

Please sign in to comment.