diff --git a/development/nautobot_config.py b/development/nautobot_config.py
index f715a27..8b3fdac 100644
--- a/development/nautobot_config.py
+++ b/development/nautobot_config.py
@@ -145,8 +145,22 @@
"username": os.getenv("SECRET_SERVER_USERNAME", ""),
},
"hashicorp_vault": {
- "url": os.environ.get("HASHICORP_VAULT_URL"),
- "token": os.environ.get("HASHICORP_VAULT_TOKEN"),
+ # "url": os.environ.get("HASHICORP_VAULT_URL"),
+ # "token": os.environ.get("HASHICORP_VAULT_TOKEN"),
+ "vaults": {
+ "hashicorp_approle": {
+ "url": os.environ.get("HASHICORP_VAULT_URL"),
+ "auth_method": "approle",
+ "role_id": os.getenv("NAUTOBOT_HASHICORP_VAULT_ROLE_ID"),
+ "secret_id": os.getenv("NAUTOBOT_HASHICORP_VAULT_SECRET_ID"),
+ },
+ "hashicorp_v1_custom_mount": {
+ "url": os.environ.get("HASHICORP_VAULT_URL"),
+ "token": os.environ.get("HASHICORP_VAULT_TOKEN"),
+ "kv_version": "v1",
+ "default_mount_point": "secret_kv",
+ },
+ }
},
},
}
diff --git a/docs/admin/compatibility_matrix.md b/docs/admin/compatibility_matrix.md
index 74f5a5f..2734c17 100644
--- a/docs/admin/compatibility_matrix.md
+++ b/docs/admin/compatibility_matrix.md
@@ -6,3 +6,4 @@
| 1.4.X | 1.4.0 | 1.99.99 |
| 2.0.X | 2.0.0 | 2.99.99 |
| 3.0.X | 2.0.0 | 2.99.99 |
+| 3.1.X | 2.0.0 | 2.99.99 |
diff --git a/docs/admin/providers/hashicorp_setup.md b/docs/admin/providers/hashicorp_setup.md
index 3fd3ae5..04a7db3 100644
--- a/docs/admin/providers/hashicorp_setup.md
+++ b/docs/admin/providers/hashicorp_setup.md
@@ -27,3 +27,38 @@ PLUGINS_CONFIG = {
- `secret_id` - (optional) Required when `"auth_method": "approle"`.As with other sensitive service credentials, we recommend that you provide the secret_id value as an environment variable and retrieve it with `{"secret_id": os.getenv("NAUTOBOT_HASHICORP_VAULT_SECRET_ID")}` rather than hard-coding it in your `nautobot_config.py`.
- `login_kwargs` - (optional) Additional optional parameters to pass to the login method for [`approle`](https://hvac.readthedocs.io/en/stable/source/hvac_api_auth_methods.html#hvac.api.auth_methods.AppRole.login), [`aws`](https://hvac.readthedocs.io/en/stable/source/hvac_api_auth_methods.html#hvac.api.auth_methods.Aws.iam_login) and [`kubernetes`](https://hvac.readthedocs.io/en/stable/source/hvac_api_auth_methods.html#hvac.api.auth_methods.Kubernetes.login) authentication methods.
- `namespace` - (optional) Namespace to use for the [`X-Vault-Namespace` header](https://github.com/hvac/hvac/blob/main/hvac/adapters.py#L287) on all hvac client requests. Required when the [`Namespaces`](https://developer.hashicorp.com/vault/docs/enterprise/namespaces#usage) feature is enabled in Vault Enterprise.
+
+### Multiple Hashicorp Vaults
+
++++ 3.1.0
+
+Hashicorp Provider now supports using multiple vaults (configurations). You will be able to choose the vault when creating a secret, For example, you could have one vault using `approle` authentication, and a second vault using `token` authentication in combination with a different default mount point:
+
+```python
+PLUGINS_CONFIG = {
+ "nautobot_secrets_providers": {
+ "hashicorp_vault": {
+ "vaults": {
+ "hashicorp_approle": {
+ "url": os.environ.get("HASHICORP_VAULT_URL"),
+ "auth_method": "approle",
+ "role_id": os.getenv("NAUTOBOT_HASHICORP_VAULT_ROLE_ID"),
+ "secret_id": os.getenv("NAUTOBOT_HASHICORP_VAULT_SECRET_ID"),
+ },
+ "hashicorp_v1_custom_mount": {
+ "url": os.environ.get("HASHICORP_VAULT_URL"),
+ "token": os.environ.get("HASHICORP_VAULT_TOKEN"),
+ "kv_version": "v1",
+ "default_mount_point": "secret_kv",
+ },
+ }
+ }
+ },
+}
+```
+
+![Select Secret Configuration](../../images/light/hashicorp_multiple_vaults.png#only-light)
+![Select Secret Configuration](../../images/dark/hashicorp_multiple_vaults.png#only-dark)
+
+!!! note
+ If using this option, you should not have any keys except `vaults` under `hashicorp_vault`.
diff --git a/docs/admin/release_notes/version_3.1.md b/docs/admin/release_notes/version_3.1.md
new file mode 100644
index 0000000..783f9ab
--- /dev/null
+++ b/docs/admin/release_notes/version_3.1.md
@@ -0,0 +1,21 @@
+# v3.1 Release Notes
+
+This document describes all new features and changes in the release `3.1`. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## Release Overview
+
+This release adds support for multiple HashiCorp Vault secrets providers.
+
+## [v3.1.0 (2024-08-01)](https://github.com/nautobot/nautobot-app-secrets-providers/releases/tag/v3.1.0)
+
+### Added
+
+- [#67](https://github.com/nautobot/nautobot-app-secrets-providers/issues/67) - Added the ability to choose between multiple vaults (configurations) for HashiCorp.
+
+### Documentation
+
+- [#137](https://github.com/nautobot/nautobot-app-secrets-providers/issues/137) - Updated documentation links for installed apps page.
+
+### Housekeeping
+
+- [#140](https://github.com/nautobot/nautobot-app-secrets-providers/issues/140) - Updated development environment to use `certifi` `2024.7.4`.
diff --git a/docs/images/dark/hashicorp_multiple_vaults.png b/docs/images/dark/hashicorp_multiple_vaults.png
new file mode 100644
index 0000000..bc38e6d
Binary files /dev/null and b/docs/images/dark/hashicorp_multiple_vaults.png differ
diff --git a/docs/images/light/hashicorp_multiple_vaults.png b/docs/images/light/hashicorp_multiple_vaults.png
new file mode 100644
index 0000000..10eb187
Binary files /dev/null and b/docs/images/light/hashicorp_multiple_vaults.png differ
diff --git a/nautobot_secrets_providers/__init__.py b/nautobot_secrets_providers/__init__.py
index 748e0de..b19b732 100644
--- a/nautobot_secrets_providers/__init__.py
+++ b/nautobot_secrets_providers/__init__.py
@@ -22,6 +22,7 @@ class NautobotSecretsProvidersConfig(NautobotAppConfig):
max_version = "2.9999"
default_settings = {}
caching_config = {}
+ docs_view_name = "plugins:nautobot_secrets_providers:docs"
# URL reverse lookup names
home_view_name = "plugins:nautobot_secrets_providers:home"
diff --git a/nautobot_secrets_providers/providers/hashicorp.py b/nautobot_secrets_providers/providers/hashicorp.py
index c0a5cd8..a1989c6 100644
--- a/nautobot_secrets_providers/providers/hashicorp.py
+++ b/nautobot_secrets_providers/providers/hashicorp.py
@@ -13,7 +13,7 @@
except ImportError:
hvac = None
-from nautobot.core.forms import BootstrapMixin
+from nautobot.core.forms import add_blank_choice, BootstrapMixin
from nautobot.extras.secrets import exceptions, SecretsProvider
from .choices import HashicorpKVVersionChoices
@@ -24,19 +24,17 @@
AUTH_METHOD_CHOICES = ["approle", "aws", "kubernetes", "token"]
-# Default mount point for the HVAC client
-try:
- plugins_config = settings.PLUGINS_CONFIG["nautobot_secrets_providers"]
- DEFAULT_MOUNT_POINT = plugins_config["hashicorp_vault"]["default_mount_point"]
-except KeyError:
- DEFAULT_MOUNT_POINT = "secret"
+def vault_choices():
+ """Generate Choices for vault form field.
-# Default kv version for the HVAC client
-try:
- plugins_config = settings.PLUGINS_CONFIG["nautobot_secrets_providers"]
- DEFAULT_KV_VERSION = plugins_config["hashicorp_vault"]["default_kv_version"]
-except KeyError:
- DEFAULT_KV_VERSION = HashicorpKVVersionChoices.KV_VERSION_2
+ If `vaults` is a key in the vault config,
+ then we build a form option for each key in vaults.
+ Otherwise we fall "Default" to make this a non-breaking change.
+ """
+ plugin_settings = settings.PLUGINS_CONFIG["nautobot_secrets_providers"]
+ if "vaults" in plugin_settings["hashicorp_vault"]:
+ return [(key, key.replace("_", " ").title()) for key in plugin_settings["hashicorp_vault"]["vaults"].keys()]
+ return [("default", "Default")]
class HashiCorpVaultSecretsProvider(SecretsProvider):
@@ -59,28 +57,50 @@ class ParametersForm(BootstrapMixin, forms.Form):
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=f"The path where the secret engine was mounted on (Default: {DEFAULT_MOUNT_POINT}
)",
- initial=DEFAULT_MOUNT_POINT,
+ 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=HashicorpKVVersionChoices,
- help_text=f"The version of the kv engine (either v1 or v2) (Default: {DEFAULT_KV_VERSION}
)",
- initial=DEFAULT_KV_VERSION,
+ 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):
+ def validate_vault_settings(cls, secret=None, vault_name=None):
"""Validate the vault settings."""
- # This is only required for HashiCorp Vault therefore not defined in
- # `required_settings` for the plugin config.
- plugin_settings = settings.PLUGINS_CONFIG["nautobot_secrets_providers"]
- if "hashicorp_vault" not in plugin_settings:
- raise exceptions.SecretProviderError(secret, cls, "HashiCorp Vault is not configured!")
+ 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!")
- vault_settings = plugin_settings.get("hashicorp_vault", {})
auth_method = vault_settings.get("auth_method", "token")
kv_version = vault_settings.get("kv_version", HashicorpKVVersionChoices.KV_VERSION_2)
@@ -93,33 +113,29 @@ def validate_vault_settings(cls, secret=None):
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":
- if not boto3:
- raise exceptions.SecretProviderError(
- secret, cls, "HashiCorp Vault AWS Authentication Method requires the boto3 library!"
- )
- elif auth_method == "token":
- if "token" not in vault_settings:
- raise exceptions.SecretProviderError(
- secret, cls, "HashiCorp Vault configuration is missing a token for token authentication!"
- )
- elif auth_method == "kubernetes":
- if "role_name" not in vault_settings:
- raise exceptions.SecretProviderError(
- secret, cls, "HashiCorp Vault configuration is missing a role name for kubernetes authentication!"
- )
- elif auth_method == "approle":
- if "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!"
- )
+ 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):
+ 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_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)
@@ -136,7 +152,10 @@ def get_client(cls, secret=None):
try:
if auth_method == "token":
client = hvac.Client(
- url=vault_settings["url"], token=vault_settings["token"], verify=ca_cert, namespace=namespace
+ 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)
@@ -178,16 +197,30 @@ 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"]
- secret_mount_point = parameters.get("mount_point", DEFAULT_MOUNT_POINT)
- secret_kv_version = parameters.get("kv_version", DEFAULT_KV_VERSION)
+ # 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)
+ client = cls.get_client(secret, vault_name)
try:
if secret_kv_version == HashicorpKVVersionChoices.KV_VERSION_1:
diff --git a/nautobot_secrets_providers/tests/test_providers.py b/nautobot_secrets_providers/tests/test_providers.py
index c4311c7..d876746 100644
--- a/nautobot_secrets_providers/tests/test_providers.py
+++ b/nautobot_secrets_providers/tests/test_providers.py
@@ -18,6 +18,7 @@
AWSSystemsManagerParameterStore,
HashiCorpVaultSecretsProvider,
)
+from nautobot_secrets_providers.providers.hashicorp import vault_choices
from nautobot_secrets_providers.providers.choices import HashicorpKVVersionChoices
@@ -201,6 +202,15 @@ def setUp(self):
)
self.test_path = "http://localhost:8200/v1/secret/data/hello"
self.test_mountpoint_path = "http://localhost:8200/v1/mymount/data/hello"
+ self.secret_configuration = Secret.objects.create(
+ name="hello-hashicorp-configuration",
+ provider=self.provider.slug,
+ parameters={
+ "path": "hello",
+ "key": "location",
+ "vault": "example",
+ },
+ )
@requests_mock.Mocker()
def test_v1(self, requests_mocker):
@@ -292,6 +302,43 @@ def test_retrieve_mount_point_success(self, requests_mocker):
response = self.provider.get_value_for_secret(self.secret_mounting_point)
self.assertEqual(self.mock_response["data"]["data"]["location"], response)
+ @requests_mock.Mocker()
+ def test_retrieve_configuration_success(self, requests_mocker):
+ requests_mocker.register_uri(method="GET", url=self.test_path, json=self.mock_response)
+
+ multiple_plugins_config = {
+ "nautobot_secrets_providers": {
+ "hashicorp_vault": {
+ "vaults": {
+ "example": {"token": "nautobot", "url": "http://localhost:8200"},
+ "example_2": {"token": "nautobot", "url": "http://example.com"},
+ }
+ }
+ }
+ }
+ with self.settings(PLUGINS_CONFIG=multiple_plugins_config):
+ response = self.provider.get_value_for_secret(self.secret_configuration)
+ self.assertEqual(self.mock_response["data"]["data"]["location"], response)
+
+ def test_retrieve_configuration_non_configured_vault(self):
+ multiple_plugins_config = {
+ "nautobot_secrets_providers": {
+ "hashicorp_vault": {
+ "vaults": {
+ "example": {"token": "nautobot", "url": "http://localhost:8200"},
+ "example_2": {"token": "nautobot", "url": "http://example.com"},
+ }
+ }
+ }
+ }
+ with self.settings(PLUGINS_CONFIG=multiple_plugins_config):
+ with self.assertRaises(exceptions.SecretProviderError) as err:
+ self.provider.validate_vault_settings(self.secret, "test")
+ self.assertEqual(
+ str(err.exception),
+ 'SecretProviderError: Secret "hello-hashicorp" (provider "HashiCorpVaultSecretsProvider"): HashiCorp Vault test is not configured!',
+ )
+
@requests_mock.Mocker()
def test_retrieve_invalid_parameters(self, requests_mocker):
"""Try and fail to retrieve a secret with incorrect parameters."""
@@ -409,13 +456,35 @@ def test_valid_settings(self):
returned_settings = self.provider.validate_vault_settings(self.secret)
self.assertEqual(returned_settings, settings.PLUGINS_CONFIG["nautobot_secrets_providers"]["hashicorp_vault"])
+ # Test with default configuration
+ returned_settings = self.provider.validate_vault_settings(self.secret, "default")
+ self.assertEqual(returned_settings, settings.PLUGINS_CONFIG["nautobot_secrets_providers"]["hashicorp_vault"])
+
+ # Test with named default configuration
+ multiple_plugins_config = {
+ "nautobot_secrets_providers": {
+ "hashicorp_vault": {
+ "vaults": {
+ "default": {"token": "nautobot", "url": "http://localhost:8200"},
+ "example_2": {"token": "nautobot", "url": "http://example.com"},
+ }
+ }
+ }
+ }
+ with self.settings(PLUGINS_CONFIG=multiple_plugins_config):
+ returned_settings = self.provider.validate_vault_settings(self.secret, "default")
+ self.assertEqual(
+ returned_settings,
+ settings.PLUGINS_CONFIG["nautobot_secrets_providers"]["hashicorp_vault"]["vaults"]["default"],
+ )
+
# No nautobot_secrets_providers
with self.settings(PLUGINS_CONFIG={"nautobot_secrets_providers": {}}):
with self.assertRaises(exceptions.SecretProviderError) as err:
- self.provider.validate_vault_settings(self.secret)
+ self.provider.validate_vault_settings(self.secret, "default")
self.assertEqual(
str(err.exception),
- 'SecretProviderError: Secret "hello-hashicorp" (provider "HashiCorpVaultSecretsProvider"): HashiCorp Vault is not configured!',
+ 'SecretProviderError: Secret "hello-hashicorp" (provider "HashiCorpVaultSecretsProvider"): HashiCorp Vault default is not configured!',
)
vault_url = "http://localhost:8200"
@@ -491,6 +560,30 @@ def test_valid_settings(self):
'SecretProviderError: Secret "hello-hashicorp" (provider "HashiCorpVaultSecretsProvider"): HashiCorp Vault configuration is missing a role_id and/or secret_id!',
)
+ def test_multiple_valid_settings(self):
+ # Test with a configuration passed in
+ multiple_plugins_config = {
+ "nautobot_secrets_providers": {
+ "hashicorp_vault": {
+ "vaults": {
+ "example": {"token": "nautobot", "url": "http://localhost:8200"},
+ "example_2": {"token": "nautobot", "url": "http://example.com"},
+ }
+ }
+ }
+ }
+ with self.settings(PLUGINS_CONFIG=multiple_plugins_config):
+ returned_settings = self.provider.validate_vault_settings(self.secret, "example")
+ self.assertEqual(
+ returned_settings,
+ settings.PLUGINS_CONFIG["nautobot_secrets_providers"]["hashicorp_vault"]["vaults"]["example"],
+ )
+ returned_settings = self.provider.validate_vault_settings(self.secret, "example_2")
+ self.assertEqual(
+ returned_settings,
+ settings.PLUGINS_CONFIG["nautobot_secrets_providers"]["hashicorp_vault"]["vaults"]["example_2"],
+ )
+
@patch.dict(os.environ, aws_auth_env_vars)
@requests_mock.Mocker()
def test_get_client_aws(self, requests_mocker):
@@ -534,6 +627,23 @@ def test_get_client_aws(self, requests_mocker):
'SecretProviderError: Secret "hello-hashicorp" (provider "HashiCorpVaultSecretsProvider"): HashiCorp Vault Login failed (auth_method: aws). Error: , on post http://localhost:8200/v1/auth/aws/login',
)
+ def test_vault_choices(self):
+ choices = vault_choices()
+ self.assertEqual(choices, [("default", "Default")])
+ multiple_plugins_config = {
+ "nautobot_secrets_providers": {
+ "hashicorp_vault": {
+ "vaults": {
+ "example": {"token": "nautobot", "url": "http://localhost:8200"},
+ "example_2": {"token": "nautobot", "url": "http://example.com"},
+ }
+ }
+ }
+ }
+ with self.settings(PLUGINS_CONFIG=multiple_plugins_config):
+ choices = vault_choices()
+ self.assertEqual(choices, [("example", "Example"), ("example_2", "Example 2")])
+
class AWSSystemsManagerParameterStoreTestCase(SecretsProviderTestCase):
"""Tests for AWSSystemsManagerParameterStore."""
diff --git a/nautobot_secrets_providers/urls.py b/nautobot_secrets_providers/urls.py
index 5299d1d..13e12a9 100644
--- a/nautobot_secrets_providers/urls.py
+++ b/nautobot_secrets_providers/urls.py
@@ -1,6 +1,8 @@
"""Django urlpatterns declaration for nautobot_secrets_providers app."""
from django.urls import path
+from django.templatetags.static import static
+from django.views.generic import RedirectView
from nautobot_secrets_providers import views
@@ -9,4 +11,5 @@
urlpatterns = [
path("", views.SecretsProvidersHomeView.as_view(), name="home"),
+ path("docs/", RedirectView.as_view(url=static("nautobot_secrets_providers/docs/index.html")), name="docs"),
]
diff --git a/poetry.lock b/poetry.lock
index 27fd91a..fcdb4b9 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -417,13 +417,13 @@ zstd = ["zstandard (==0.22.0)"]
[[package]]
name = "certifi"
-version = "2024.6.2"
+version = "2024.7.4"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
- {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
- {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
+ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
+ {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
]
[[package]]
@@ -2973,6 +2973,7 @@ files = [
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -2980,8 +2981,16 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+ {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -2998,6 +3007,7 @@ files = [
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -3005,6 +3015,7 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
diff --git a/pyproject.toml b/pyproject.toml
index 5cf63b0..30e035d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "nautobot-secrets-providers"
-version = "3.0.0"
+version = "3.1.0"
description = "Nautobot Secrets Providers App"
authors = ["Network to Code, LLC "]
license = "Apache-2.0"