Skip to content

Nautobot Secrets Providers Package

nautobot_secrets_providers.providers

Nautobot Secrets Providers.

AWSSecretsManagerSecretsProvider

Bases: SecretsProvider

A secrets provider for AWS Secrets Manager.

Source code in nautobot_secrets_providers/providers/aws.py
class AWSSecretsManagerSecretsProvider(SecretsProvider):
    """A secrets provider for AWS Secrets Manager."""

    slug = "aws-secrets-manager"
    name = "AWS Secrets Manager"
    is_available = boto3 is not None

    # TBD: Remove after pylint-nautobot bump
    # pylint: disable-next=nb-incorrect-base-class
    class ParametersForm(BootstrapMixin, forms.Form):
        """Required parameters for AWS Secrets Manager."""

        name = forms.CharField(
            required=True,
            help_text="The name of the AWS Secrets Manager secret",
        )
        region = forms.CharField(
            required=True,
            help_text="The region name of the AWS Secrets Manager secret",
        )
        key = forms.CharField(
            required=True,
            help_text="The key of the AWS Secrets Manager secret",
        )

    @classmethod
    def get_value_for_secret(cls, secret, obj=None, **kwargs):
        """Return the secret value by name and region."""
        # Extract the parameters from the Secret.
        parameters = secret.rendered_parameters(obj=obj)

        secret_name = parameters.get("name")
        secret_key = parameters.get("key")
        region_name = parameters.get("region")

        # Create a Secrets Manager client.
        session = boto3.session.Session()
        client = session.client(service_name="secretsmanager", region_name=region_name)

        # This is based on sample code to only handle the specific exceptions for the 'GetSecretValue' API.
        # See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
        # We rethrow the exception by default.
        try:
            get_secret_value_response = client.get_secret_value(SecretId=secret_name)
        except ClientError as err:
            if err.response["Error"]["Code"] == "DecryptionFailureException":  # pylint: disable=no-else-raise
                # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
                # Deal with the exception here, and/or rethrow at your discretion.
                raise exceptions.SecretProviderError(secret, cls, str(err))
            elif err.response["Error"]["Code"] == "InternalServiceErrorException":
                # An error occurred on the server side.
                # Deal with the exception here, and/or rethrow at your discretion.
                raise exceptions.SecretProviderError(secret, cls, str(err))
            elif err.response["Error"]["Code"] == "InvalidParameterException":
                # You provided an invalid value for a parameter.
                # Deal with the exception here, and/or rethrow at your discretion.
                raise exceptions.SecretParametersError(secret, cls, str(err))
            elif err.response["Error"]["Code"] == "InvalidRequestException":
                # You provided a parameter value that is not valid for the current state of the resource.
                # Deal with the exception here, and/or rethrow at your discretion.
                raise exceptions.SecretProviderError(secret, cls, str(err))
            elif err.response["Error"]["Code"] == "ResourceNotFoundException":
                # We can't find the resource that you asked for.
                # Deal with the exception here, and/or rethrow at your discretion.
                raise exceptions.SecretValueNotFoundError(secret, cls, str(err))
            else:
                # We got an error that isn't defined above
                raise exceptions.SecretProviderError(secret, cls, str(err))
        else:
            # Decrypts secret using the associated KMS CMK.
            # Depending on whether the secret is a string or binary, one of these fields will be populated.
            if "SecretString" in get_secret_value_response:
                secret_value = get_secret_value_response["SecretString"]
            else:
                # TODO(jathan): Do we care about this? Let's figure out what to do about a binary value?
                secret_value = base64.b64decode(get_secret_value_response["SecretBinary"])  # noqa

        # If we get this far it should be valid JSON.
        data = json.loads(secret_value)

        # Retrieve the value using the key or complain loudly.
        try:
            return data[secret_key]
        except KeyError as err:
            msg = f"The secret value could not be retrieved using key {err}"
            raise exceptions.SecretValueNotFoundError(secret, cls, msg) from err

ParametersForm

Bases: BootstrapMixin, Form

Required parameters for AWS Secrets Manager.

Source code in nautobot_secrets_providers/providers/aws.py
class ParametersForm(BootstrapMixin, forms.Form):
    """Required parameters for AWS Secrets Manager."""

    name = forms.CharField(
        required=True,
        help_text="The name of the AWS Secrets Manager secret",
    )
    region = forms.CharField(
        required=True,
        help_text="The region name of the AWS Secrets Manager secret",
    )
    key = forms.CharField(
        required=True,
        help_text="The key of the AWS Secrets Manager secret",
    )

get_value_for_secret(secret, obj=None, **kwargs) classmethod

Return the secret value by name and region.

Source code in nautobot_secrets_providers/providers/aws.py
@classmethod
def get_value_for_secret(cls, secret, obj=None, **kwargs):
    """Return the secret value by name and region."""
    # Extract the parameters from the Secret.
    parameters = secret.rendered_parameters(obj=obj)

    secret_name = parameters.get("name")
    secret_key = parameters.get("key")
    region_name = parameters.get("region")

    # Create a Secrets Manager client.
    session = boto3.session.Session()
    client = session.client(service_name="secretsmanager", region_name=region_name)

    # This is based on sample code to only handle the specific exceptions for the 'GetSecretValue' API.
    # See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
    # We rethrow the exception by default.
    try:
        get_secret_value_response = client.get_secret_value(SecretId=secret_name)
    except ClientError as err:
        if err.response["Error"]["Code"] == "DecryptionFailureException":  # pylint: disable=no-else-raise
            # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise exceptions.SecretProviderError(secret, cls, str(err))
        elif err.response["Error"]["Code"] == "InternalServiceErrorException":
            # An error occurred on the server side.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise exceptions.SecretProviderError(secret, cls, str(err))
        elif err.response["Error"]["Code"] == "InvalidParameterException":
            # You provided an invalid value for a parameter.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise exceptions.SecretParametersError(secret, cls, str(err))
        elif err.response["Error"]["Code"] == "InvalidRequestException":
            # You provided a parameter value that is not valid for the current state of the resource.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise exceptions.SecretProviderError(secret, cls, str(err))
        elif err.response["Error"]["Code"] == "ResourceNotFoundException":
            # We can't find the resource that you asked for.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise exceptions.SecretValueNotFoundError(secret, cls, str(err))
        else:
            # We got an error that isn't defined above
            raise exceptions.SecretProviderError(secret, cls, str(err))
    else:
        # Decrypts secret using the associated KMS CMK.
        # Depending on whether the secret is a string or binary, one of these fields will be populated.
        if "SecretString" in get_secret_value_response:
            secret_value = get_secret_value_response["SecretString"]
        else:
            # TODO(jathan): Do we care about this? Let's figure out what to do about a binary value?
            secret_value = base64.b64decode(get_secret_value_response["SecretBinary"])  # noqa

    # If we get this far it should be valid JSON.
    data = json.loads(secret_value)

    # Retrieve the value using the key or complain loudly.
    try:
        return data[secret_key]
    except KeyError as err:
        msg = f"The secret value could not be retrieved using key {err}"
        raise exceptions.SecretValueNotFoundError(secret, cls, msg) from err

AWSSystemsManagerParameterStore

Bases: SecretsProvider

A secrets provider for AWS Systems Manager Parameter Store.

Documentation: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html

Source code in nautobot_secrets_providers/providers/aws.py
class AWSSystemsManagerParameterStore(SecretsProvider):
    """
    A secrets provider for AWS Systems Manager Parameter Store.

    Documentation: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html
    """

    slug = "aws-sm-parameter-store"
    name = "AWS Systems Manager Parameter Store"
    is_available = boto3 is not None

    # TBD: Remove after pylint-nautobot bump
    # pylint: disable-next=nb-incorrect-base-class
    class ParametersForm(BootstrapMixin, forms.Form):
        """Required parameters for AWS Parameter Store."""

        name = forms.CharField(
            required=True,
            help_text="The name of the AWS Parameter Store secret",
        )
        region = forms.CharField(
            required=True,
            help_text="The region name of the AWS Parameter Store secret",
        )
        key = forms.CharField(
            required=True,
            help_text="The key name to retrieve from AWS Parameter Store",
        )

    @classmethod
    def get_value_for_secret(cls, secret, obj=None, **kwargs):
        """Return the parameter value by name and region."""
        # Extract the parameters from the Nautobot secret.
        parameters = secret.rendered_parameters(obj=obj)

        # Create a SSM client.
        session = boto3.session.Session()
        client = session.client(service_name="ssm", region_name=parameters.get("region"))
        try:
            get_secret_value_response = client.get_parameter(Name=parameters.get("name"), WithDecryption=True)
        except ClientError as err:
            if err.response["Error"]["Code"] == "ParameterNotFound":
                raise exceptions.SecretParametersError(secret, cls, str(err))

            if err.response["Error"]["Code"] == "ParameterVersionNotFound":
                raise exceptions.SecretValueNotFoundError(secret, cls, str(err))

            raise exceptions.SecretProviderError(secret, cls, str(err))

        try:
            # Fetch the Value field from the parameter which must be a json field.
            data = json.loads(get_secret_value_response["Parameter"]["Value"])
        except ValueError as err:
            msg = "InvalidJson"
            raise exceptions.SecretValueNotFoundError(secret, cls, msg) from err

        try:
            # Return the value of the secret key configured in the nautobot secret.
            return data[parameters.get("key")]
        except KeyError as err:
            msg = f"InvalidKeyName {err}"
            raise exceptions.SecretParametersError(secret, cls, msg) from err

ParametersForm

Bases: BootstrapMixin, Form

Required parameters for AWS Parameter Store.

Source code in nautobot_secrets_providers/providers/aws.py
class ParametersForm(BootstrapMixin, forms.Form):
    """Required parameters for AWS Parameter Store."""

    name = forms.CharField(
        required=True,
        help_text="The name of the AWS Parameter Store secret",
    )
    region = forms.CharField(
        required=True,
        help_text="The region name of the AWS Parameter Store secret",
    )
    key = forms.CharField(
        required=True,
        help_text="The key name to retrieve from AWS Parameter Store",
    )

get_value_for_secret(secret, obj=None, **kwargs) classmethod

Return the parameter value by name and region.

Source code in nautobot_secrets_providers/providers/aws.py
@classmethod
def get_value_for_secret(cls, secret, obj=None, **kwargs):
    """Return the parameter value by name and region."""
    # Extract the parameters from the Nautobot secret.
    parameters = secret.rendered_parameters(obj=obj)

    # Create a SSM client.
    session = boto3.session.Session()
    client = session.client(service_name="ssm", region_name=parameters.get("region"))
    try:
        get_secret_value_response = client.get_parameter(Name=parameters.get("name"), WithDecryption=True)
    except ClientError as err:
        if err.response["Error"]["Code"] == "ParameterNotFound":
            raise exceptions.SecretParametersError(secret, cls, str(err))

        if err.response["Error"]["Code"] == "ParameterVersionNotFound":
            raise exceptions.SecretValueNotFoundError(secret, cls, str(err))

        raise exceptions.SecretProviderError(secret, cls, str(err))

    try:
        # Fetch the Value field from the parameter which must be a json field.
        data = json.loads(get_secret_value_response["Parameter"]["Value"])
    except ValueError as err:
        msg = "InvalidJson"
        raise exceptions.SecretValueNotFoundError(secret, cls, msg) from err

    try:
        # Return the value of the secret key configured in the nautobot secret.
        return data[parameters.get("key")]
    except KeyError as err:
        msg = f"InvalidKeyName {err}"
        raise exceptions.SecretParametersError(secret, cls, msg) from err

AzureKeyVaultSecretsProvider

Bases: SecretsProvider

A secrets provider for Azure Key Vault.

Source code in nautobot_secrets_providers/providers/azure.py
class AzureKeyVaultSecretsProvider(SecretsProvider):
    """A secrets provider for Azure Key Vault."""

    slug = "azure-key-vault"
    name = "Azure Key Vault"
    is_available = azure_available

    # pylint: disable-next=nb-incorrect-base-class
    class ParametersForm(BootstrapMixin, forms.Form):
        """Required parameters for Azure Key Vault."""

        vault_url = forms.CharField(
            required=True,
            help_text="The URL of the Azure Key Vault",
        )
        secret_name = forms.CharField(
            required=True,
            help_text="The name of the secret in the Azure Key Vault",
        )

    @classmethod
    def get_value_for_secret(cls, secret, obj=None, **kwargs):
        """Return the secret value by name from Azure Key Vault."""
        # Extract the parameters from the Secret.
        parameters = secret.rendered_parameters(obj=obj)
        vault_url = parameters.get("vault_url")
        secret_name = parameters.get("secret_name")

        # Authenticate with Azure Key Vault using default credentials.
        # This assumes that environment variables for Azure authentication are set.
        credential = DefaultAzureCredential()
        client = SecretClient(vault_url=vault_url, credential=credential)

        try:
            # Retrieve the secret from Azure Key Vault.
            response = client.get_secret(secret_name)
        except Exception as err:
            # Handle exceptions from the Azure SDK.
            raise exceptions.SecretProviderError(secret, cls, str(err))

        # The value is in the 'value' attribute of the response.
        secret_value = response.value

        # Return the secret value.
        return secret_value

ParametersForm

Bases: BootstrapMixin, Form

Required parameters for Azure Key Vault.

Source code in nautobot_secrets_providers/providers/azure.py
class ParametersForm(BootstrapMixin, forms.Form):
    """Required parameters for Azure Key Vault."""

    vault_url = forms.CharField(
        required=True,
        help_text="The URL of the Azure Key Vault",
    )
    secret_name = forms.CharField(
        required=True,
        help_text="The name of the secret in the Azure Key Vault",
    )

get_value_for_secret(secret, obj=None, **kwargs) classmethod

Return the secret value by name from Azure Key Vault.

Source code in nautobot_secrets_providers/providers/azure.py
@classmethod
def get_value_for_secret(cls, secret, obj=None, **kwargs):
    """Return the secret value by name from Azure Key Vault."""
    # Extract the parameters from the Secret.
    parameters = secret.rendered_parameters(obj=obj)
    vault_url = parameters.get("vault_url")
    secret_name = parameters.get("secret_name")

    # Authenticate with Azure Key Vault using default credentials.
    # This assumes that environment variables for Azure authentication are set.
    credential = DefaultAzureCredential()
    client = SecretClient(vault_url=vault_url, credential=credential)

    try:
        # Retrieve the secret from Azure Key Vault.
        response = client.get_secret(secret_name)
    except Exception as err:
        # Handle exceptions from the Azure SDK.
        raise exceptions.SecretProviderError(secret, cls, str(err))

    # The value is in the 'value' attribute of the response.
    secret_value = response.value

    # Return the secret value.
    return secret_value

DelineaSecretServerSecretsProviderId

Bases: DelineaSecretServerSecretsProviderBase

A secrets provider for Delinea Secret Server.

Source code in nautobot_secrets_providers/providers/delinea.py
class DelineaSecretServerSecretsProviderId(DelineaSecretServerSecretsProviderBase):
    """A secrets provider for Delinea Secret Server."""

    slug = "delinea-tss-id"  # type: ignore
    name = "Delinea Secret Server by ID"  # type: ignore

    # TBD: Remove after pylint-nautobot bump
    # pylint: disable-next=nb-incorrect-base-class
    class ParametersForm(BootstrapMixin, forms.Form):
        """Required parameters for Delinea Secret Server."""

        secret_id = forms.IntegerField(
            label="Secret ID",
            required=True,
            min_value=1,
            help_text="The secret-id used to select the entry in Delinea Secret Server.",
        )
        secret_selected_value = forms.ChoiceField(
            label="Return value",
            required=True,
            choices=DelineaSecretChoices,
            help_text="Select which value to return.",
        )

ParametersForm

Bases: BootstrapMixin, Form

Required parameters for Delinea Secret Server.

Source code in nautobot_secrets_providers/providers/delinea.py
class ParametersForm(BootstrapMixin, forms.Form):
    """Required parameters for Delinea Secret Server."""

    secret_id = forms.IntegerField(
        label="Secret ID",
        required=True,
        min_value=1,
        help_text="The secret-id used to select the entry in Delinea Secret Server.",
    )
    secret_selected_value = forms.ChoiceField(
        label="Return value",
        required=True,
        choices=DelineaSecretChoices,
        help_text="Select which value to return.",
    )

DelineaSecretServerSecretsProviderPath

Bases: DelineaSecretServerSecretsProviderBase

A secrets provider for Delinea Secret Server.

Source code in nautobot_secrets_providers/providers/delinea.py
class DelineaSecretServerSecretsProviderPath(DelineaSecretServerSecretsProviderBase):
    """A secrets provider for Delinea Secret Server."""

    slug = "delinea-tss-path"  # type: ignore
    name = "Delinea Secret Server by Path"  # type: ignore

    # TBD: Remove after pylint-nautobot bump
    # pylint: disable-next=nb-incorrect-base-class
    class ParametersForm(BootstrapMixin, forms.Form):
        """Required parameters for Delinea Secret Server."""

        secret_path = forms.CharField(
            required=True,
            max_length=300,
            min_length=3,
            help_text=r"Enter the secret's path (e.g. \FolderPath\Secret Name).",
        )
        secret_selected_value = forms.ChoiceField(
            label="Return value",
            required=True,
            choices=DelineaSecretChoices,
            help_text="Select which value to return.",
        )

ParametersForm

Bases: BootstrapMixin, Form

Required parameters for Delinea Secret Server.

Source code in nautobot_secrets_providers/providers/delinea.py
class ParametersForm(BootstrapMixin, forms.Form):
    """Required parameters for Delinea Secret Server."""

    secret_path = forms.CharField(
        required=True,
        max_length=300,
        min_length=3,
        help_text=r"Enter the secret's path (e.g. \FolderPath\Secret Name).",
    )
    secret_selected_value = forms.ChoiceField(
        label="Return value",
        required=True,
        choices=DelineaSecretChoices,
        help_text="Select which value to return.",
    )

HashiCorpVaultSecretsProvider

Bases: SecretsProvider

A secrets provider for HashiCorp Vault.

Source code in nautobot_secrets_providers/providers/hashicorp.py
class HashiCorpVaultSecretsProvider(SecretsProvider):
    """A secrets provider for HashiCorp Vault."""

    slug = "hashicorp-vault"
    name = "HashiCorp Vault"
    is_available = hvac is not None

    # TBD: Remove after pylint-nautobot bump
    # pylint: disable-next=nb-incorrect-base-class
    class ParametersForm(BootstrapMixin, forms.Form):
        """Required parameters for HashiCorp Vault."""

        path = forms.CharField(
            required=True,
            help_text="The path to the HashiCorp Vault secret",
        )
        key = forms.CharField(
            required=True,
            help_text="The key of the HashiCorp Vault secret",
        )
        vault = forms.ChoiceField(
            required=False,  # This should be required, but would be a breaking change
            choices=vault_choices,
            help_text="HashiCorp Vault to retrieve the secret from.",
        )
        mount_point = forms.CharField(
            required=False,
            help_text="Override Vault Setting: The path where the secret engine was mounted on.",
            label="Mount Point (override)",
        )
        kv_version = forms.ChoiceField(
            required=False,
            choices=add_blank_choice(HashicorpKVVersionChoices),
            help_text="Override Vault Setting: The version of the kv engine (either v1 or v2).",
            label="KV Version (override)",
        )

    @staticmethod
    def retrieve_vault_settings(name=None):
        """Retrieve the configuration from settings that matches the provided vault name.

        Args:
            name (str, optional): Vault name to retrieve from settings. Defaults to None.

        Returns:
            vault_settings (dict): Hashicorp Vault Settings
        """
        vault_settings = settings.PLUGINS_CONFIG["nautobot_secrets_providers"].get("hashicorp_vault", {})
        if name and "vaults" in vault_settings:
            return vault_settings["vaults"][name]
        return vault_settings

    @classmethod
    def validate_vault_settings(cls, secret=None, vault_name=None):
        """Validate the vault settings."""
        try:
            vault_settings = cls.retrieve_vault_settings(vault_name)
        except KeyError as err:
            raise exceptions.SecretProviderError(
                secret, cls, f"HashiCorp Vault {vault_name} is not configured!"
            ) from err
        if not vault_settings:
            raise exceptions.SecretProviderError(secret, cls, f"HashiCorp Vault {vault_name} is not configured!")

        auth_method = vault_settings.get("auth_method", "token")
        kv_version = vault_settings.get("kv_version", HashicorpKVVersionChoices.KV_VERSION_2)

        if "url" not in vault_settings:
            raise exceptions.SecretProviderError(secret, cls, "HashiCorp Vault configuration is missing a url")

        if auth_method not in AUTH_METHOD_CHOICES:
            raise exceptions.SecretProviderError(secret, cls, f"HashiCorp Vault Auth Method {auth_method} is invalid!")

        if kv_version not in HashicorpKVVersionChoices.as_dict():
            raise exceptions.SecretProviderError(secret, cls, f"HashiCorp Vault KV version {kv_version} is invalid!")

        if auth_method == "aws" and not boto3:
            raise exceptions.SecretProviderError(
                secret, cls, "HashiCorp Vault AWS Authentication Method requires the boto3 library!"
            )
        if auth_method == "token" and "token" not in vault_settings:
            raise exceptions.SecretProviderError(
                secret, cls, "HashiCorp Vault configuration is missing a token for token authentication!"
            )
        if auth_method == "kubernetes" and "role_name" not in vault_settings:
            raise exceptions.SecretProviderError(
                secret, cls, "HashiCorp Vault configuration is missing a role name for kubernetes authentication!"
            )
        if auth_method == "approle" and ("role_id" not in vault_settings or "secret_id" not in vault_settings):
            raise exceptions.SecretProviderError(
                secret, cls, "HashiCorp Vault configuration is missing a role_id and/or secret_id!"
            )

        return vault_settings

    @classmethod
    def get_client(cls, secret=None, vault_name=None):  # pylint: disable-msg=too-many-locals
        """Authenticate and return a hashicorp client."""
        vault_settings = cls.validate_vault_settings(secret, vault_name)
        auth_method = vault_settings.get("auth_method", "token")

        k8s_token_path = vault_settings.get("k8s_token_path", K8S_TOKEN_DEFAULT_PATH)
        login_kwargs = vault_settings.get("login_kwargs", {})

        # According to the docs (https://hvac.readthedocs.io/en/stable/source/hvac_v1.html?highlight=verify#hvac.v1.Client.__init__)
        # the client verify parameter is either a boolean or a path to a ca certificate file to verify.  This is non-intuitive
        # so we use a parameter to specify the path to the ca_cert, if not provided we use the default of None
        ca_cert = vault_settings.get("ca_cert", None)

        namespace = vault_settings.get("namespace", None)

        # Get the client and attempt to retrieve the secret.
        try:
            if auth_method == "token":
                client = hvac.Client(
                    url=vault_settings["url"],
                    token=vault_settings["token"],
                    verify=ca_cert,
                    namespace=namespace,
                )
            else:
                client = hvac.Client(url=vault_settings["url"], verify=ca_cert, namespace=namespace)
                if auth_method == "approle":
                    client.auth.approle.login(
                        role_id=vault_settings["role_id"],
                        secret_id=vault_settings["secret_id"],
                        **login_kwargs,
                    )
                elif auth_method == "kubernetes":
                    with open(k8s_token_path, "r", encoding="utf-8") as token_file:
                        jwt = token_file.read()
                    client.auth.kubernetes.login(role=vault_settings["role_name"], jwt=jwt, **login_kwargs)
                elif auth_method == "aws":
                    session = boto3.Session()
                    aws_creds = session.get_credentials()
                    aws_region = session.region_name or "us-east-1"
                    client.auth.aws.iam_login(
                        access_key=aws_creds.access_key,
                        secret_key=aws_creds.secret_key,
                        session_token=aws_creds.token,
                        region=aws_region,
                        role=vault_settings.get("role_name", None),
                        **login_kwargs,
                    )
        except hvac.exceptions.InvalidRequest as err:
            raise exceptions.SecretProviderError(
                secret, cls, f"HashiCorp Vault Login failed (auth_method: {auth_method}). Error: {err}"
            ) from err
        except hvac.exceptions.Forbidden as err:
            raise exceptions.SecretProviderError(
                secret, cls, f"HashiCorp Vault Access Denied (auth_method: {auth_method}). Error: {err}"
            ) from err

        return client

    @classmethod
    def get_value_for_secret(cls, secret, obj=None, **kwargs):
        """Return the value stored under the secret’s key in the secret’s path."""
        # Try to get parameters and error out early.
        parameters = secret.rendered_parameters(obj=obj)
        try:
            vault_name = parameters.get("vault", "default")
            vault_settings = cls.retrieve_vault_settings(vault_name)
        except KeyError:
            vault_settings = {}
        # Get the mount_point and kv_version from the Vault configuration. These default to the
        # default Vault that HashiCorp provides.
        secret_mount_point = vault_settings.get("default_mount_point", "secret")
        secret_kv_version = vault_settings.get("kv_version", HashicorpKVVersionChoices.KV_VERSION_2)

        try:
            secret_path = parameters["path"]
            secret_key = parameters["key"]
            # If the user does choose to override the Vault settings at their own risk, we will use
            # the settings they provide. These are here to support multiple vaults (vault engines) when
            # that was not allowed by the settings. Ideally these should be deprecated and removed in
            # the future.
            secret_mount_point = parameters.get("mount_point", secret_mount_point) or secret_mount_point
            secret_kv_version = parameters.get("kv_version", secret_kv_version) or secret_kv_version
        except KeyError as err:
            msg = f"The secret parameter could not be retrieved for field {err}"
            raise exceptions.SecretParametersError(secret, cls, msg) from err

        client = cls.get_client(secret, vault_name)

        try:
            if secret_kv_version == HashicorpKVVersionChoices.KV_VERSION_1:
                response = client.secrets.kv.v1.read_secret(path=secret_path, mount_point=secret_mount_point)
            else:
                response = client.secrets.kv.v2.read_secret(path=secret_path, mount_point=secret_mount_point)
        except hvac.exceptions.InvalidPath as err:
            raise exceptions.SecretValueNotFoundError(secret, cls, str(err)) from err

        # Retrieve the value using the key or complain loudly.
        try:
            if secret_kv_version == HashicorpKVVersionChoices.KV_VERSION_1:
                return response["data"][secret_key]
            return response["data"]["data"][secret_key]
        except KeyError as err:
            msg = f"The secret value could not be retrieved using key {err}"
            raise exceptions.SecretValueNotFoundError(secret, cls, msg) from err

ParametersForm

Bases: BootstrapMixin, Form

Required parameters for HashiCorp Vault.

Source code in nautobot_secrets_providers/providers/hashicorp.py
class ParametersForm(BootstrapMixin, forms.Form):
    """Required parameters for HashiCorp Vault."""

    path = forms.CharField(
        required=True,
        help_text="The path to the HashiCorp Vault secret",
    )
    key = forms.CharField(
        required=True,
        help_text="The key of the HashiCorp Vault secret",
    )
    vault = forms.ChoiceField(
        required=False,  # This should be required, but would be a breaking change
        choices=vault_choices,
        help_text="HashiCorp Vault to retrieve the secret from.",
    )
    mount_point = forms.CharField(
        required=False,
        help_text="Override Vault Setting: The path where the secret engine was mounted on.",
        label="Mount Point (override)",
    )
    kv_version = forms.ChoiceField(
        required=False,
        choices=add_blank_choice(HashicorpKVVersionChoices),
        help_text="Override Vault Setting: The version of the kv engine (either v1 or v2).",
        label="KV Version (override)",
    )

get_client(secret=None, vault_name=None) classmethod

Authenticate and return a hashicorp client.

Source code in nautobot_secrets_providers/providers/hashicorp.py
@classmethod
def get_client(cls, secret=None, vault_name=None):  # pylint: disable-msg=too-many-locals
    """Authenticate and return a hashicorp client."""
    vault_settings = cls.validate_vault_settings(secret, vault_name)
    auth_method = vault_settings.get("auth_method", "token")

    k8s_token_path = vault_settings.get("k8s_token_path", K8S_TOKEN_DEFAULT_PATH)
    login_kwargs = vault_settings.get("login_kwargs", {})

    # According to the docs (https://hvac.readthedocs.io/en/stable/source/hvac_v1.html?highlight=verify#hvac.v1.Client.__init__)
    # the client verify parameter is either a boolean or a path to a ca certificate file to verify.  This is non-intuitive
    # so we use a parameter to specify the path to the ca_cert, if not provided we use the default of None
    ca_cert = vault_settings.get("ca_cert", None)

    namespace = vault_settings.get("namespace", None)

    # Get the client and attempt to retrieve the secret.
    try:
        if auth_method == "token":
            client = hvac.Client(
                url=vault_settings["url"],
                token=vault_settings["token"],
                verify=ca_cert,
                namespace=namespace,
            )
        else:
            client = hvac.Client(url=vault_settings["url"], verify=ca_cert, namespace=namespace)
            if auth_method == "approle":
                client.auth.approle.login(
                    role_id=vault_settings["role_id"],
                    secret_id=vault_settings["secret_id"],
                    **login_kwargs,
                )
            elif auth_method == "kubernetes":
                with open(k8s_token_path, "r", encoding="utf-8") as token_file:
                    jwt = token_file.read()
                client.auth.kubernetes.login(role=vault_settings["role_name"], jwt=jwt, **login_kwargs)
            elif auth_method == "aws":
                session = boto3.Session()
                aws_creds = session.get_credentials()
                aws_region = session.region_name or "us-east-1"
                client.auth.aws.iam_login(
                    access_key=aws_creds.access_key,
                    secret_key=aws_creds.secret_key,
                    session_token=aws_creds.token,
                    region=aws_region,
                    role=vault_settings.get("role_name", None),
                    **login_kwargs,
                )
    except hvac.exceptions.InvalidRequest as err:
        raise exceptions.SecretProviderError(
            secret, cls, f"HashiCorp Vault Login failed (auth_method: {auth_method}). Error: {err}"
        ) from err
    except hvac.exceptions.Forbidden as err:
        raise exceptions.SecretProviderError(
            secret, cls, f"HashiCorp Vault Access Denied (auth_method: {auth_method}). Error: {err}"
        ) from err

    return client

get_value_for_secret(secret, obj=None, **kwargs) classmethod

Return the value stored under the secret’s key in the secret’s path.

Source code in nautobot_secrets_providers/providers/hashicorp.py
@classmethod
def get_value_for_secret(cls, secret, obj=None, **kwargs):
    """Return the value stored under the secret’s key in the secret’s path."""
    # Try to get parameters and error out early.
    parameters = secret.rendered_parameters(obj=obj)
    try:
        vault_name = parameters.get("vault", "default")
        vault_settings = cls.retrieve_vault_settings(vault_name)
    except KeyError:
        vault_settings = {}
    # Get the mount_point and kv_version from the Vault configuration. These default to the
    # default Vault that HashiCorp provides.
    secret_mount_point = vault_settings.get("default_mount_point", "secret")
    secret_kv_version = vault_settings.get("kv_version", HashicorpKVVersionChoices.KV_VERSION_2)

    try:
        secret_path = parameters["path"]
        secret_key = parameters["key"]
        # If the user does choose to override the Vault settings at their own risk, we will use
        # the settings they provide. These are here to support multiple vaults (vault engines) when
        # that was not allowed by the settings. Ideally these should be deprecated and removed in
        # the future.
        secret_mount_point = parameters.get("mount_point", secret_mount_point) or secret_mount_point
        secret_kv_version = parameters.get("kv_version", secret_kv_version) or secret_kv_version
    except KeyError as err:
        msg = f"The secret parameter could not be retrieved for field {err}"
        raise exceptions.SecretParametersError(secret, cls, msg) from err

    client = cls.get_client(secret, vault_name)

    try:
        if secret_kv_version == HashicorpKVVersionChoices.KV_VERSION_1:
            response = client.secrets.kv.v1.read_secret(path=secret_path, mount_point=secret_mount_point)
        else:
            response = client.secrets.kv.v2.read_secret(path=secret_path, mount_point=secret_mount_point)
    except hvac.exceptions.InvalidPath as err:
        raise exceptions.SecretValueNotFoundError(secret, cls, str(err)) from err

    # Retrieve the value using the key or complain loudly.
    try:
        if secret_kv_version == HashicorpKVVersionChoices.KV_VERSION_1:
            return response["data"][secret_key]
        return response["data"]["data"][secret_key]
    except KeyError as err:
        msg = f"The secret value could not be retrieved using key {err}"
        raise exceptions.SecretValueNotFoundError(secret, cls, msg) from err

retrieve_vault_settings(name=None) staticmethod

Retrieve the configuration from settings that matches the provided vault name.

Parameters:

Name Type Description Default
name str

Vault name to retrieve from settings. Defaults to None.

None

Returns:

Name Type Description
vault_settings dict

Hashicorp Vault Settings

Source code in nautobot_secrets_providers/providers/hashicorp.py
@staticmethod
def retrieve_vault_settings(name=None):
    """Retrieve the configuration from settings that matches the provided vault name.

    Args:
        name (str, optional): Vault name to retrieve from settings. Defaults to None.

    Returns:
        vault_settings (dict): Hashicorp Vault Settings
    """
    vault_settings = settings.PLUGINS_CONFIG["nautobot_secrets_providers"].get("hashicorp_vault", {})
    if name and "vaults" in vault_settings:
        return vault_settings["vaults"][name]
    return vault_settings

validate_vault_settings(secret=None, vault_name=None) classmethod

Validate the vault settings.

Source code in nautobot_secrets_providers/providers/hashicorp.py
@classmethod
def validate_vault_settings(cls, secret=None, vault_name=None):
    """Validate the vault settings."""
    try:
        vault_settings = cls.retrieve_vault_settings(vault_name)
    except KeyError as err:
        raise exceptions.SecretProviderError(
            secret, cls, f"HashiCorp Vault {vault_name} is not configured!"
        ) from err
    if not vault_settings:
        raise exceptions.SecretProviderError(secret, cls, f"HashiCorp Vault {vault_name} is not configured!")

    auth_method = vault_settings.get("auth_method", "token")
    kv_version = vault_settings.get("kv_version", HashicorpKVVersionChoices.KV_VERSION_2)

    if "url" not in vault_settings:
        raise exceptions.SecretProviderError(secret, cls, "HashiCorp Vault configuration is missing a url")

    if auth_method not in AUTH_METHOD_CHOICES:
        raise exceptions.SecretProviderError(secret, cls, f"HashiCorp Vault Auth Method {auth_method} is invalid!")

    if kv_version not in HashicorpKVVersionChoices.as_dict():
        raise exceptions.SecretProviderError(secret, cls, f"HashiCorp Vault KV version {kv_version} is invalid!")

    if auth_method == "aws" and not boto3:
        raise exceptions.SecretProviderError(
            secret, cls, "HashiCorp Vault AWS Authentication Method requires the boto3 library!"
        )
    if auth_method == "token" and "token" not in vault_settings:
        raise exceptions.SecretProviderError(
            secret, cls, "HashiCorp Vault configuration is missing a token for token authentication!"
        )
    if auth_method == "kubernetes" and "role_name" not in vault_settings:
        raise exceptions.SecretProviderError(
            secret, cls, "HashiCorp Vault configuration is missing a role name for kubernetes authentication!"
        )
    if auth_method == "approle" and ("role_id" not in vault_settings or "secret_id" not in vault_settings):
        raise exceptions.SecretProviderError(
            secret, cls, "HashiCorp Vault configuration is missing a role_id and/or secret_id!"
        )

    return vault_settings