airbyte.secrets.util

Helper functions for working with secrets.

  1# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
  2"""Helper functions for working with secrets."""
  3
  4from __future__ import annotations
  5
  6import warnings
  7from contextlib import suppress
  8from typing import Any, cast
  9
 10from airbyte import exceptions as exc
 11from airbyte.constants import SECRETS_HYDRATION_PREFIX
 12from airbyte.secrets.base import SecretManager, SecretSourceEnum, SecretString
 13from airbyte.secrets.config import _get_secret_sources
 14
 15
 16def is_secret_available(
 17    secret_name: str,
 18) -> bool:
 19    """Check if a secret is available in any of the configured secret sources.
 20
 21    This function checks all available secret sources for the given secret name.
 22    If the secret is found in any source, it returns `True`; otherwise, it returns `False`.
 23    """
 24    try:
 25        _ = get_secret(secret_name, allow_prompt=False)
 26    except exc.PyAirbyteSecretNotFoundError:
 27        return False
 28    else:
 29        # If no exception was raised, the secret was found.
 30        return True
 31
 32
 33def try_get_secret(
 34    secret_name: str,
 35    /,
 36    default: str | SecretString | None = None,
 37    sources: list[SecretManager | SecretSourceEnum] | None = None,
 38    **kwargs: dict[str, Any],
 39) -> SecretString | None:
 40    """Try to get a secret from the environment, failing gracefully.
 41
 42    This function attempts to retrieve a secret from the configured secret sources.
 43    If the secret is found, it returns the secret value; otherwise, it returns the
 44    default value or None.
 45
 46    This function will not prompt the user for input if the secret is not found.
 47
 48    Raises:
 49        PyAirbyteInputError: If an invalid source name is provided in the `sources` argument.
 50    """
 51    with suppress(exc.PyAirbyteSecretNotFoundError):
 52        return get_secret(
 53            secret_name,
 54            sources=sources,
 55            allow_prompt=False,
 56            default=default,
 57            **kwargs,
 58        )
 59
 60    return None
 61
 62
 63def get_secret(
 64    secret_name: str,
 65    /,
 66    *,
 67    sources: list[SecretManager | SecretSourceEnum] | None = None,
 68    default: str | SecretString | None = None,
 69    allow_prompt: bool = True,
 70    **kwargs: dict[str, Any],
 71) -> SecretString:
 72    """Get a secret from the environment.
 73
 74    The optional `sources` argument of enum type `SecretSourceEnum` or list of `SecretSourceEnum`
 75    options. If left blank, all available sources will be checked. If a list of `SecretSourceEnum`
 76    entries is passed, then the sources will be checked using the provided ordering.
 77
 78    If `allow_prompt` is `True` or if SecretSourceEnum.PROMPT is declared in the `source` arg, then
 79    the user will be prompted to enter the secret if it is not found in any of the other sources.
 80
 81    Raises:
 82        PyAirbyteSecretNotFoundError: If the secret is not found in any of the configured sources,
 83            and if no default value is provided.
 84        PyAirbyteInputError: If an invalid source name is provided in the `sources` argument.
 85    """
 86    if secret_name.startswith(SECRETS_HYDRATION_PREFIX):
 87        # If the secret name starts with the hydration prefix, we assume it's a secret reference.
 88        # We strip the prefix and get the actual secret name.
 89        secret_name = secret_name.removeprefix(SECRETS_HYDRATION_PREFIX).lstrip()
 90
 91    if "source" in kwargs:
 92        warnings.warn(
 93            message="The `source` argument is deprecated. Use the `sources` argument instead.",
 94            category=DeprecationWarning,
 95            stacklevel=2,
 96        )
 97        sources = kwargs.pop("source")  # type: ignore [assignment]
 98
 99    available_sources: dict[str, SecretManager] = {}
100    for available_source in _get_secret_sources():
101        # Add available sources to the dict. Order matters.
102        available_sources[available_source.name] = available_source
103
104    if sources is None:
105        # If ANY is in the list, then we don't need to check any other sources.
106        # This is the default behavior.
107        sources = list(available_sources.values())
108
109    elif not isinstance(sources, list):
110        sources = [sources]  # type: ignore [unreachable]  # This is a 'just in case' catch.
111
112    # Replace any SecretSourceEnum strings with the matching SecretManager object
113    for source in list(sources):
114        if isinstance(source, SecretSourceEnum):
115            if source not in available_sources:
116                raise exc.PyAirbyteInputError(
117                    guidance="Invalid secret source name.",
118                    input_value=source,
119                    context={
120                        "Available Sources": list(available_sources.keys()),
121                    },
122                )
123
124            sources[sources.index(source)] = available_sources[source]
125
126    secret_managers = cast("list[SecretManager]", sources)
127
128    if SecretSourceEnum.PROMPT in secret_managers:
129        prompt_source = secret_managers.pop(
130            # Mis-typed, but okay here since we have equality logic for the enum comparison:
131            secret_managers.index(SecretSourceEnum.PROMPT),  # type: ignore [arg-type]
132        )
133
134        if allow_prompt:
135            # Always check prompt last. Add it to the end of the list.
136            secret_managers.append(prompt_source)
137
138    for secret_mgr in secret_managers:
139        val = secret_mgr.get_secret(secret_name)
140        if val:
141            return SecretString(val)
142
143    if default:
144        return SecretString(default)
145
146    raise exc.PyAirbyteSecretNotFoundError(
147        secret_name=secret_name,
148        sources=[str(s) for s in available_sources],
149    )
def is_secret_available(secret_name: str) -> bool:
17def is_secret_available(
18    secret_name: str,
19) -> bool:
20    """Check if a secret is available in any of the configured secret sources.
21
22    This function checks all available secret sources for the given secret name.
23    If the secret is found in any source, it returns `True`; otherwise, it returns `False`.
24    """
25    try:
26        _ = get_secret(secret_name, allow_prompt=False)
27    except exc.PyAirbyteSecretNotFoundError:
28        return False
29    else:
30        # If no exception was raised, the secret was found.
31        return True

Check if a secret is available in any of the configured secret sources.

This function checks all available secret sources for the given secret name. If the secret is found in any source, it returns True; otherwise, it returns False.

def try_get_secret( secret_name: str, /, default: str | airbyte.secrets.SecretString | None = None, sources: list[airbyte.secrets.SecretManager | airbyte.SecretSourceEnum] | None = None, **kwargs: dict[str, typing.Any]) -> airbyte.secrets.SecretString | None:
34def try_get_secret(
35    secret_name: str,
36    /,
37    default: str | SecretString | None = None,
38    sources: list[SecretManager | SecretSourceEnum] | None = None,
39    **kwargs: dict[str, Any],
40) -> SecretString | None:
41    """Try to get a secret from the environment, failing gracefully.
42
43    This function attempts to retrieve a secret from the configured secret sources.
44    If the secret is found, it returns the secret value; otherwise, it returns the
45    default value or None.
46
47    This function will not prompt the user for input if the secret is not found.
48
49    Raises:
50        PyAirbyteInputError: If an invalid source name is provided in the `sources` argument.
51    """
52    with suppress(exc.PyAirbyteSecretNotFoundError):
53        return get_secret(
54            secret_name,
55            sources=sources,
56            allow_prompt=False,
57            default=default,
58            **kwargs,
59        )
60
61    return None

Try to get a secret from the environment, failing gracefully.

This function attempts to retrieve a secret from the configured secret sources. If the secret is found, it returns the secret value; otherwise, it returns the default value or None.

This function will not prompt the user for input if the secret is not found.

Raises:
  • PyAirbyteInputError: If an invalid source name is provided in the sources argument.
def get_secret( secret_name: str, /, *, sources: list[airbyte.secrets.SecretManager | airbyte.SecretSourceEnum] | None = None, default: str | airbyte.secrets.SecretString | None = None, allow_prompt: bool = True, **kwargs: dict[str, typing.Any]) -> airbyte.secrets.SecretString:
 64def get_secret(
 65    secret_name: str,
 66    /,
 67    *,
 68    sources: list[SecretManager | SecretSourceEnum] | None = None,
 69    default: str | SecretString | None = None,
 70    allow_prompt: bool = True,
 71    **kwargs: dict[str, Any],
 72) -> SecretString:
 73    """Get a secret from the environment.
 74
 75    The optional `sources` argument of enum type `SecretSourceEnum` or list of `SecretSourceEnum`
 76    options. If left blank, all available sources will be checked. If a list of `SecretSourceEnum`
 77    entries is passed, then the sources will be checked using the provided ordering.
 78
 79    If `allow_prompt` is `True` or if SecretSourceEnum.PROMPT is declared in the `source` arg, then
 80    the user will be prompted to enter the secret if it is not found in any of the other sources.
 81
 82    Raises:
 83        PyAirbyteSecretNotFoundError: If the secret is not found in any of the configured sources,
 84            and if no default value is provided.
 85        PyAirbyteInputError: If an invalid source name is provided in the `sources` argument.
 86    """
 87    if secret_name.startswith(SECRETS_HYDRATION_PREFIX):
 88        # If the secret name starts with the hydration prefix, we assume it's a secret reference.
 89        # We strip the prefix and get the actual secret name.
 90        secret_name = secret_name.removeprefix(SECRETS_HYDRATION_PREFIX).lstrip()
 91
 92    if "source" in kwargs:
 93        warnings.warn(
 94            message="The `source` argument is deprecated. Use the `sources` argument instead.",
 95            category=DeprecationWarning,
 96            stacklevel=2,
 97        )
 98        sources = kwargs.pop("source")  # type: ignore [assignment]
 99
100    available_sources: dict[str, SecretManager] = {}
101    for available_source in _get_secret_sources():
102        # Add available sources to the dict. Order matters.
103        available_sources[available_source.name] = available_source
104
105    if sources is None:
106        # If ANY is in the list, then we don't need to check any other sources.
107        # This is the default behavior.
108        sources = list(available_sources.values())
109
110    elif not isinstance(sources, list):
111        sources = [sources]  # type: ignore [unreachable]  # This is a 'just in case' catch.
112
113    # Replace any SecretSourceEnum strings with the matching SecretManager object
114    for source in list(sources):
115        if isinstance(source, SecretSourceEnum):
116            if source not in available_sources:
117                raise exc.PyAirbyteInputError(
118                    guidance="Invalid secret source name.",
119                    input_value=source,
120                    context={
121                        "Available Sources": list(available_sources.keys()),
122                    },
123                )
124
125            sources[sources.index(source)] = available_sources[source]
126
127    secret_managers = cast("list[SecretManager]", sources)
128
129    if SecretSourceEnum.PROMPT in secret_managers:
130        prompt_source = secret_managers.pop(
131            # Mis-typed, but okay here since we have equality logic for the enum comparison:
132            secret_managers.index(SecretSourceEnum.PROMPT),  # type: ignore [arg-type]
133        )
134
135        if allow_prompt:
136            # Always check prompt last. Add it to the end of the list.
137            secret_managers.append(prompt_source)
138
139    for secret_mgr in secret_managers:
140        val = secret_mgr.get_secret(secret_name)
141        if val:
142            return SecretString(val)
143
144    if default:
145        return SecretString(default)
146
147    raise exc.PyAirbyteSecretNotFoundError(
148        secret_name=secret_name,
149        sources=[str(s) for s in available_sources],
150    )

Get a secret from the environment.

The optional sources argument of enum type SecretSourceEnum or list of SecretSourceEnum options. If left blank, all available sources will be checked. If a list of SecretSourceEnum entries is passed, then the sources will be checked using the provided ordering.

If allow_prompt is True or if SecretSourceEnum.PROMPT is declared in the source arg, then the user will be prompted to enter the secret if it is not found in any of the other sources.

Raises:
  • PyAirbyteSecretNotFoundError: If the secret is not found in any of the configured sources, and if no default value is provided.
  • PyAirbyteInputError: If an invalid source name is provided in the sources argument.