# # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # from typing import Any, List, Mapping import dpath.util def get_secret_paths(spec: Mapping[str, Any]) -> List[List[str]]: paths = [] def traverse_schema(schema_item: Any, path: List[str]): """ schema_item can be any property or value in the originally input jsonschema, depending on how far down the recursion stack we go path is the path to that schema item in the original input for example if we have the input {'password': {'type': 'string', 'airbyte_secret': True}} then the arguments will evolve as follows: schema_item=, path=[] schema_item={'type': 'string', 'airbyte_secret': True}, path=['password'] schema_item='string', path=['password', 'type'] schema_item=True, path=['password', 'airbyte_secret'] """ if isinstance(schema_item, dict): for k, v in schema_item.items(): traverse_schema(v, [*path, k]) elif isinstance(schema_item, list): for i in schema_item: traverse_schema(i, path) else: if path[-1] == "airbyte_secret" and schema_item is True: filtered_path = [p for p in path[:-1] if p not in ["properties", "oneOf"]] paths.append(filtered_path) traverse_schema(spec, []) return paths def get_secrets(connection_specification: Mapping[str, Any], config: Mapping[str, Any]) -> List[Any]: """ Get a list of secret values from the source config based on the source specification :type connection_specification: the connection_specification field of an AirbyteSpecification i.e the JSONSchema definition """ secret_paths = get_secret_paths(connection_specification.get("properties", {})) result = [] for path in secret_paths: try: result.append(dpath.util.get(config, path)) except KeyError: # Since we try to get paths to all known secrets in the spec, in the case of oneOfs, some secret fields may not be present # In that case, a KeyError is thrown. This is expected behavior. pass return result __SECRETS_FROM_CONFIG: List[str] = [] def update_secrets(secrets: List[str]): """Update the list of secrets to be replaced""" global __SECRETS_FROM_CONFIG __SECRETS_FROM_CONFIG = secrets def filter_secrets(string: str) -> str: """Filter secrets from a string by replacing them with ****""" # TODO this should perform a maximal match for each secret. if "x" and "xk" are both secret values, and this method is called twice on # the input "xk", then depending on call order it might only obfuscate "*k". This is a bug. for secret in __SECRETS_FROM_CONFIG: if secret: string = string.replace(str(secret), "****") return string