diff --git a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/__init__.py b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/__init__.py index 5f45ea2182a5..0b3dd35321d4 100644 --- a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/__init__.py +++ b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/__init__.py @@ -3,9 +3,11 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for # license information. # -------------------------------------------------------------------------- +from .keys._client import KeyClient +from .secrets._client import SecretClient from .vault_client import VaultClient from .version import VERSION -__all__ = ["VaultClient"] +__all__ = ["VaultClient", "KeyClient", "SecretClient"] __version__ = VERSION diff --git a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/_internal.py b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/_internal.py index 871a4e6ec401..ecd11ad97617 100644 --- a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/_internal.py +++ b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/_internal.py @@ -3,19 +3,34 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for # license information. # -------------------------------------------------------------------------- -from azure.core.pipeline.policies import HTTPPolicy +from collections import namedtuple +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # pylint:disable=unused-import + from typing import Any, Mapping, Optional + from azure.core.credentials import TokenCredential + from azure.core.pipeline.transport import HttpTransport try: import urllib.parse as parse except ImportError: import urlparse as parse # pylint: disable=import-error -from collections import namedtuple +from azure.core import Configuration +from azure.core.pipeline import Pipeline +from azure.core.pipeline.policies import BearerTokenCredentialPolicy, HTTPPolicy +from azure.core.pipeline.transport import RequestsTransport + +from ._generated import KeyVaultClient _VaultId = namedtuple("VaultId", ["vault_url", "collection", "name", "version"]) +KEY_VAULT_SCOPES = ("https://vault.azure.net/.default",) + + def _parse_vault_id(url): try: parsed_uri = parse.urlparse(url) @@ -37,13 +52,67 @@ def _parse_vault_id(url): ) -# TODO: integrate with azure.core -class _BearerTokenCredentialPolicy(HTTPPolicy): - def __init__(self, credentials): - self._credentials = credentials +class _KeyVaultClientBase(object): + """ + :param credential: A credential or credential provider which can be used to authenticate to the vault, + a ValueError will be raised if the entity is not provided + :type credential: azure.core.credentials.TokenCredential + :param str vault_url: The url of the vault to which the client will connect, + a ValueError will be raised if the entity is not provided + :param ~azure.core.configuration.Configuration config: The configuration for the KeyClient + """ + + @staticmethod + def create_config(credential, api_version=None, **kwargs): + # type: (TokenCredential, Optional[str], Mapping[str, Any]) -> Configuration + if api_version is None: + api_version = KeyVaultClient.DEFAULT_API_VERSION + config = KeyVaultClient.get_configuration_class(api_version, aio=False)(credential, **kwargs) + config.authentication_policy = BearerTokenCredentialPolicy(credential, scopes=KEY_VAULT_SCOPES) + return config + + def __init__(self, vault_url, credential, config=None, transport=None, api_version=None, **kwargs): + # type: (str, TokenCredential, Configuration, Optional[HttpTransport], Optional[str], **Any) -> None + if not credential: + raise ValueError( + "credential should be an object supporting the TokenCredential protocol, such as a credential from azure-identity" + ) + if not vault_url: + raise ValueError("vault_url must be the URL of an Azure Key Vault") + + self._vault_url = vault_url.strip(" /") + + client = kwargs.pop("generated_client", None) + if client: + # caller provided a configured client -> nothing left to initialize + self._client = client + return + + if api_version is None: + api_version = KeyVaultClient.DEFAULT_API_VERSION + + config = config or self.create_config(credential, api_version=api_version, **kwargs) + pipeline = kwargs.pop("pipeline", None) or self._build_pipeline(config, transport) + self._client = KeyVaultClient(credential, api_version=api_version, pipeline=pipeline, aio=False, **kwargs) + + def _build_pipeline(self, config, transport): + # type: (Configuration, HttpTransport) -> Pipeline + policies = [ + config.headers_policy, + config.user_agent_policy, + config.proxy_policy, + config.redirect_policy, + config.retry_policy, + config.authentication_policy, + config.logging_policy, + ] + + if transport is None: + transport = RequestsTransport(config) - def send(self, request, **kwargs): - auth_header = "Bearer " + self._credentials.token["access_token"] - request.http_request.headers["Authorization"] = auth_header + return Pipeline(transport, policies=policies) - return self.next.send(request, **kwargs) + @property + def vault_url(self): + # type: () -> str + return self._vault_url diff --git a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/_internal.py b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/_internal.py index e37f81029049..34599bb92b53 100644 --- a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/_internal.py +++ b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/_internal.py @@ -3,25 +3,112 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for # license information. # -------------------------------------------------------------------------- -from typing import Any, Callable +from typing import Any, Callable, Mapping, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + try: + from azure.core.credentials import TokenCredential + except ImportError: + # TokenCredential is a typing_extensions.Protocol; we don't depend on that package + pass from azure.core.async_paging import AsyncPagedMixin +from azure.core.configuration import Configuration +from azure.core.pipeline import AsyncPipeline +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy +from azure.core.pipeline.transport import AsyncioRequestsTransport, HttpTransport from msrest.serialization import Model +from .._generated import KeyVaultClient +from .._internal import KEY_VAULT_SCOPES + class AsyncPagingAdapter: """For each item in an AsyncPagedMixin, returns the result of applying fn to that item. Python 3.6 added syntax that could replace this (yield within async for).""" - def __init__(self, pages, fn): - # type: (AsyncPagedMixin, Callable[[Model], Any]) -> None + + def __init__(self, pages: AsyncPagedMixin, fn: Callable[[Model], Any]) -> None: self._pages = pages self._fn = fn def __aiter__(self): return self - async def __anext__(self): + async def __anext__(self) -> Any: item = await self._pages.__anext__() if not item: raise StopAsyncIteration return self._fn(item) + + +class _AsyncKeyVaultClientBase: + """ + :param credential: A credential or credential provider which can be used to authenticate to the vault, + a ValueError will be raised if the entity is not provided + :type credential: azure.authentication.Credential or azure.authentication.CredentialProvider + :param str vault_url: The url of the vault to which the client will connect, + a ValueError will be raised if the entity is not provided + :param ~azure.core.configuration.Configuration config: The configuration for the SecretClient + """ + + @staticmethod + def create_config( + credential: "TokenCredential", api_version: str = None, **kwargs: Mapping[str, Any] + ) -> Configuration: + if api_version is None: + api_version = KeyVaultClient.DEFAULT_API_VERSION + config = KeyVaultClient.get_configuration_class(api_version, aio=True)(credential, **kwargs) + config.authentication_policy = AsyncBearerTokenCredentialPolicy(credential, scopes=KEY_VAULT_SCOPES) + return config + + def __init__( + self, + vault_url: str, + credential: "TokenCredential", + config: Configuration = None, + transport: HttpTransport = None, + api_version: str = None, + **kwargs: Any + ) -> None: + if not credential: + raise ValueError( + "credential should be an object supporting the TokenCredential protocol, such as a credential from azure-identity" + ) + if not vault_url: + raise ValueError("vault_url must be the URL of an Azure Key Vault") + + self._vault_url = vault_url.strip(" /") + + client = kwargs.pop("generated_client", None) + if client: + # caller provided a configured client -> nothing left to initialize + self._client = client + return + + if api_version is None: + api_version = KeyVaultClient.DEFAULT_API_VERSION + + config = config or self.create_config(credential, api_version=api_version, **kwargs) + pipeline = kwargs.pop("pipeline", None) or self._build_pipeline(config, transport=transport) + self._client = KeyVaultClient(credential, api_version=api_version, pipeline=pipeline, aio=True) + + @staticmethod + def _build_pipeline(config: Configuration, transport: HttpTransport) -> AsyncPipeline: + policies = [ + config.headers_policy, + config.user_agent_policy, + config.proxy_policy, + config.redirect_policy, + config.retry_policy, + config.authentication_policy, + config.logging_policy, + ] + + if transport is None: + transport = AsyncioRequestsTransport(config) + + return AsyncPipeline(transport, policies=policies) + + @property + def vault_url(self) -> str: + return self._vault_url diff --git a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/keys/_client.py b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/keys/_client.py index 7d25ae01376f..e7a896214f68 100644 --- a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/keys/_client.py +++ b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/keys/_client.py @@ -3,31 +3,18 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for # license information. # -------------------------------------------------------------------------- -from typing import Any, AsyncIterable, Mapping, Optional, Dict, List from datetime import datetime +from typing import Any, AsyncIterable, Mapping, Optional, Dict, List -from azure.core.configuration import Configuration from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError -from azure.core.pipeline.transport import AsyncioRequestsTransport -from azure.core.pipeline import AsyncPipeline - -from azure.security.keyvault._internal import _BearerTokenCredentialPolicy -from azure.security.keyvault._generated import KeyVaultClient -from azure.security.keyvault.aio._internal import AsyncPagingAdapter +from .._internal import _AsyncKeyVaultClientBase, AsyncPagingAdapter from ...keys._models import Key, DeletedKey, KeyBase, KeyOperationResult -class KeyClient: +class KeyClient(_AsyncKeyVaultClientBase): """The KeyClient class defines a high level interface for managing keys in the specified vault. - - :param credentials: A credential or credential provider which can be used to authenticate to the vault, - a ValueError will be raised if the entity is not provided - :type credentials: azure.authentication.Credential or azure.authentication.CredentialProvider - :param str vault_url: The url of the vault to which the client will connect, - a ValueError will be raised if the entity is not provided - :param ~azure.core.configuration.Configuration config: The configuration for the KeyClient - + Example: .. literalinclude:: ../tests/test_examples_keys_async.py :start-after: [START create_key_client] @@ -37,38 +24,6 @@ class KeyClient: :caption: Creates a new instance of the Key client """ - @staticmethod - def create_config(**kwargs): - pass # TODO - - def __init__(self, vault_url, credentials, config=None, api_version=None, **kwargs): - if not credentials: - raise ValueError("credentials") - if not vault_url: - raise ValueError("vault_url") - self._vault_url = vault_url - if api_version is None: - api_version = KeyVaultClient.DEFAULT_API_VERSION - # TODO: need config to get default policies, config requires credentials but doesn't do anything with them - config = config or KeyVaultClient.get_configuration_class(api_version, aio=True)(credentials) - # TODO generated default pipeline should be fine when token policy isn't necessary - policies = [ - config.headers_policy, - config.user_agent_policy, - config.proxy_policy, - _BearerTokenCredentialPolicy(credentials), - config.redirect_policy, - config.retry_policy, - config.logging_policy, - ] - transport = AsyncioRequestsTransport(config) - pipeline = AsyncPipeline(transport, policies=policies) - self._client = KeyVaultClient(credentials, api_version=api_version, pipeline=pipeline, aio=True) - - @property - def vault_url(self) -> str: - return self._vault_url - async def get_key(self, name: str, version: Optional[str] = None, **kwargs: Mapping[str, Any]) -> Key: """Gets the public part of a stored key. @@ -146,7 +101,7 @@ async def create_key( :type tags: Dict[str, str] :returns: The created key :rtype: ~azure.security.keyvault.keys._models.Key - + Example: .. literalinclude:: ../tests/test_examples_keys_async.py :start-after: [START create_key] @@ -204,7 +159,7 @@ async def create_rsa_key( :type tags: Dict[str, str] :returns: The created key :rtype: ~azure.security.keyvault.keys._models.Key - + Example: .. literalinclude:: ../tests/test_examples_keys_async.py :start-after: [START create_rsa_key] @@ -356,7 +311,7 @@ async def update_key( def list_keys(self, **kwargs: Mapping[str, Any]) -> AsyncIterable[KeyBase]: """List keys in the specified vault. - + Retrieves a list of the keys in the Key Vault as JSON Web Key structures that contain the public part of a stored key. The LIST operation is applicable to all key types, however only the base key @@ -367,7 +322,7 @@ def list_keys(self, **kwargs: Mapping[str, Any]) -> AsyncIterable[KeyBase]: :returns: An iterator like instance of KeyBase :rtype: typing.AsyncIterable[~azure.security.keyvault.keys._models.KeyBase] - + Example: .. literalinclude:: ../tests/test_examples_keys_async.py :start-after: [START list_keys] @@ -383,7 +338,7 @@ def list_keys(self, **kwargs: Mapping[str, Any]) -> AsyncIterable[KeyBase]: def list_key_versions(self, name: str, **kwargs: Mapping[str, Any]) -> AsyncIterable[KeyBase]: """Retrieves a list of individual key versions with the same key name. - + The full key identifier, attributes, and tags are provided in the response. This operation requires the keys/list permission. @@ -392,7 +347,7 @@ def list_key_versions(self, name: str, **kwargs: Mapping[str, Any]) -> AsyncIter :returns: An iterator like instance of KeyBase :rtype: typing.AsyncIterable[~azure.security.keyvault.keys._models.KeyBase] - + Example: .. literalinclude:: ../tests/test_examples_keys_async.py :start-after: [START list_key_versions] @@ -409,7 +364,7 @@ def list_key_versions(self, name: str, **kwargs: Mapping[str, Any]) -> AsyncIter async def backup_key(self, name: str, **kwargs: Mapping[str, Any]) -> bytes: """Requests that a backup of the specified key be downloaded to the client. - + The Key Backup operation exports a key from Azure Key Vault in a protected form. Note that this operation does NOT return key material in a form that can be used outside the Azure Key Vault system, the @@ -424,7 +379,7 @@ async def backup_key(self, name: str, **kwargs: Mapping[str, Any]) -> bytes: another geographical area. For example, a backup from the US geographical area cannot be restored in an EU geographical area. This operation requires the key/backup permission. - + :param name: The name of the key. :type name :return: The raw bytes of the key backup. @@ -444,7 +399,7 @@ async def backup_key(self, name: str, **kwargs: Mapping[str, Any]) -> bytes: async def restore_key(self, backup: bytes, **kwargs: Mapping[str, Any]) -> Key: """Restores a backed up key to a vault. - + Imports a previously backed up key into Azure Key Vault, restoring the key, its key identifier, attributes and access control policies. The RESTORE operation may be used to import a previously backed up key. @@ -465,7 +420,7 @@ async def restore_key(self, backup: bytes, **kwargs: Mapping[str, Any]) -> Key: :returns: The restored key :rtype: ~azure.security.keyvault.keys._models.Key :raises: ~azure.core.exceptions.ResourceExistsError if the client failed to retrieve the key - + Example: .. literalinclude:: ../tests/test_examples_keys_async.py :start-after: [START restore_key] @@ -529,7 +484,7 @@ async def get_deleted_key(self, name: str, **kwargs: Mapping[str, Any]) -> Delet def list_deleted_keys(self, **kwargs: Mapping[str, Any]) -> AsyncIterable[DeletedKey]: """Lists the deleted keys in the specified vault. - + Retrieves a list of the keys in the Key Vault as JSON Web Key structures that contain the public part of a deleted key. This operation includes deletion-specific information. The Get Deleted Keys @@ -556,12 +511,12 @@ def list_deleted_keys(self, **kwargs: Mapping[str, Any]) -> AsyncIterable[Delete async def purge_deleted_key(self, name: str, **kwargs: Mapping[str, Any]) -> None: """Permanently deletes the specified key. - + The Purge Deleted Key operation is applicable for soft-delete enabled vaults. While the operation can be invoked on any vault, it will return an error if invoked on a non soft-delete enabled vault. This operation requires the keys/purge permission. - + :param name: The name of the key :type name :returns: None @@ -579,7 +534,7 @@ async def purge_deleted_key(self, name: str, **kwargs: Mapping[str, Any]) -> Non async def recover_deleted_key(self, name: str, **kwargs: Mapping[str, Any]) -> Key: """Recovers the deleted key to its latest version. - + The Recover Deleted Key operation is applicable for deleted keys in soft-delete enabled vaults. It recovers the deleted key back to its latest version under /keys. An attempt to recover an non-deleted key @@ -591,7 +546,7 @@ async def recover_deleted_key(self, name: str, **kwargs: Mapping[str, Any]) -> K :type name: str :returns: The recovered deleted key :rtype: ~azure.security.keyvault.keys._models.Key - + Example: .. literalinclude:: ../tests/test_examples_keys_async.py :start-after: [START recover_deleted_key] diff --git a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/secrets/_client.py b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/secrets/_client.py index c07e694f5fd1..1b80226317bb 100644 --- a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/secrets/_client.py +++ b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/secrets/_client.py @@ -3,34 +3,20 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for # license information. # -------------------------------------------------------------------------- +from datetime import datetime from typing import Any, AsyncIterable, Mapping, Optional, Dict -from azure.core.configuration import Configuration from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError -from azure.core.pipeline.policies import UserAgentPolicy, AsyncRetryPolicy, AsyncRedirectPolicy -from azure.core.pipeline.transport import AsyncioRequestsTransport -from azure.core.pipeline import AsyncPipeline - -from azure.security.keyvault._internal import _BearerTokenCredentialPolicy -from azure.security.keyvault._generated import KeyVaultClient -from azure.security.keyvault.aio._internal import AsyncPagingAdapter +from .._internal import _AsyncKeyVaultClientBase, AsyncPagingAdapter from ...secrets._models import Secret, DeletedSecret, SecretAttributes -from datetime import datetime # TODO: update all returns and raises -class SecretClient: +class SecretClient(_AsyncKeyVaultClientBase): """The SecretClient class defines a high level interface for managing secrets in the specified vault. - :param credentials: A credential or credential provider which can be used to authenticate to the vault, - a ValueError will be raised if the entity is not provided - :type credentials: azure.authentication.Credential or azure.authentication.CredentialProvider - :param str vault_url: The url of the vault to which the client will connect, - a ValueError will be raised if the entity is not provided - :param ~azure.core.configuration.Configuration config: The configuration for the SecretClient - Example: .. literalinclude:: ../tests/test_examples_keyvault.py :start-after: [START create_secret_client] @@ -40,44 +26,6 @@ class SecretClient: :caption: Creates a new instance of the Secret client """ - @staticmethod - def create_config(**kwargs): - pass # TODO - - def __init__(self, vault_url, credentials, config=None, api_version=None, **kwargs): - if not credentials: - raise ValueError("credentials") - - if not vault_url: - raise ValueError("vault_url") - - self._vault_url = vault_url - - if api_version is None: - api_version = KeyVaultClient.DEFAULT_API_VERSION - - # TODO: need config to get default policies, config requires credentials but doesn't do anything with them - config = config or KeyVaultClient.get_configuration_class(api_version, aio=True)(credentials) - - # TODO generated default pipeline should be fine when token policy isn't necessary - policies = [ - config.headers_policy, - config.user_agent_policy, - config.proxy_policy, - _BearerTokenCredentialPolicy(credentials), - config.redirect_policy, - config.retry_policy, - config.logging_policy, - ] - transport = AsyncioRequestsTransport(config) - pipeline = AsyncPipeline(transport, policies=policies) - - self._client = KeyVaultClient(credentials, api_version=api_version, pipeline=pipeline, aio=True) - - @property - def vault_url(self) -> str: - return self._vault_url - async def get_secret(self, name: str, version: str, **kwargs: Mapping[str, Any]) -> Secret: """Get a specified secret from the vault. @@ -152,10 +100,10 @@ async def set_secret( ) return Secret._from_secret_bundle(bundle) - async def update_secret_attributes( + async def update_secret( self, name: str, - version: str, + version: Optional[str] = None, content_type: Optional[str] = None, enabled: Optional[bool] = None, not_before: Optional[datetime] = None, @@ -186,8 +134,8 @@ async def update_secret_attributes( Example: .. literalinclude:: ../tests/test_examples_keyvault_async.py - :start-after: [START update_secret_attributes] - :end-before: [END update_secret_attributes] + :start-after: [START update_secret] + :end-before: [END update_secret] :language: python :dedent: 4 :caption: Updates the attributes associated with a specified secret in the key vault @@ -199,7 +147,7 @@ async def update_secret_attributes( bundle = await self._client.update_secret( self.vault_url, name, - secret_version=version, + secret_version=version or "", content_type=content_type, tags=tags, secret_attributes=attributes, diff --git a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/vault_client.py b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/vault_client.py index cc66d7fcb575..577681616a8f 100644 --- a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/vault_client.py +++ b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/aio/vault_client.py @@ -3,16 +3,37 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for # license information. # -------------------------------------------------------------------------- +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + try: + from azure.core.credentials import TokenCredential + except ImportError: + # TokenCredential is a typing_extensions.Protocol; we don't depend on that package + pass + +from azure.core import Configuration +from azure.core.pipeline.transport import HttpTransport + +from ._internal import _AsyncKeyVaultClientBase from .keys._client import KeyClient from .secrets._client import SecretClient -class VaultClient(object): - - def __init__(self, vault_url, credentials, config=None, **kwargs): - self.vault_url = vault_url - self._secrets = SecretClient(vault_url, credentials, config=config, **kwargs) - self._keys = KeyClient(vault_url, credentials, config=config, **kwargs) +class VaultClient(_AsyncKeyVaultClientBase): + def __init__( + self, + vault_url: str, + credential: "TokenCredential", + config: Configuration = None, + transport: HttpTransport = None, + api_version: str = None, + **kwargs: Any + ) -> None: + super(VaultClient, self).__init__( + vault_url, credential, config=config, transport=transport, api_version=api_version, **kwargs + ) + self._secrets = SecretClient(self.vault_url, credential, generated_client=self._client, **kwargs) + self._keys = KeyClient(self.vault_url, credential, generated_client=self._client, **kwargs) @property def secrets(self): @@ -33,4 +54,4 @@ def keys(self): # """ # :rtype: ~azure.keyvault.aio.certificates.CertificateClient` # """ - # pass \ No newline at end of file + # pass diff --git a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/keys/_client.py b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/keys/_client.py index c7f0bc47089e..801f618ad80a 100644 --- a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/keys/_client.py +++ b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/keys/_client.py @@ -6,27 +6,16 @@ from typing import Any, Dict, Generator, Mapping, Optional, List from datetime import datetime -from azure.core import Configuration from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError -from azure.core.pipeline import Pipeline -from azure.core.pipeline.transport import RequestsTransport -from azure.security.keyvault._internal import _BearerTokenCredentialPolicy -from .._generated import KeyVaultClient +from .._internal import _KeyVaultClientBase from ._models import Key, KeyBase, DeletedKey, KeyOperationResult -class KeyClient: +class KeyClient(_KeyVaultClientBase): """KeyClient defines a high level interface for managing keys in the specified vault. - :param credentials: A credential or credential provider which can be used to authenticate to the vault, - a ValueError will be raised if the entity is not provided - :type credentials: azure.authentication.Credential or azure.authentication.CredentialProvider - :param str vault_url: The url of the vault to which the client will connect, - a ValueError will be raised if the entity is not provided - :param ~azure.core.configuration.Configuration config: The configuration for the KeyClient - Example: .. literalinclude:: ../tests/test_examples_keyvault.py :start-after: [START create_key_client] @@ -38,46 +27,6 @@ class KeyClient: # pylint:disable=protected-access - @staticmethod - def create_config(**kwargs): - pass # TODO - - def __init__(self, vault_url, credentials, config=None, api_version=None, **kwargs): - # type: (str, Any, Configuration, Optional[str], Mapping[str, Any]) -> None - # TODO: update type hint for credentials - if not credentials: - raise ValueError("credentials") - - if not vault_url: - raise ValueError("vault_url") - - self._vault_url = vault_url - - if api_version is None: - api_version = KeyVaultClient.DEFAULT_API_VERSION - - config = config or KeyVaultClient.get_configuration_class(api_version)(credentials) - - # TODO generated default pipeline should be fine when token policy isn't necessary - policies = [ - config.headers_policy, - config.user_agent_policy, - config.proxy_policy, - _BearerTokenCredentialPolicy(credentials), - config.redirect_policy, - config.retry_policy, - config.logging_policy, - ] - transport = RequestsTransport(config) - pipeline = Pipeline(transport, policies=policies) - - self._client = KeyVaultClient(credentials, api_version=api_version, pipeline=pipeline) - - @property - def vault_url(self): - # type: () -> str - return self._vault_url - def create_key( self, name, diff --git a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/secrets/_client.py b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/secrets/_client.py index 421eb4e1c232..7deed936cf51 100644 --- a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/secrets/_client.py +++ b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/secrets/_client.py @@ -6,28 +6,16 @@ from typing import Any, Dict, Generator, Mapping, Optional from datetime import datetime -from azure.core import Configuration from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError -from azure.core.pipeline import Pipeline -from azure.core.pipeline.policies import RetryPolicy, RedirectPolicy, UserAgentPolicy -from azure.core.pipeline.transport import RequestsTransport -from azure.security.keyvault._internal import _BearerTokenCredentialPolicy -from .._generated import KeyVaultClient +from .._internal import _KeyVaultClientBase from ._models import Secret, DeletedSecret, SecretAttributes -class SecretClient: +class SecretClient(_KeyVaultClientBase): """SecretClient defines a high level interface for managing secrets in the specified vault. - :param credentials: A credential or credential provider which can be used to authenticate to the vault, - a ValueError will be raised if the entity is not provided - :type credentials: azure.authentication.Credential or azure.authentication.CredentialProvider - :param str vault_url: The url of the vault to which the client will connect, - a ValueError will be raised if the entity is not provided - :param ~azure.core.configuration.Configuration config: The configuration for the SecretClient - Example: .. literalinclude:: ../tests/test_examples_keyvault.py :start-after: [START create_secret_client] @@ -41,44 +29,6 @@ class SecretClient: _api_version = "7.0" - @staticmethod - def create_config(**kwargs): - pass # TODO - - def __init__(self, vault_url, credentials, config=None, api_version=None, **kwargs): - if not credentials: - raise ValueError("credentials") - - if not vault_url: - raise ValueError("vault_url") - - self._vault_url = vault_url - - if api_version is None: - api_version = KeyVaultClient.DEFAULT_API_VERSION - - config = config or KeyVaultClient.get_configuration_class(api_version)(credentials) - - # TODO generated default pipeline should be fine when token policy isn't necessary - policies = [ - config.headers_policy, - config.user_agent_policy, - config.proxy_policy, - _BearerTokenCredentialPolicy(credentials), - config.redirect_policy, - config.retry_policy, - config.logging_policy, - ] - transport = RequestsTransport(config) - pipeline = Pipeline(transport, policies=policies) - - self._client = KeyVaultClient(credentials, api_version=self._api_version, pipeline=pipeline) - - @property - def vault_url(self): - # type: () -> str - return self._vault_url - def get_secret(self, name, version=None, **kwargs): # type: (str, str, Mapping[str, Any]) -> Secret """Get a specified secret from the vault. @@ -100,9 +50,8 @@ def get_secret(self, name, version=None, **kwargs): :language: python :dedent: 4 :caption: Get secret from the key vault - """ - bundle = self._client.get_secret(self._vault_url, name, version, error_map={404: ResourceNotFoundError}) + bundle = self._client.get_secret(self._vault_url, name, version or "", error_map={404: ResourceNotFoundError}) return Secret._from_secret_bundle(bundle) def set_secret( @@ -148,10 +97,10 @@ def set_secret( ) return Secret._from_secret_bundle(bundle) - def update_secret_attributes( - self, name, version, content_type=None, enabled=None, not_before=None, expires=None, tags=None, **kwargs + def update_secret( + self, name, version=None, content_type=None, enabled=None, not_before=None, expires=None, tags=None, **kwargs ): - # type: (str, str, Optional[str], Optional[bool], Optional[datetime], Optional[datetime], Optional[Dict[str, str]], Mapping[str, Any]) -> SecretAttributes + # type: (str, Optional[str], Optional[str], Optional[bool], Optional[datetime], Optional[datetime], Optional[Dict[str, str]], Mapping[str, Any]) -> SecretAttributes """Updates the attributes associated with a specified secret in the key vault. The UPDATE operation changes specified attributes of an existing stored secret. @@ -175,8 +124,8 @@ def update_secret_attributes( Example: .. literalinclude:: ../tests/test_examples_keyvault.py - :start-after: [START update_secret_attributes] - :end-before: [END update_secret_attributes] + :start-after: [START update_secret] + :end-before: [END update_secret] :language: python :dedent: 4 :caption: Updates the attributes associated with a specified secret in the key vault @@ -189,7 +138,7 @@ def update_secret_attributes( bundle = self._client.update_secret( self.vault_url, name, - secret_version=version, + secret_version=version or "", content_type=content_type, tags=tags, secret_attributes=attributes, diff --git a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/vault_client.py b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/vault_client.py index 40f6be9430c2..0de695b2c809 100644 --- a/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/vault_client.py +++ b/sdk/keyvault/azure-security-keyvault/azure/security/keyvault/vault_client.py @@ -3,15 +3,31 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for # license information. # -------------------------------------------------------------------------- +try: + from typing import TYPE_CHECKING +except ImportError: + TYPE_CHECKING = False + +if TYPE_CHECKING: + # pylint:disable=unused-import + from azure.core import Configuration + from azure.core.credentials import TokenCredential + from azure.core.pipeline.transport import HttpTransport + from typing import Any, Mapping, Optional + +from ._internal import _KeyVaultClientBase from .keys._client import KeyClient from .secrets._client import SecretClient -class VaultClient(object): - def __init__(self, vault_url, credentials, config=None, **kwargs): - self._vault_url = vault_url.strip(" /") - self._secrets = SecretClient(self._vault_url, credentials, config=config, **kwargs) - self._keys = KeyClient(self._vault_url, credentials, config=config, **kwargs) +class VaultClient(_KeyVaultClientBase): + def __init__(self, vault_url, credential, config=None, transport=None, api_version=None, **kwargs): + # type: (str, TokenCredential, Configuration, Optional[HttpTransport], Optional[str], **Any) -> None + super(VaultClient, self).__init__( + vault_url, credential, config=config, transport=transport, api_version=api_version, **kwargs + ) + self._secrets = SecretClient(self.vault_url, credential, generated_client=self._client, **kwargs) + self._keys = KeyClient(self.vault_url, credential, generated_client=self._client, **kwargs) @property def secrets(self): @@ -27,13 +43,9 @@ def keys(self): """ return self._keys - @property - def certificates(self): - """ - :rtype: ~azure.keyvault.certificates.CertificateClient - """ - pass - - @property - def vault_url(self): - return self._vault_url + # @property + # def certificates(self): + # """ + # :rtype: ~azure.keyvault.certificates.CertificateClient + # """ + # pass diff --git a/sdk/keyvault/azure-security-keyvault/dev_requirements.txt b/sdk/keyvault/azure-security-keyvault/dev_requirements.txt index 24991df94887..fa269d4c6a63 100644 --- a/sdk/keyvault/azure-security-keyvault/dev_requirements.txt +++ b/sdk/keyvault/azure-security-keyvault/dev_requirements.txt @@ -1,2 +1,3 @@ -e ../../core/azure-core +-e ../../identity/azure-identity aiohttp>=3.0; python_version >= '3.5' diff --git a/sdk/keyvault/azure-security-keyvault/tests/async_preparer.py b/sdk/keyvault/azure-security-keyvault/tests/async_preparer.py new file mode 100644 index 000000000000..d5f14431a8aa --- /dev/null +++ b/sdk/keyvault/azure-security-keyvault/tests/async_preparer.py @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +import asyncio +from unittest.mock import Mock + +from azure.identity.aio import AsyncEnvironmentCredential +from azure.security.keyvault.aio import VaultClient + +from preparer import VaultClientPreparer + + +class AsyncVaultClientPreparer(VaultClientPreparer): + def create_vault_client(self, vault_uri): + if self.is_live: + credential = AsyncEnvironmentCredential() + else: + credential = Mock(get_token=asyncio.coroutine(lambda _: "fake-token")) + return VaultClient(vault_uri, credential) diff --git a/sdk/keyvault/azure-security-keyvault/tests/async_test_case.py b/sdk/keyvault/azure-security-keyvault/tests/async_test_case.py new file mode 100644 index 000000000000..38bd1387a629 --- /dev/null +++ b/sdk/keyvault/azure-security-keyvault/tests/async_test_case.py @@ -0,0 +1,44 @@ +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import asyncio +import functools + +from azure.core.exceptions import ResourceNotFoundError + +from test_case import KeyVaultTestCase + + +class AsyncKeyVaultTestCase(KeyVaultTestCase): + @staticmethod + def await_prepared_test(test_fn): + """Synchronous wrapper for async test methods. Used to avoid making changes + upstream to AbstractPreparer (which doesn't await the functions it wraps) + """ + + @functools.wraps(test_fn) + def run(test_class_instance, *args, **kwargs): + vault_client = kwargs.get("vault_client") + loop = asyncio.get_event_loop() + return loop.run_until_complete(test_fn(test_class_instance, vault_client)) + + return run + + async def _poll_until_resource_found(self, fn, secret_names, max_retries=20, retry_delay=6): + """polling helper for live tests because some operations take an unpredictable amount of time to complete""" + + if not self.is_live: + return + + for i in range(max_retries): + await asyncio.sleep(retry_delay) + try: + for name in secret_names: + # TODO: this enables polling get_secret but it'd be better if caller applied args to fn + await fn(name, version="") + break + except ResourceNotFoundError: + if i == max_retries - 1: + raise diff --git a/sdk/keyvault/azure-security-keyvault/tests/auth_request_filter.py b/sdk/keyvault/azure-security-keyvault/tests/auth_request_filter.py new file mode 100644 index 000000000000..a1e021a6e8ae --- /dev/null +++ b/sdk/keyvault/azure-security-keyvault/tests/auth_request_filter.py @@ -0,0 +1,16 @@ +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from azure_devtools.scenario_tests import RecordingProcessor + + +class OAuthv2RequestResponsesFilter(RecordingProcessor): + """Remove oauth authentication requests and responses from recording.""" + + def process_request(self, request): + import re + if not re.match('https://login.microsoftonline.com/([^/]+)/oauth2(?:/v2.0)?/token', request.uri): + return request + return None diff --git a/sdk/keyvault/azure-security-keyvault/tests/preparer.py b/sdk/keyvault/azure-security-keyvault/tests/preparer.py index 4f8ef74fb53d..08c38d9f3a33 100644 --- a/sdk/keyvault/azure-security-keyvault/tests/preparer.py +++ b/sdk/keyvault/azure-security-keyvault/tests/preparer.py @@ -2,14 +2,15 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See LICENSE.txt in the project root for # license information. -# -------------------------------------------------------------------------- -from collections import namedtuple -import io -import os -import requests -import time +# ------------------------------------------------------------------------- +try: + from unittest.mock import Mock +except ImportError: # python < 3.3 + from mock import Mock +from azure.identity import EnvironmentCredential from azure.security.keyvault import VaultClient + from azure.mgmt.keyvault import KeyVaultManagementClient from azure.mgmt.keyvault.models import ( SecretPermissions, @@ -22,16 +23,12 @@ AccessPolicyEntry, VaultProperties, VaultCreateOrUpdateParameters, - Vault, ) -from azure_devtools.scenario_tests.preparers import AbstractPreparer, SingleValueReplacer from azure_devtools.scenario_tests.exceptions import AzureTestError -from devtools_testutils import AzureMgmtPreparer, ResourceGroupPreparer, FakeResource +from devtools_testutils import AzureMgmtPreparer, ResourceGroupPreparer from devtools_testutils.resource_testcase import RESOURCE_GROUP_PARAM -FakeAccount = namedtuple("FakeResource", ["name", "account_endpoint"]) - DEFAULT_PERMISSIONS = Permissions( keys=[perm.value for perm in KeyPermissions], secrets=[perm.value for perm in SecretPermissions], @@ -117,11 +114,17 @@ def create_resource(self, name, **kwargs): # playback => we need only the uri used in the recording vault_uri = "https://{}.vault.azure.net/".format(name) - vault_credentials = self.test_class_instance.settings.get_credentials(resource="https://vault.azure.net") - client = VaultClient(vault_uri, vault_credentials) + client = self.create_vault_client(vault_uri) return {self.parameter_name: client} + def create_vault_client(self, vault_uri): + if self.is_live: + credential = EnvironmentCredential() + else: + credential = Mock(get_token=lambda _: "fake-token") + return VaultClient(vault_uri, credential) + def remove_resource(self, name, **kwargs): if self.is_live: group = self._get_resource_group(**kwargs).name diff --git a/sdk/keyvault/azure-security-keyvault/tests/test_case.py b/sdk/keyvault/azure-security-keyvault/tests/test_case.py index f64165ccfbb1..a085eb7578f2 100644 --- a/sdk/keyvault/azure-security-keyvault/tests/test_case.py +++ b/sdk/keyvault/azure-security-keyvault/tests/test_case.py @@ -3,11 +3,17 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for # license information. # -------------------------------------------------------------------------- -from azure_devtools.scenario_tests import GeneralNameReplacer from devtools_testutils import AzureMgmtTestCase +from auth_request_filter import OAuthv2RequestResponsesFilter + class KeyVaultTestCase(AzureMgmtTestCase): + def __init__(self, *args, **kwargs): + super(KeyVaultTestCase, self).__init__(*args, **kwargs) + # workaround for upstream issue https://github.com/Azure/azure-python-devtools/pull/57 + self.recording_processors.append(OAuthv2RequestResponsesFilter()) + def setUp(self): self.list_test_size = 7 super(KeyVaultTestCase, self).setUp() diff --git a/sdk/keyvault/azure-security-keyvault/tests/test_examples_keys_async.py b/sdk/keyvault/azure-security-keyvault/tests/test_examples_keys_async.py index 9d5ce41b34c2..f3e9c3061a6b 100644 --- a/sdk/keyvault/azure-security-keyvault/tests/test_examples_keys_async.py +++ b/sdk/keyvault/azure-security-keyvault/tests/test_examples_keys_async.py @@ -9,28 +9,11 @@ from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError, HttpResponseError from devtools_testutils import ResourceGroupPreparer -from preparer import VaultClientPreparer -from test_case import KeyVaultTestCase +from async_preparer import AsyncVaultClientPreparer +from async_test_case import AsyncKeyVaultTestCase from azure.security.keyvault.aio import VaultClient -def await_prepared_test(test_fn): - """Synchronous wrapper for async test methods. Used to avoid making changes - upstream to AbstractPreparer (which doesn't await the functions it wraps) - """ - - @functools.wraps(test_fn) - def run(test_class_instance, *args, **kwargs): - # TODO: this is a workaround for VaultClientPreparer creating a sync client; let's obviate it - vault_client = kwargs.get("vault_client") - credentials = test_class_instance.settings.get_credentials(resource="https://vault.azure.net") - aio_client = VaultClient(vault_client.vault_url, credentials) - loop = asyncio.get_event_loop() - return loop.run_until_complete(test_fn(test_class_instance, vault_client=aio_client)) - - return run - - def create_vault_client(): client_id = "" client_secret = "" @@ -38,15 +21,13 @@ def create_vault_client(): vault_url = "" # [START create_vault_client] + from azure.identity import AsyncClientSecretCredential from azure.security.keyvault.aio import VaultClient - from azure.common.credentials import ServicePrincipalCredentials - credentials = ServicePrincipalCredentials( - client_id=client_id, secret=client_secret, tenant=tenant_id, resource="https://vault.azure.net" - ) + credential = AsyncClientSecretCredential(client_id=client_id, secret=client_secret, tenant_id=tenant_id) - # Create a new Vault client using Azure credentials - vault_client = VaultClient(vault_url=vault_url, credentials=credentials) + # Create a new Vault client using a client secret credential + vault_client = VaultClient(vault_url=vault_url, credential=credential) # [END create_vault_client] return vault_client @@ -58,23 +39,21 @@ def create_key_client(): vault_url = "" # [START create_key_client] - from azure.common.credentials import ServicePrincipalCredentials + from azure.identity import AsyncClientSecretCredential from azure.security.keyvault.aio import KeyClient - credentials = ServicePrincipalCredentials( - client_id=client_id, secret=client_secret, tenant=tenant_id, resource="https://vault.azure.net" - ) + credential = AsyncClientSecretCredential(client_id=client_id, secret=client_secret, tenant_id=tenant_id) - # Create a new key client using Azure credentials - key_client = KeyClient(vault_url=vault_url, credentials=credentials) + # Create a new key client using a client secret credential + key_client = KeyClient(vault_url=vault_url, credential=credential) # [END create_key_client] return key_client -class TestExamplesKeyVault(KeyVaultTestCase): +class TestExamplesKeyVault(AsyncKeyVaultTestCase): @ResourceGroupPreparer() - @VaultClientPreparer() - @await_prepared_test + @AsyncVaultClientPreparer() + @AsyncKeyVaultTestCase.await_prepared_test async def test_example_key_crud_operations(self, vault_client, **kwargs): from dateutil import parser as date_parse @@ -186,8 +165,8 @@ async def test_example_key_crud_operations(self, vault_client, **kwargs): pass @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_example_key_list_operations(self, vault_client, **kwargs): key_client = vault_client.keys try: @@ -234,8 +213,8 @@ async def test_example_key_list_operations(self, vault_client, **kwargs): pass @ResourceGroupPreparer() - @VaultClientPreparer() - @await_prepared_test + @AsyncVaultClientPreparer() + @AsyncKeyVaultTestCase.await_prepared_test async def test_example_keys_backup_restore(self, vault_client, **kwargs): key_client = vault_client.keys created_key = await key_client.create_key("keyrec", "RSA") @@ -266,8 +245,8 @@ async def test_example_keys_backup_restore(self, vault_client, **kwargs): pass @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_example_keys_recover_purge(self, vault_client, **kwargs): key_client = vault_client.keys created_key = await key_client.create_key("key-name", "RSA") diff --git a/sdk/keyvault/azure-security-keyvault/tests/test_examples_keyvault.py b/sdk/keyvault/azure-security-keyvault/tests/test_examples_keyvault.py index 6ed449c024b8..a17c7a670bae 100644 --- a/sdk/keyvault/azure-security-keyvault/tests/test_examples_keyvault.py +++ b/sdk/keyvault/azure-security-keyvault/tests/test_examples_keyvault.py @@ -26,7 +26,7 @@ def create_vault_client(): ) # Create a new Vault client using Azure credentials - vault_client = VaultClient(vault_url=vault_url, credentials=credentials) + vault_client = VaultClient(vault_url=vault_url, credential=credentials) # [END create_vault_client] return vault_client @@ -46,7 +46,7 @@ def create_secret_client(): ) # Create a new Secret client using Azure credentials - secret_client = SecretClient(vault_url=vault_url, credentials=credentials) + secret_client = SecretClient(vault_url=vault_url, credential=credentials) # [END create_secret_client] return secret_client @@ -95,14 +95,14 @@ def test_example_secret_crud_operations(self, vault_client, **kwargs): pass try: - # [START update_secret_attributes] + # [START update_secret] # update attributes of an existing secret content_type = "text/plain" tags = {"foo": "updated tag"} secret_version = secret.version - updated_secret = secret_client.update_secret_attributes( + updated_secret = secret_client.update_secret( "secret-name", secret_version, content_type=content_type, tags=tags ) @@ -111,7 +111,7 @@ def test_example_secret_crud_operations(self, vault_client, **kwargs): print(updated_secret.content_type) print(updated_secret.tags) - # [END update_secret_attributes] + # [END update_secret] except HttpResponseError: pass diff --git a/sdk/keyvault/azure-security-keyvault/tests/test_examples_keyvault_async.py b/sdk/keyvault/azure-security-keyvault/tests/test_examples_keyvault_async.py index 320451c13ef9..9f8f27614c17 100644 --- a/sdk/keyvault/azure-security-keyvault/tests/test_examples_keyvault_async.py +++ b/sdk/keyvault/azure-security-keyvault/tests/test_examples_keyvault_async.py @@ -9,34 +9,16 @@ import functools from devtools_testutils import ResourceGroupPreparer -from preparer import VaultClientPreparer -from test_case import KeyVaultTestCase +from async_preparer import AsyncVaultClientPreparer +from async_test_case import AsyncKeyVaultTestCase from azure.security.keyvault._generated.v7_0.models import KeyVaultErrorException from azure.security.keyvault.aio.vault_client import VaultClient -# TODO: Remove, later? -def await_prepared_test(test_fn): - """Synchronous wrapper for async test methods. Used to avoid making changes - upstream to AbstractPreparer (which doesn't await the functions it wraps) - """ - @functools.wraps(test_fn) - def run(test_class_instance, *args, **kwargs): - # TODO: this is a workaround for VaultClientPreparer creating a sync client; let's obviate it - vault_client = kwargs.get("vault_client") - credentials = test_class_instance.settings.get_credentials( - resource="https://vault.azure.net") - aio_client = VaultClient(vault_client.vault_url, credentials) - loop = asyncio.get_event_loop() - return loop.run_until_complete(test_fn(test_class_instance, vault_client=aio_client)) - return run - - -class TestExamplesKeyVault(KeyVaultTestCase): - +class TestExamplesKeyVault(AsyncKeyVaultTestCase): @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_example_secret_crud_operations(self, vault_client, **kwargs): secret_client = vault_client.secrets try: @@ -77,14 +59,14 @@ async def test_example_secret_crud_operations(self, vault_client, **kwargs): pass try: - # [START update_secret_attributes] + # [START update_secret] # update attributes of an existing secret content_type = 'text/plain' tags = {'foo': 'updated tag'} secret_version = secret.version - updated_secret = await secret_client.update_secret_attributes( + updated_secret = await secret_client.update_secret( 'secret-name', secret_version, content_type=content_type, tags=tags) @@ -94,7 +76,7 @@ async def test_example_secret_crud_operations(self, vault_client, **kwargs): print(updated_secret.content_type) print(updated_secret.tags) - # [END update_secret_attributes] + # [END update_secret] except KeyVaultErrorException: pass @@ -112,8 +94,8 @@ async def test_example_secret_crud_operations(self, vault_client, **kwargs): pass @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_example_secret_list_operations(self, vault_client, **kwargs): secret_client = vault_client.secrets try: @@ -163,8 +145,8 @@ async def test_example_secret_list_operations(self, vault_client, **kwargs): pass @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_example_secrets_backup_restore(self, vault_client, **kwargs): secret_client = vault_client.secrets created_secret = await secret_client.set_secret('secret-name', 'secret-value') @@ -213,8 +195,8 @@ async def test_example_secrets_backup_restore(self, vault_client, **kwargs): pass @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_example_secrets_recover_purge(self, vault_client, **kwargs): secret_client = vault_client.secrets created_secret = await secret_client.set_secret('secret-name', 'secret-value') diff --git a/sdk/keyvault/azure-security-keyvault/tests/test_keys_async.py b/sdk/keyvault/azure-security-keyvault/tests/test_keys_async.py index 366db9c2e999..ed084be69d28 100644 --- a/sdk/keyvault/azure-security-keyvault/tests/test_keys_async.py +++ b/sdk/keyvault/azure-security-keyvault/tests/test_keys_async.py @@ -9,8 +9,8 @@ from azure.core.exceptions import ResourceNotFoundError from devtools_testutils import ResourceGroupPreparer -from preparer import VaultClientPreparer -from test_case import KeyVaultTestCase +from async_preparer import AsyncVaultClientPreparer +from async_test_case import AsyncKeyVaultTestCase from azure.security.keyvault.aio import VaultClient from azure.security.keyvault._generated.v7_0.models import JsonWebKey @@ -18,41 +18,7 @@ from dateutil import parser as date_parse -def await_prepared_test(test_fn): - """Synchronous wrapper for async test methods. Used to avoid making changes - upstream to AbstractPreparer (which doesn't await the functions it wraps) - """ - - @functools.wraps(test_fn) - def run(test_class_instance, *args, **kwargs): - # TODO: this is a workaround for VaultClientPreparer creating a sync client - vault_client = kwargs.get("vault_client") - credentials = test_class_instance.settings.get_credentials(resource="https://vault.azure.net") - aio_client = VaultClient(vault_client.vault_url, credentials) - loop = asyncio.get_event_loop() - return loop.run_until_complete(test_fn(test_class_instance, vault_client=aio_client)) - - return run - - -class KeyVaultKeyTest(KeyVaultTestCase): - async def _poll_until_resource_found(self, fn, key_names, max_retries=20, retry_delay=6): - """polling helper for live tests because some operations take an unpredictable amount of time to complete""" - - if not self.is_live: - return - - for i in range(max_retries): - await asyncio.sleep(retry_delay) - try: - for name in key_names: - # TODO: this enables polling get_key but it'd be better if caller applied args to fn - await fn(name, version="") - break - except ResourceNotFoundError: - if i == max_retries - 1: - raise - +class KeyVaultKeyTest(AsyncKeyVaultTestCase): def _assert_key_attributes_equal(self, k1, k2): self.assertEqual(k1.name, k2.name) self.assertEqual(k1.vault_url, k2.vault_url) @@ -161,8 +127,8 @@ def _to_bytes(hex): return imported_key @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_key_crud_operations(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.keys @@ -211,8 +177,8 @@ async def test_key_crud_operations(self, vault_client, **kwargs): self.assertEqual(created_rsa_key.id, deleted_key.id) @ResourceGroupPreparer() - @VaultClientPreparer() - @await_prepared_test + @AsyncVaultClientPreparer() + @AsyncKeyVaultTestCase.await_prepared_test async def test_key_list(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.keys @@ -231,8 +197,8 @@ async def test_key_list(self, vault_client, **kwargs): await self._validate_key_list(result, expected) @ResourceGroupPreparer() - @VaultClientPreparer() - @await_prepared_test + @AsyncVaultClientPreparer() + @AsyncKeyVaultTestCase.await_prepared_test async def test_list_versions(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.keys @@ -257,8 +223,8 @@ async def test_list_versions(self, vault_client, **kwargs): self.assertEqual(0, len(expected)) @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_list_deleted_keys(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.keys @@ -281,8 +247,8 @@ async def test_list_deleted_keys(self, vault_client, **kwargs): await self._validate_key_list(result, expected) @ResourceGroupPreparer() - @VaultClientPreparer() - @await_prepared_test + @AsyncVaultClientPreparer() + @AsyncKeyVaultTestCase.await_prepared_test async def test_backup_restore(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.keys @@ -306,8 +272,8 @@ async def test_backup_restore(self, vault_client, **kwargs): self._assert_key_attributes_equal(created_bundle, restored) @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_recover_purge(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) @@ -351,8 +317,8 @@ async def test_recover_purge(self, vault_client, **kwargs): self.assertEqual(len(set(expected.keys()) & set(actual.keys())), len(expected)) @ResourceGroupPreparer() - @VaultClientPreparer() - @await_prepared_test + @AsyncVaultClientPreparer() + @AsyncKeyVaultTestCase.await_prepared_test async def test_key_wrap_and_unwrap(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.keys diff --git a/sdk/keyvault/azure-security-keyvault/tests/test_secrets_async.py b/sdk/keyvault/azure-security-keyvault/tests/test_secrets_async.py index f1bf2d2070b5..4b36fd340742 100644 --- a/sdk/keyvault/azure-security-keyvault/tests/test_secrets_async.py +++ b/sdk/keyvault/azure-security-keyvault/tests/test_secrets_async.py @@ -8,8 +8,8 @@ from azure.core.exceptions import ResourceNotFoundError from devtools_testutils import ResourceGroupPreparer -from preparer import VaultClientPreparer -from test_case import KeyVaultTestCase +from async_preparer import AsyncVaultClientPreparer +from async_test_case import AsyncKeyVaultTestCase from azure.security.keyvault.aio.vault_client import VaultClient @@ -17,41 +17,7 @@ import time -def await_prepared_test(test_fn): - """Synchronous wrapper for async test methods. Used to avoid making changes - upstream to AbstractPreparer (which doesn't await the functions it wraps) - """ - - @functools.wraps(test_fn) - def run(test_class_instance, *args, **kwargs): - # TODO: this is a workaround for VaultClientPreparer creating a sync client - vault_client = kwargs.get("vault_client") - credentials = test_class_instance.settings.get_credentials(resource="https://vault.azure.net") - aio_client = VaultClient(vault_client.vault_url, credentials) - loop = asyncio.get_event_loop() - return loop.run_until_complete(test_fn(test_class_instance, vault_client=aio_client)) - - return run - - -class KeyVaultSecretTest(KeyVaultTestCase): - async def _poll_until_resource_found(self, fn, secret_names, max_retries=20, retry_delay=6): - """polling helper for live tests because some operations take an unpredictable amount of time to complete""" - - if not self.is_live: - return - - for i in range(max_retries): - await asyncio.sleep(retry_delay) - try: - for name in secret_names: - # TODO: this enables polling get_secret but it'd be better if caller applied args to fn - await fn(name, version="") - break - except ResourceNotFoundError: - if i == max_retries - 1: - raise - +class KeyVaultSecretTest(AsyncKeyVaultTestCase): def _assert_secret_attributes_equal(self, s1, s2): # self.assertEqual(s1.id , s2.id) self.assertEqual(s1.content_type, s2.content_type) @@ -84,8 +50,8 @@ async def _validate_secret_list(self, secrets, expected): self.assertEqual(len(expected), 0) @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_secret_crud_operations(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.secrets @@ -120,7 +86,7 @@ async def _update_secret(secret): content_type = "text/plain" expires = date_parse.parse("2050-02-02T08:00:00.000Z") tags = {"foo": "updated tag"} - secret_bundle = await client.update_secret_attributes( + secret_bundle = await client.update_secret( secret.name, secret.version, content_type=content_type, expires=expires, tags=tags ) self.assertEqual(tags, secret_bundle.tags) @@ -146,8 +112,8 @@ async def _update_secret(secret): await client.get_secret(updated.name, "") @ResourceGroupPreparer() - @VaultClientPreparer() - @await_prepared_test + @AsyncVaultClientPreparer() + @AsyncKeyVaultTestCase.await_prepared_test async def test_secret_list(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.secrets @@ -169,8 +135,8 @@ async def test_secret_list(self, vault_client, **kwargs): await self._validate_secret_list(result, expected) @ResourceGroupPreparer() - @VaultClientPreparer() - @await_prepared_test + @AsyncVaultClientPreparer() + @AsyncKeyVaultTestCase.await_prepared_test async def test_list_versions(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.secrets @@ -199,8 +165,8 @@ async def test_list_versions(self, vault_client, **kwargs): self.assertEqual(len(expected), 0) @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_list_deleted_secrets(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.secrets @@ -223,8 +189,8 @@ async def test_list_deleted_secrets(self, vault_client, **kwargs): await self._validate_secret_list(result, expected) @ResourceGroupPreparer() - @VaultClientPreparer() - @await_prepared_test + @AsyncVaultClientPreparer() + @AsyncKeyVaultTestCase.await_prepared_test async def test_backup_restore(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.secrets @@ -247,8 +213,8 @@ async def test_backup_restore(self, vault_client, **kwargs): self._assert_secret_attributes_equal(created_bundle, restored) @ResourceGroupPreparer() - @VaultClientPreparer(enable_soft_delete=True) - @await_prepared_test + @AsyncVaultClientPreparer(enable_soft_delete=True) + @AsyncKeyVaultTestCase.await_prepared_test async def test_recover_purge(self, vault_client, **kwargs): self.assertIsNotNone(vault_client) client = vault_client.secrets diff --git a/sdk/keyvault/azure-security-keyvault/tests/test_secrets_client.py b/sdk/keyvault/azure-security-keyvault/tests/test_secrets_client.py index 3aa8f842f9d9..95887a7b9a40 100644 --- a/sdk/keyvault/azure-security-keyvault/tests/test_secrets_client.py +++ b/sdk/keyvault/azure-security-keyvault/tests/test_secrets_client.py @@ -87,7 +87,7 @@ def _update_secret(secret): content_type = "text/plain" expires = date_parse.parse("2050-01-02T08:00:00.000Z") tags = {"foo": "updated tag"} - secret_bundle = client.update_secret_attributes( + secret_bundle = client.update_secret( secret.name, secret.version, content_type=content_type, expires=expires, tags=tags ) self.assertEqual(tags, secret_bundle.tags)