diff --git a/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md b/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md index 7d60250d6f86..363e3be72235 100644 --- a/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md @@ -4,6 +4,10 @@ ### Fixed - Correct typing for async paging methods +### Added +- Added method `parse_key_vault_secret_id` that parses out a full ID returned by Key Vault, so users can easily +access the secret's `name`, `vault_url`, and `version`. + ## 4.2.0 (2020-08-11) ### Fixed - Values of `x-ms-keyvault-region` and `x-ms-keyvault-service-version` headers diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/__init__.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/__init__.py index 3f6e1c5ff4fc..e2885143bebd 100644 --- a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/__init__.py +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/__init__.py @@ -3,10 +3,20 @@ # Licensed under the MIT License. # ------------------------------------ from ._models import DeletedSecret, KeyVaultSecret, SecretProperties +from ._parse_id import parse_key_vault_secret_id +from ._shared import KeyVaultResourceId from ._shared.client_base import ApiVersion from ._client import SecretClient -__all__ = ["ApiVersion", "SecretClient", "KeyVaultSecret", "SecretProperties", "DeletedSecret"] +__all__ = [ + "ApiVersion", + "SecretClient", + "KeyVaultSecret", + "SecretProperties", + "DeletedSecret", + "parse_key_vault_secret_id", + "KeyVaultResourceId" +] from ._version import VERSION __version__ = VERSION diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_models.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_models.py index 0f022470b6e6..fdddcaa8cbde 100644 --- a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_models.py +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_models.py @@ -2,7 +2,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from ._shared import parse_vault_id +from ._shared import parse_key_vault_id try: from typing import TYPE_CHECKING @@ -23,7 +23,7 @@ def __init__(self, attributes, vault_id, **kwargs): # type: (_models.SecretAttributes, str, **Any) -> None self._attributes = attributes self._id = vault_id - self._vault_id = parse_vault_id(vault_id) + self._vault_id = parse_key_vault_id(vault_id) self._content_type = kwargs.get("content_type", None) self._key_id = kwargs.get("key_id", None) self._managed = kwargs.get("managed", None) diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_parse_id.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_parse_id.py new file mode 100644 index 000000000000..d7a3374ce118 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_parse_id.py @@ -0,0 +1,29 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +from ._shared import parse_key_vault_id, KeyVaultResourceId + + +def parse_key_vault_secret_id(source_id): + # type: (str) -> KeyVaultResourceId + """Parses a secret's full ID into a class with parsed contents as attributes. + + :param str source_id: the full original identifier of a secret + :returns: Returns a parsed secret ID as a :class:`KeyVaultResourceId` + :rtype: ~azure.keyvault.secrets.KeyVaultResourceId + :raises: ValueError + Example: + .. literalinclude:: ../tests/test_parse_id.py + :start-after: [START parse_key_vault_secret_id] + :end-before: [END parse_key_vault_secret_id] + :language: python + :caption: Parse a secret's ID + :dedent: 8 + """ + parsed_id = parse_key_vault_id(source_id) + + return KeyVaultResourceId( + name=parsed_id.name, source_id=parsed_id.source_id, vault_url=parsed_id.vault_url, version=parsed_id.version + ) diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py index e13f15a61c71..578deccfb147 100644 --- a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py @@ -2,19 +2,22 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from collections import namedtuple - try: import urllib.parse as parse except ImportError: # pylint:disable=import-error import urlparse as parse # type: ignore +from typing import TYPE_CHECKING from .challenge_auth_policy import ChallengeAuthPolicy, ChallengeAuthPolicyBase from .client_base import KeyVaultClientBase from .http_challenge import HttpChallenge from . import http_challenge_cache as HttpChallengeCache +if TYPE_CHECKING: + # pylint: disable=unused-import + from typing import Optional + __all__ = [ "ChallengeAuthPolicy", @@ -24,25 +27,45 @@ "KeyVaultClientBase", ] -_VaultId = namedtuple("VaultId", ["vault_url", "collection", "name", "version"]) +class KeyVaultResourceId(): + """Represents a Key Vault identifier and its parsed contents. + + :param str source_id: The complete identifier received from Key Vault + :param str vault_url: The vault URL + :param str name: The name extracted from the ID + :param str version: The version extracted from the ID + """ + + def __init__( + self, + source_id, # type: str + vault_url, # type: str + name, # type: str + version=None # type: Optional[str] + ): + self.source_id = source_id + self.vault_url = vault_url + self.name = name + self.version = version -def parse_vault_id(url): +def parse_key_vault_id(source_id): + # type: (str) -> KeyVaultResourceId try: - parsed_uri = parse.urlparse(url) + parsed_uri = parse.urlparse(source_id) except Exception: # pylint: disable=broad-except - raise ValueError("'{}' is not not a valid url".format(url)) + raise ValueError("'{}' is not not a valid url".format(source_id)) if not (parsed_uri.scheme and parsed_uri.hostname): - raise ValueError("'{}' is not not a valid url".format(url)) + raise ValueError("'{}' is not not a valid url".format(source_id)) path = list(filter(None, parsed_uri.path.split("/"))) if len(path) < 2 or len(path) > 3: - raise ValueError("'{}' is not not a valid vault url".format(url)) + raise ValueError("'{}' is not not a valid vault url".format(source_id)) - return _VaultId( + return KeyVaultResourceId( + source_id=source_id, vault_url="{}://{}".format(parsed_uri.scheme, parsed_uri.hostname), - collection=path[0], name=path[1], version=path[2] if len(path) == 3 else None, ) diff --git a/sdk/keyvault/azure-keyvault-secrets/tests/recordings/test_parse_id.test_parse_secret_id_with_version.yaml b/sdk/keyvault/azure-keyvault-secrets/tests/recordings/test_parse_id.test_parse_secret_id_with_version.yaml new file mode 100644 index 000000000000..65ac7c1dede3 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-secrets/tests/recordings/test_parse_id.test_parse_secret_id_with_version.yaml @@ -0,0 +1,146 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-secrets/4.2.1 Python/3.5.3 (Windows-10-10.0.19041-SP0) + method: PUT + uri: https://vaultname.vault.azure.net/secrets/secretce671360?api-version=7.1 + response: + body: + string: '{"error":{"code":"Unauthorized","message":"Request is missing a Bearer + or PoP token."}}' + headers: + cache-control: + - no-cache + content-length: + - '87' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 06 Nov 2020 23:51:37 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000;includeSubDomains + www-authenticate: + - Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: + - nosniff + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=162.211.216.102;act_addr_fam=InterNetwork; + x-ms-keyvault-region: + - westus + x-ms-keyvault-service-version: + - 1.2.58.0 + x-powered-by: + - ASP.NET + status: + code: 401 + message: Unauthorized +- request: + body: '{"value": "secret_value"}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '25' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-secrets/4.2.1 Python/3.5.3 (Windows-10-10.0.19041-SP0) + method: PUT + uri: https://vaultname.vault.azure.net/secrets/secretce671360?api-version=7.1 + response: + body: + string: '{"value":"secret_value","id":"https://vaultname.vault.azure.net/secrets/secretce671360/0fb32b11fdbf47eb9973e04a064a5b3f","attributes":{"enabled":true,"created":1604706698,"updated":1604706698,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: + - no-cache + content-length: + - '269' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 06 Nov 2020 23:51:38 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000;includeSubDomains + x-content-type-options: + - nosniff + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=162.211.216.102;act_addr_fam=InterNetwork; + x-ms-keyvault-region: + - westus + x-ms-keyvault-service-version: + - 1.2.58.0 + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - azsdk-python-keyvault-secrets/4.2.1 Python/3.5.3 (Windows-10-10.0.19041-SP0) + method: GET + uri: https://vaultname.vault.azure.net/secrets/secretce671360/?api-version=7.1 + response: + body: + string: '{"value":"secret_value","id":"https://vaultname.vault.azure.net/secrets/secretce671360/0fb32b11fdbf47eb9973e04a064a5b3f","attributes":{"enabled":true,"created":1604706698,"updated":1604706698,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: + - no-cache + content-length: + - '269' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 06 Nov 2020 23:51:38 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000;includeSubDomains + x-content-type-options: + - nosniff + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=162.211.216.102;act_addr_fam=InterNetwork; + x-ms-keyvault-region: + - westus + x-ms-keyvault-service-version: + - 1.2.58.0 + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +version: 1 diff --git a/sdk/keyvault/azure-keyvault-secrets/tests/test_parse_id.py b/sdk/keyvault/azure-keyvault-secrets/tests/test_parse_id.py new file mode 100644 index 000000000000..efd2b239692a --- /dev/null +++ b/sdk/keyvault/azure-keyvault-secrets/tests/test_parse_id.py @@ -0,0 +1,54 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------- +from azure.keyvault.secrets import SecretClient, parse_key_vault_secret_id +from devtools_testutils import ResourceGroupPreparer, KeyVaultPreparer + +from _shared.preparer import KeyVaultClientPreparer +from _shared.test_case import KeyVaultTestCase + + +class TestParseId(KeyVaultTestCase): + @ResourceGroupPreparer(random_name_enabled=True) + @KeyVaultPreparer() + @KeyVaultClientPreparer(SecretClient) + def test_parse_secret_id_with_version(self, client): + secret_name = self.get_resource_name("secret") + secret_value = "secret_value" + # create secret + created_secret = client.set_secret(secret_name, secret_value) + + # [START parse_key_vault_secret_id] + secret = client.get_secret(secret_name) + parsed_secret_id = parse_key_vault_secret_id(secret.id) + + print(parsed_secret_id.name) + print(parsed_secret_id.vault_url) + print(parsed_secret_id.version) + print(parsed_secret_id.source_id) + # [END parse_key_vault_secret_id] + assert parsed_secret_id.name == secret_name + assert parsed_secret_id.vault_url == client.vault_url + assert parsed_secret_id.version == secret.properties.version + assert parsed_secret_id.source_id == secret.id + + +def test_parse_secret_id_with_pending_version(): + source_id = "https://keyvault-name.vault.azure.net/secrets/secret-name/pending" + parsed_secret_id = parse_key_vault_secret_id(source_id) + + assert parsed_secret_id.name == "secret-name" + assert parsed_secret_id.vault_url == "https://keyvault-name.vault.azure.net" + assert parsed_secret_id.version == "pending" + assert parsed_secret_id.source_id == "https://keyvault-name.vault.azure.net/secrets/secret-name/pending" + + +def test_parse_deleted_secret_id(): + source_id = "https://keyvault-name.vault.azure.net/deletedsecrets/deleted-secret" + parsed_secret_id = parse_key_vault_secret_id(source_id) + + assert parsed_secret_id.name == "deleted-secret" + assert parsed_secret_id.vault_url == "https://keyvault-name.vault.azure.net" + assert parsed_secret_id.version is None + assert parsed_secret_id.source_id == "https://keyvault-name.vault.azure.net/deletedsecrets/deleted-secret"