From 75732c48f286c4345cdb260eb87aff76cd880cd4 Mon Sep 17 00:00:00 2001 From: Joe Cooter Date: Sun, 4 Jul 2021 18:40:50 -0400 Subject: [PATCH 1/4] Modify VaultCredentialProvider to accept any kv pair --- vault12factor/__init__.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/vault12factor/__init__.py b/vault12factor/__init__.py index 920ec18..7ca146e 100644 --- a/vault12factor/__init__.py +++ b/vault12factor/__init__.py @@ -219,18 +219,25 @@ class VaultCredentialProvider: } """ def __init__(self, vaulturl: str, vaultauth: VaultAuthentication, secretpath: str, pin_cacert: str=None, - ssl_verify: bool=False, debug_output: bool=False) -> None: + ssl_verify: bool=False, debug_output: bool=False, attributes: list=["username", "password"]) -> None: self.vaulturl = vaulturl self._vaultauth = vaultauth self.secretpath = secretpath self.pin_cacert = pin_cacert self.ssl_verify = ssl_verify self.debug_output = debug_output - self._cache = None # type: Dict[str, str] + self.attributes = attributes + self._cache = {} # type: Dict[str, str] self._leasetime = None # type: datetime.datetime self._updatetime = None # type: datetime.datetime self._lease_id = None # type: str + def _attach_secret_attribute(instance, attribute): + class_name = instance.__class__.__name__ + 'Child' + child_class = type(class_name, (instance.__class__,), {attribute: property(lambda self: self._get_or_update(attribute))}) + + instance.__class__ = child_class + def _now(self) -> datetime.datetime: return datetime.datetime.now(pytz.timezone("UTC")) @@ -248,41 +255,35 @@ def _refresh(self) -> None: (self.secretpath, str(e)) ) from e - if "data" not in result or "username" not in result["data"] or "password" not in result["data"]: + if "data" not in result: raise VaultCredentialProviderException( - "Read dict from Vault path %s did not match expected structure (data->{username, password}): %s" % + "Read dict from Vault path %s did not match expected structure: %s" % (self.secretpath, str(result)) ) + for old_key in list(set(self._cache.keys()) - set(data.keys())): + delattr(self.__class__, old_key) self._cache = result["data"] + for attribute in result["data"].keys(): + if not getattr(self, attribute, None): + self._attach_secret_attribute(attribute) self._lease_id = result["lease_id"] self._leasetime = self._now() self._updatetime = self._leasetime + datetime.timedelta(seconds=int(result["lease_duration"])) - _log.debug("Loaded new Vault DB credentials from %s:\nlease_id=%s\nleasetime=%s\nduration=%s\n" - "username=%s\npassword=%s", + _log.debug("Loaded new Vault credentials from %s:\nlease_id=%s\nleasetime=%s\nduration=%s", self.secretpath, - self._lease_id, str(self._leasetime), result["lease_duration"], self._cache["username"], - self._cache["password"] if self.debug_output else "Password withheld, debug output is disabled") + self._lease_id, str(self._leasetime), result["lease_duration"]) def _get_or_update(self, key: str) -> str: if self._cache is None or (self._updatetime - self._now()).total_seconds() < 10: # if we have less than 10 seconds in a lease ot no lease at all, we get new credentials - _log.info("Vault DB credential lease has expired, refreshing for %s" % key) + _log.info("Vault credential lease has expired, refreshing for %s" % key) self._refresh() _log.info("refresh done (%s, %s)" % (self._lease_id, str(self._updatetime))) return self._cache[key] - @property - def username(self) -> str: - return self._get_or_update("username") - - @property - def password(self) -> str: - return self._get_or_update("password") - - class DjangoAutoRefreshDBCredentialsDict(dict): def __init__(self, provider: VaultCredentialProvider, *args: Any, **kwargs: Any) -> None: self._provider = provider From 3dbae5a9f3297d61072d1db2dde25096d6678cc8 Mon Sep 17 00:00:00 2001 From: Joe Cooter Date: Sun, 4 Jul 2021 21:23:15 -0400 Subject: [PATCH 2/4] Grab credentials on init --- vault12factor/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vault12factor/__init__.py b/vault12factor/__init__.py index 7ca146e..dcf3e93 100644 --- a/vault12factor/__init__.py +++ b/vault12factor/__init__.py @@ -231,6 +231,7 @@ def __init__(self, vaulturl: str, vaultauth: VaultAuthentication, secretpath: st self._leasetime = None # type: datetime.datetime self._updatetime = None # type: datetime.datetime self._lease_id = None # type: str + self._refresh() def _attach_secret_attribute(instance, attribute): class_name = instance.__class__.__name__ + 'Child' From 432eed498eac16c94fc6e83b43c33b2ada8f76a8 Mon Sep 17 00:00:00 2001 From: Joe Cooter Date: Sun, 4 Jul 2021 22:40:30 -0400 Subject: [PATCH 3/4] enable kv engine secrets --- vault12factor/__init__.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/vault12factor/__init__.py b/vault12factor/__init__.py index dcf3e93..c088e8a 100644 --- a/vault12factor/__init__.py +++ b/vault12factor/__init__.py @@ -256,25 +256,49 @@ def _refresh(self) -> None: (self.secretpath, str(e)) ) from e + if not result: + try: + mountpoint, path = self.secretpath.split('/', 1) + result = vcl.secrets.kv.v2.read_secret_version(mount_point=mountpoint,path=path) + except RequestException as e: + raise VaultCredentialProviderException( + "Unable to read credentials from path '%s' with request error: %s" % + (self.secretpath, str(e)) + ) from e + if "data" not in result: raise VaultCredentialProviderException( "Read dict from Vault path %s did not match expected structure: %s" % (self.secretpath, str(result)) ) - - for old_key in list(set(self._cache.keys()) - set(data.keys())): +globals() + # Before updating the cache, check for vault keys that have been removed and remove the property + for old_key in list(set(self._cache.keys()) - set(result["data"].keys())): delattr(self.__class__, old_key) - self._cache = result["data"] - for attribute in result["data"].keys(): + + # Normalize secret data between kv engines and everything else + if "data" in result["data"]: + secret = result["data"]["data"] + else: + secret = result["data"] + + self._cache = secret + + # Add any keys in the to our object as a property + for attribute in secret.keys(): if not getattr(self, attribute, None): self._attach_secret_attribute(attribute) - self._lease_id = result["lease_id"] + + self._lease_id = secret.get("lease_id") self._leasetime = self._now() - self._updatetime = self._leasetime + datetime.timedelta(seconds=int(result["lease_duration"])) + self._updatetime = self._leasetime + datetime.timedelta( + seconds=int(secret.get("lease_duration", os.getenv("VAULT_DEFAULT_CACHE_DURATION", "3600")))) _log.debug("Loaded new Vault credentials from %s:\nlease_id=%s\nleasetime=%s\nduration=%s", self.secretpath, - self._lease_id, str(self._leasetime), result["lease_duration"]) + self._lease_id, + str(self._leasetime), + secret.get("lease_duration", os.getenv("VAULT_DEFAULT_CACHE_DURATION", "3600"))) def _get_or_update(self, key: str) -> str: if self._cache is None or (self._updatetime - self._now()).total_seconds() < 10: From 84e4a3c29de2ab1b8aac0f9bd9fb1d5abc4b2f7e Mon Sep 17 00:00:00 2001 From: Joe Cooter Date: Sun, 4 Jul 2021 22:43:46 -0400 Subject: [PATCH 4/4] cleanup --- vault12factor/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vault12factor/__init__.py b/vault12factor/__init__.py index c088e8a..c7b1c4e 100644 --- a/vault12factor/__init__.py +++ b/vault12factor/__init__.py @@ -219,14 +219,13 @@ class VaultCredentialProvider: } """ def __init__(self, vaulturl: str, vaultauth: VaultAuthentication, secretpath: str, pin_cacert: str=None, - ssl_verify: bool=False, debug_output: bool=False, attributes: list=["username", "password"]) -> None: + ssl_verify: bool=False, debug_output: bool=False) -> None: self.vaulturl = vaulturl self._vaultauth = vaultauth self.secretpath = secretpath self.pin_cacert = pin_cacert self.ssl_verify = ssl_verify self.debug_output = debug_output - self.attributes = attributes self._cache = {} # type: Dict[str, str] self._leasetime = None # type: datetime.datetime self._updatetime = None # type: datetime.datetime @@ -271,7 +270,7 @@ def _refresh(self) -> None: "Read dict from Vault path %s did not match expected structure: %s" % (self.secretpath, str(result)) ) -globals() + # Before updating the cache, check for vault keys that have been removed and remove the property for old_key in list(set(self._cache.keys()) - set(result["data"].keys())): delattr(self.__class__, old_key)