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"