# Copyright 2017 Hynek Schlawack # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Handling of sensitive data. """ from __future__ import absolute_import, division, print_function import codecs import logging import sys from configparser import NoOptionError, RawConfigParser import attr from ._environ_config import CNF_KEY, RAISE, _ConfigEntry from .exceptions import MissingSecretError log = logging.getLogger(__name__) @attr.s class INISecrets(object): """ Load secrets from an `INI file <https://en.wikipedia.org/wiki/INI_file>`_ using `configparser.RawConfigParser`. """ section = attr.ib() _cfg = attr.ib(default=None) _env_name = attr.ib(default=None) _env_default = attr.ib(default=None) @classmethod def from_path(cls, path, section="secrets"): """ Look for secrets in *section* of *path*. :param str path: A path to an INI file. :param str section: The section in the INI file to read the secrets from. """ return cls(section, _load_ini(path), None, None) @classmethod def from_path_in_env(cls, env_name, default=None, section="secrets"): """ Get the path from the environment variable *env_name* **at runtime** and then load the secrets from it. This allows you to overwrite the path to the secrets file in development. :param str env_name: Environment variable that is used to determine the path of the secrets file. :param str default: The default path to load from. :param str section: The section in the INI file to read the secrets from. """ return cls(section, None, env_name, default) def secret( self, default=RAISE, converter=None, name=None, section=None, help=None ): """ Declare a secret on an `environ.config`-decorated class. :param str section: Overwrite the section where to look for the values. Other parameters work just like in `environ.var`. """ if section is None: section = self.section return attr.ib( default=default, metadata={ CNF_KEY: _ConfigEntry(name, default, None, self._get, help), CNF_INI_SECRET_KEY: _INIConfig(section), }, converter=converter, ) def _get(self, environ, metadata, prefix, name): # Delayed loading. if self._cfg is None and self._env_name is not None: log.debug("looking for env var '%s'." % (self._env_name,)) self._cfg = _load_ini( environ.get(self._env_name, self._env_default) ) ce = metadata[CNF_KEY] ic = metadata[CNF_INI_SECRET_KEY] section = ic.section if ce.name is not None: var = ce.name else: var = "_".join((prefix + (name,))) try: log.debug("looking for '%s' in section '%s'." % (var, section)) return _SecretStr(self._cfg.get(section, var)) except NoOptionError: if isinstance(ce.default, attr.Factory): return attr.NOTHING elif ce.default is not RAISE: return ce.default raise MissingSecretError(var) @attr.s class VaultEnvSecrets(object): """ Loads secrets from environment variables that follow the naming style from `envconsul <https://github.com/hashicorp/envconsul>`_. """ vault_prefix = attr.ib() def secret(self, default=RAISE, converter=None, name=None, help=None): """ Almost identical to `environ.var` except that it takes *envconsul* naming into account. """ return attr.ib( default=default, metadata={ CNF_KEY: _ConfigEntry(name, default, None, self._get, help) }, converter=converter, ) def _get(self, environ, metadata, prefix, name): ce = metadata[CNF_KEY] if ce.name is not None: var = ce.name else: if callable(self.vault_prefix): vp = self.vault_prefix(environ) else: vp = self.vault_prefix var = "_".join(((vp,) + prefix + (name,))).upper() log.debug("looking for env var '%s'." % (var,)) val = environ.get( var, ( attr.NOTHING if isinstance(ce.default, attr.Factory) else ce.default ), ) if val is RAISE: raise MissingSecretError(var) return _SecretStr(val) class _SecretStr(str): """ String that censors its __repr__ if called from an attrs repr. """ def __repr__(self): # The frame numbers varies across attrs versions. Use this convoluted # form to make the call lazy. if ( sys._getframe(1).f_code.co_name == "__repr__" or sys._getframe(2).f_code.co_name == "__repr__" ): return "<SECRET>" else: return str.__repr__(self) CNF_INI_SECRET_KEY = CNF_KEY + "_ini_secret" @attr.s class _INIConfig(object): section = attr.ib() def _load_ini(path): """ Load an INI file from *path*. """ cfg = RawConfigParser() with codecs.open(path, mode="r", encoding="utf-8") as f: cfg.read_file(f) return cfg