PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/vault/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/vault/kv.py |
import logging from salt.utils.vault.exceptions import ( VaultException, VaultInvocationError, VaultPermissionDeniedError, VaultUnsupportedOperationError, ) log = logging.getLogger(__name__) class VaultKV: """ Interface to Vault secret paths """ def __init__(self, client, metadata_cache): self.client = client self.metadata_cache = metadata_cache def read(self, path, include_metadata=False): """ Read secret data at path. include_metadata For kv-v2, include metadata in the return value: ``{"data": {} ,"metadata": {}}``. """ v2_info = self.is_v2(path) if v2_info["v2"]: path = v2_info["data"] res = self.client.get(path) ret = res["data"] if v2_info["v2"] and not include_metadata: return ret["data"] return ret def write(self, path, data): """ Write secret data to path. """ v2_info = self.is_v2(path) if v2_info["v2"]: path = v2_info["data"] data = {"data": data} return self.client.post(path, payload=data) def patch(self, path, data): """ Patch existing data. Tries to use a PATCH request, otherwise falls back to updating in memory and writing back the whole secret, thus might consume more than one token use. Since this uses JSON Merge Patch format, values set to ``null`` (``None``) will be dropped. For details, see https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-merge-patch-07 """ def apply_json_merge_patch(data, patch): if not patch: return data if not isinstance(data, dict) or not isinstance(patch, dict): raise ValueError("Data and patch must be dictionaries.") for key, value in patch.items(): if value is None: data.pop(key, None) elif isinstance(value, dict): data[key] = apply_json_merge_patch(data.get(key, {}), value) else: data[key] = value return data def patch_in_memory(path, data): current = self.read(path) updated = apply_json_merge_patch(current, data) return self.write(path, updated) v2_info = self.is_v2(path) if not v2_info["v2"]: return patch_in_memory(path, data) path = v2_info["data"] payload = {"data": data} add_headers = {"Content-Type": "application/merge-patch+json"} try: return self.client.patch(path, payload=payload, add_headers=add_headers) except VaultPermissionDeniedError: log.warning("Failed patching secret, is the `patch` capability set?") except VaultUnsupportedOperationError: pass return patch_in_memory(path, data) def delete(self, path, versions=None): """ Delete secret path data. For kv-v1, this is permanent. For kv-v2, this only soft-deletes the data. versions For kv-v2, specifies versions to soft-delete. Needs to be castable to a list of integers. """ method = "DELETE" payload = None versions = self._parse_versions(versions) v2_info = self.is_v2(path) if v2_info["v2"]: if versions is not None: method = "POST" path = v2_info["delete_versions"] payload = {"versions": versions} else: # data and delete operations only differ by HTTP verb path = v2_info["data"] elif versions is not None: raise VaultInvocationError("Versioning support requires kv-v2.") return self.client.request(method, path, payload=payload) def destroy(self, path, versions): """ Permanently remove version data. Requires kv-v2. versions Specifies versions to destroy. Needs to be castable to a list of integers. """ versions = self._parse_versions(versions) v2_info = self.is_v2(path) if not v2_info["v2"]: raise VaultInvocationError("Destroy operation requires kv-v2.") path = v2_info["destroy"] payload = {"versions": versions} return self.client.post(path, payload=payload) def _parse_versions(self, versions): if versions is None: return versions if not isinstance(versions, list): versions = [versions] try: versions = [int(x) for x in versions] except ValueError as err: raise VaultInvocationError( "Versions have to be specified as integers." ) from err return versions def nuke(self, path): """ Delete path metadata and version data, including all version history. Requires kv-v2. """ v2_info = self.is_v2(path) if not v2_info["v2"]: raise VaultInvocationError("Nuke operation requires kv-v2.") path = v2_info["metadata"] return self.client.delete(path) def list(self, path): """ List keys at path. """ v2_info = self.is_v2(path) if v2_info["v2"]: path = v2_info["metadata"] return self.client.list(path)["data"]["keys"] def is_v2(self, path): """ Determines if a given secret path is kv version 1 or 2. """ ret = { "v2": False, "data": path, "metadata": path, "delete": path, "type": None, } path_metadata = self._get_secret_path_metadata(path) if not path_metadata: # metadata lookup failed. Simply return not v2 return ret ret["type"] = path_metadata.get("type", "kv") if ( ret["type"] == "kv" and path_metadata["options"] is not None and path_metadata.get("options", {}).get("version", "1") in ["2"] ): ret["v2"] = True ret["data"] = self._v2_the_path(path, path_metadata.get("path", path)) ret["metadata"] = self._v2_the_path( path, path_metadata.get("path", path), "metadata" ) ret["delete"] = ret["data"] ret["delete_versions"] = self._v2_the_path( path, path_metadata.get("path", path), "delete" ) ret["destroy"] = self._v2_the_path( path, path_metadata.get("path", path), "destroy" ) return ret def _v2_the_path(self, path, pfilter, ptype="data"): """ Given a path, a filter, and a path type, properly inject 'data' or 'metadata' into the path. """ possible_types = ["data", "metadata", "delete", "destroy"] if ptype not in possible_types: raise AssertionError() msg = f"Path {path} already contains {ptype} in the right place - saltstack duct tape?" path = path.rstrip("/").lstrip("/") pfilter = pfilter.rstrip("/").lstrip("/") together = pfilter + "/" + ptype otype = possible_types[0] if possible_types[0] != ptype else possible_types[1] other = pfilter + "/" + otype if path.startswith(other): path = path.replace(other, together, 1) msg = f'Path is a "{otype}" type but "{ptype}" type requested - Flipping: {path}' elif not path.startswith(together): old_path = path path = path.replace(pfilter, together, 1) msg = f"Converting path to v2 {old_path} => {path}" log.debug(msg) return path def _get_secret_path_metadata(self, path): """ Given a path, query vault to determine mount point, type, and version. """ cache_content = self.metadata_cache.get() or {} ret = None if path.startswith(tuple(cache_content.keys())): log.debug("Found cached metadata for %s", path) ret = next(v for k, v in cache_content.items() if path.startswith(k)) else: log.debug("Fetching metadata for %s", path) try: endpoint = f"sys/internal/ui/mounts/{path}" res = self.client.get(endpoint) if "data" in res: log.debug("Got metadata for %s", path) cache_content[path] = ret = res["data"] self.metadata_cache.store(cache_content) else: raise VaultException("Unexpected response to metadata query.") except Exception as err: # pylint: disable=broad-except log.error( "Failed to get secret metadata %s: %s", type(err).__name__, err ) return ret