From 0bd426e69e9bbbd25b94a8f01f9e9ef519f84838 Mon Sep 17 00:00:00 2001 From: azure-sdk Date: Mon, 18 May 2026 18:05:27 +0000 Subject: [PATCH 1/4] Configurations: 'specification/keyvault/data-plane/Certificates/tspconfig.yaml', API Version: 2026-03-01-preview, SDK Release Type: beta, and CommitSHA: '44729a46b902922d8f50a383573f9c6641ce8e74' in SpecRepo: 'https://github.com/Azure/azure-rest-api-specs' Pipeline run: https://dev.azure.com/azure-sdk/internal/_build/results?buildId=6315044 Refer to https://eng.ms/docs/products/azure-developer-experience/develop/sdk-release/sdk-release-prerequisites to prepare for SDK release. --- .../azure-keyvault-certificates/CHANGELOG.md | 4 + .../azure-keyvault-certificates/MANIFEST.in | 2 +- .../_metadata.json | 8 +- .../apiview-properties.json | 4 +- .../azure/__init__.py | 4 - .../azure/keyvault/__init__.py | 4 - .../azure/keyvault/certificates/__init__.py | 57 +- .../azure/keyvault/certificates/_client.py | 1079 --------- .../azure/keyvault/certificates/_enums.py | 77 - .../certificates/_generated/_client.py | 14 +- .../certificates/_generated/_configuration.py | 9 +- .../certificates/_generated/_model_base.py | 1235 ---------- .../_generated/_operations/_operations.py | 74 +- .../_generated/_operations/_patch.py | 1 - .../certificates/_generated/_patch.py | 1 - .../certificates/_generated/_serialization.py | 2050 ----------------- .../_generated/_utils/model_base.py | 245 +- .../_generated/_utils/serialization.py | 6 +- .../certificates/_generated/_vendor.py | 25 - .../certificates/_generated/_version.py | 2 +- .../certificates/_generated/aio/_client.py | 14 +- .../_generated/aio/_configuration.py | 9 +- .../_generated/aio/_operations/_operations.py | 20 +- .../_generated/aio/_operations/_patch.py | 1 - .../certificates/_generated/aio/_patch.py | 1 - .../certificates/_generated/aio/_vendor.py | 25 - .../_generated/models/__init__.py | 2 + .../certificates/_generated/models/_enums.py | 8 +- .../certificates/_generated/models/_models.py | 47 + .../certificates/_generated/models/_patch.py | 1 - .../azure/keyvault/certificates/_models.py | 1560 ------------- .../azure/keyvault/certificates/_polling.py | 56 - .../keyvault/certificates/_sdk_moniker.py | 7 - .../keyvault/certificates/_shared/__init__.py | 78 - .../keyvault/certificates/_shared/_polling.py | 136 -- .../certificates/_shared/_polling_async.py | 80 - .../_shared/async_challenge_auth_policy.py | 262 --- .../certificates/_shared/async_client_base.py | 117 - .../_shared/challenge_auth_policy.py | 270 --- .../certificates/_shared/client_base.py | 162 -- .../certificates/_shared/http_challenge.py | 186 -- .../_shared/http_challenge_cache.py | 93 - .../azure/keyvault/certificates/_version.py | 6 - .../keyvault/certificates/aio/__init__.py | 8 - .../keyvault/certificates/aio/_client.py | 1077 --------- .../certificates/aio/_polling_async.py | 57 - .../azure/keyvault/certificates/py.typed | 1 - .../samples/backup_restore_operations.py | 1 + .../backup_restore_operations_async.py | 1 + .../samples/contacts.py | 1 + .../samples/contacts_async.py | 1 + .../samples/hello_world.py | 1 + .../samples/hello_world_async.py | 1 + .../samples/import_certificate.py | 1 + .../samples/import_certificate_async.py | 1 + .../samples/issuers.py | 1 + .../samples/issuers_async.py | 1 + .../samples/list_operations.py | 1 + .../samples/list_operations_async.py | 1 + .../samples/parse_certificate.py | 1 + .../samples/parse_certificate_async.py | 1 + .../samples/recover_purge_operations.py | 1 + .../samples/recover_purge_operations_async.py | 1 + .../sdk_packaging.toml | 2 - .../azure-keyvault-certificates/setup.py | 71 + .../tests/certs.py | 1 + .../tests/test_certificates_client.py | 6 +- .../tests/test_certificates_client_async.py | 2 +- .../tests/test_examples_certificates_async.py | 1 - .../tsp-location.yaml | 2 +- 70 files changed, 427 insertions(+), 8859 deletions(-) delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_client.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_enums.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_model_base.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_serialization.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_vendor.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_vendor.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_polling.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_sdk_moniker.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling_async.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_client_base.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/client_base.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge_cache.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_version.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/__init__.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_client.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_polling_async.py delete mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/py.typed delete mode 100644 sdk/keyvault/azure-keyvault-certificates/sdk_packaging.toml create mode 100644 sdk/keyvault/azure-keyvault-certificates/setup.py diff --git a/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md b/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md index 4c4842e0816c..b60a9a1cf7b8 100644 --- a/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## 4.12.0b1 (2026-05-18) + +skip changelog generation for data-plane package and please add changelog manually. + ## 4.11.2 (Unreleased) ### Features Added diff --git a/sdk/keyvault/azure-keyvault-certificates/MANIFEST.in b/sdk/keyvault/azure-keyvault-certificates/MANIFEST.in index 7193a4bc539c..a39bafcb9a1a 100644 --- a/sdk/keyvault/azure-keyvault-certificates/MANIFEST.in +++ b/sdk/keyvault/azure-keyvault-certificates/MANIFEST.in @@ -1,6 +1,6 @@ include *.md include LICENSE -include azure/keyvault/certificates/py.typed +include azure/keyvault/certificates/_generated/py.typed recursive-include tests *.py recursive-include samples *.py *.md include azure/__init__.py diff --git a/sdk/keyvault/azure-keyvault-certificates/_metadata.json b/sdk/keyvault/azure-keyvault-certificates/_metadata.json index 752a2b95cda5..3b33050f99a7 100644 --- a/sdk/keyvault/azure-keyvault-certificates/_metadata.json +++ b/sdk/keyvault/azure-keyvault-certificates/_metadata.json @@ -1,10 +1,10 @@ { - "apiVersion": "2025-07-01", + "apiVersion": "2026-03-01-preview", "apiVersions": { - "KeyVault": "2025-07-01" + "KeyVault": "2026-03-01-preview" }, - "commit": "80cf521033f9499c3c86305d11c7de6451e796e5", + "commit": "44729a46b902922d8f50a383573f9c6641ce8e74", "repository_url": "https://github.com/Azure/azure-rest-api-specs", "typespec_src": "specification/keyvault/data-plane/Certificates", - "emitterVersion": "0.61.2" + "emitterVersion": "0.62.1" } \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault-certificates/apiview-properties.json b/sdk/keyvault/azure-keyvault-certificates/apiview-properties.json index a914a0c1052b..9b4aece65b10 100644 --- a/sdk/keyvault/azure-keyvault-certificates/apiview-properties.json +++ b/sdk/keyvault/azure-keyvault-certificates/apiview-properties.json @@ -31,6 +31,7 @@ "azure.keyvault.certificates._generated.models.KeyVaultErrorError": "KeyVaultError.error.anonymous", "azure.keyvault.certificates._generated.models.LifetimeAction": "KeyVault.LifetimeAction", "azure.keyvault.certificates._generated.models.OrganizationDetails": "KeyVault.OrganizationDetails", + "azure.keyvault.certificates._generated.models.PlatformManaged": "KeyVault.PlatformManaged", "azure.keyvault.certificates._generated.models.SecretProperties": "KeyVault.SecretProperties", "azure.keyvault.certificates._generated.models.SubjectAlternativeNames": "KeyVault.SubjectAlternativeNames", "azure.keyvault.certificates._generated.models.Trigger": "KeyVault.Trigger", @@ -94,5 +95,6 @@ "azure.keyvault.certificates._generated.aio.KeyVaultClient.purge_deleted_certificate": "KeyVault.purgeDeletedCertificate", "azure.keyvault.certificates._generated.KeyVaultClient.recover_deleted_certificate": "KeyVault.recoverDeletedCertificate", "azure.keyvault.certificates._generated.aio.KeyVaultClient.recover_deleted_certificate": "KeyVault.recoverDeletedCertificate" - } + }, + "CrossLanguageVersion": "62e5a7e522bd" } \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/__init__.py index 679ab6995134..d55ccad1f573 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/__init__.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/__init__.py @@ -1,5 +1 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ __path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/__init__.py index 679ab6995134..d55ccad1f573 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/__init__.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/__init__.py @@ -1,5 +1 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ __path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py index 3e8da4143494..d55ccad1f573 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py @@ -1,56 +1 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from ._client import CertificateClient -from ._enums import( - CertificatePolicyAction, - KeyCurveName, - KeyType, - CertificateContentType, - KeyUsageType, - WellKnownIssuerNames -) -from ._models import( - AdministratorContact, - CertificateContact, - CertificateIssuer, - CertificateOperation, - CertificateOperationError, - CertificatePolicy, - CertificateProperties, - DeletedCertificate, - IssuerProperties, - LifetimeAction, - KeyVaultCertificate, - KeyVaultCertificateIdentifier -) -from ._shared.client_base import ApiVersion - -__all__ = [ - "ApiVersion", - "CertificatePolicyAction", - "AdministratorContact", - "CertificateClient", - "CertificateContact", - "CertificateIssuer", - "CertificateOperation", - "CertificateOperationError", - "CertificatePolicy", - "CertificateProperties", - "DeletedCertificate", - "IssuerProperties", - "KeyCurveName", - "KeyType", - "KeyVaultCertificate", - "KeyVaultCertificateIdentifier", - "KeyUsageType", - "LifetimeAction", - "CertificateContentType", - "WellKnownIssuerNames", - "CertificateIssuer", - "IssuerProperties" -] - -from ._version import VERSION -__version__ = VERSION +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_client.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_client.py deleted file mode 100644 index 6ce432f1874d..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_client.py +++ /dev/null @@ -1,1079 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -# pylint:disable=too-many-lines,too-many-public-methods,bad-option-value,delete-operation-wrong-return-type -import base64 -from functools import partial -from typing import Any, Dict, List, Optional, Union - -from azure.core.polling import LROPoller -from azure.core.paging import ItemPaged -from azure.core.tracing.decorator import distributed_trace - -from ._shared import KeyVaultClientBase -from ._shared._polling import DeleteRecoverPollingMethod, KeyVaultOperationPoller -from ._models import ( - AdministratorContact, - KeyVaultCertificate, - CertificateProperties, - CertificatePolicy, - DeletedCertificate, - CertificateIssuer, - IssuerProperties, - CertificateContact, - CertificateOperation, -) -from ._polling import CreateCertificatePoller - - -NO_SAN_OR_SUBJECT = "You need to set either subject or one of the subject alternative names parameters in the policy" - - -class CertificateClient(KeyVaultClientBase): - """A high-level interface for managing a vault's certificates. - - :param str vault_url: URL of the vault the client will access. This is also called the vault's "DNS Name". - You should validate that this URL references a valid Key Vault resource. See https://aka.ms/azsdk/blog/vault-uri - for details. - :param credential: An object which can provide an access token for the vault, such as a credential from - :mod:`azure.identity` - :type credential: ~azure.core.credentials.TokenCredential - - :keyword api_version: Version of the service API to use. Defaults to the most recent. - :paramtype api_version: ~azure.keyvault.certificates.ApiVersion or str - :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key - Vault domain. Defaults to True. - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START create_certificate_client] - :end-before: [END create_certificate_client] - :language: python - :caption: Create a new ``CertificateClient`` - :dedent: 4 - """ - - # pylint:disable=protected-access - - @distributed_trace - def begin_create_certificate( - self, - certificate_name: str, - policy: CertificatePolicy, - *, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - preserve_order: Optional[bool] = None, - **kwargs: Any, - ) -> LROPoller[Union[KeyVaultCertificate, CertificateOperation]]: - """Creates a new certificate. - - If this is the first version, the certificate resource is created. This operation requires the - certificates/create permission. Waiting on the returned poller requires the certificates/get permission and - gives you the certificate if creation is successful, or the CertificateOperation if not -- otherwise, it raises - an :class:`~azure.core.exceptions.HttpResponseError`. - - :param str certificate_name: The name of the certificate. - :param policy: The management policy for the certificate. Either subject or one of the subject alternative - name properties are required. - :type policy: ~azure.keyvault.certificates.CertificatePolicy - - :keyword bool enabled: Whether the certificate is enabled for use. - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] - :keyword bool preserve_order: Whether to preserve the order of the certificate chain. - - :returns: An LROPoller for the create certificate operation. Waiting on the poller gives you the certificate if - creation is successful, or the CertificateOperation if not. - :rtype: ~azure.core.polling.LROPoller[~azure.keyvault.certificates.KeyVaultCertificate or - ~azure.keyvault.certificates.CertificateOperation] - - :raises ValueError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate policy is invalid; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START create_certificate] - :end-before: [END create_certificate] - :language: python - :caption: Create a certificate - :dedent: 8 - """ - if not ( - policy.san_emails - or policy.san_user_principal_names - or policy.san_dns_names - or policy.san_ip_addresses - or policy.san_uris - or policy.subject - ): - raise ValueError(NO_SAN_OR_SUBJECT) - - polling_interval = kwargs.pop("_polling_interval", None) - if polling_interval is None: - polling_interval = 5 - - if enabled is not None: - attributes = self._models.CertificateAttributes(enabled=enabled) - else: - attributes = None - - parameters = self._models.CertificateCreateParameters( - certificate_policy=policy._to_certificate_policy_bundle(), - certificate_attributes=attributes, - tags=tags, - preserve_cert_order=preserve_order, - ) - - pipeline_response, cert_bundle = self._client.create_certificate( - certificate_name=certificate_name, - parameters=parameters, - cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), - **kwargs, - ) - - create_certificate_operation = CertificateOperation._from_certificate_operation_bundle(cert_bundle) - - command = partial(self.get_certificate_operation, certificate_name=certificate_name, **kwargs) - - get_certificate_command = partial(self.get_certificate, certificate_name=certificate_name, **kwargs) - - create_certificate_polling = CreateCertificatePoller( - pipeline_response=pipeline_response, - get_certificate_command=get_certificate_command, - interval=polling_interval - ) - - def no_op(*_, **__) -> Any: # The deserialization callback is ignored based on polling implementation - pass - - return LROPoller(command, create_certificate_operation, no_op, create_certificate_polling) - - @distributed_trace - def get_certificate(self, certificate_name: str, **kwargs: Any) -> KeyVaultCertificate: - """Gets a certificate with its management policy attached. Requires certificates/get permission. - - Does not accept the version of the certificate as a parameter. To get a specific version of the - certificate, call :func:`get_certificate_version`. - - :param str certificate_name: The name of the certificate in the given vault. - - :returns: An instance of KeyVaultCertificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START get_certificate] - :end-before: [END get_certificate] - :language: python - :caption: Get a certificate - :dedent: 8 - """ - bundle = self._client.get_certificate( - certificate_name=certificate_name, certificate_version="", **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace - def get_certificate_version(self, certificate_name: str, version: str, **kwargs: Any) -> KeyVaultCertificate: - """Gets a specific version of a certificate without returning its management policy. - - Requires certificates/get permission. To get the latest version of the certificate, or to get the certificate's - policy as well, call :func:`get_certificate`. - - :param str certificate_name: The name of the certificate in the given vault. - :param str version: The version of the certificate. - - :returns: An instance of KeyVaultCertificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START get_certificate_version] - :end-before: [END get_certificate_version] - :language: python - :caption: Get a certificate with a specific version - :dedent: 8 - """ - bundle = self._client.get_certificate( - certificate_name=certificate_name, certificate_version=version, **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace - def begin_delete_certificate(self, certificate_name: str, **kwargs: Any) -> LROPoller[DeletedCertificate]: - """Delete all versions of a certificate. Requires certificates/delete permission. - - When this method returns Key Vault has begun deleting the certificate. Deletion may take several seconds in a - vault with soft-delete enabled. This method therefore returns a poller enabling you to wait for deletion to - complete. - - :param str certificate_name: The name of the certificate to delete. - - :returns: A poller for the delete certificate operation. The poller's `result` method returns the - :class:`~azure.keyvault.certificates.DeletedCertificate` without waiting for deletion to complete. If the - vault has soft-delete enabled and you want to immediately, permanently delete the certificate with - :func:`purge_deleted_certificate`, call the poller's `wait` method first. It will block until the deletion - is complete. The `wait` method requires certificates/get permission. - :rtype: ~azure.core.polling.LROPoller[~azure.keyvault.certificates.DeletedCertificate] - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START delete_certificate] - :end-before: [END delete_certificate] - :language: python - :caption: Delete a certificate - :dedent: 8 - """ - polling_interval = kwargs.pop("_polling_interval", None) - if polling_interval is None: - polling_interval = 2 - pipeline_response, deleted_cert_bundle = self._client.delete_certificate( - certificate_name=certificate_name, - cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), - **kwargs, - ) - deleted_cert = DeletedCertificate._from_deleted_certificate_bundle(deleted_cert_bundle) - - polling_method = DeleteRecoverPollingMethod( - # no recovery ID means soft-delete is disabled, in which case we initialize the poller as finished - finished=deleted_cert.recovery_id is None, - pipeline_response=pipeline_response, - command=partial(self.get_deleted_certificate, certificate_name=certificate_name, **kwargs), - final_resource=deleted_cert, - interval=polling_interval, - ) - - return KeyVaultOperationPoller(polling_method) - - @distributed_trace - def get_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> DeletedCertificate: - """Get a deleted certificate. Possible only in a vault with soft-delete enabled. - - Requires certificates/get permission. Retrieves the deleted certificate information plus its attributes, such as - retention interval, scheduled permanent deletion, and the current deletion recovery level. - - :param str certificate_name: The name of the certificate. - - :return: The deleted certificate - :rtype: ~azure.keyvault.certificates.DeletedCertificate - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START get_deleted_certificate] - :end-before: [END get_deleted_certificate] - :language: python - :caption: Get a deleted certificate - :dedent: 8 - """ - bundle = self._client.get_deleted_certificate( - certificate_name=certificate_name, **kwargs - ) - return DeletedCertificate._from_deleted_certificate_bundle(deleted_certificate_bundle=bundle) - - @distributed_trace - def purge_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> None: - """Permanently deletes a deleted certificate. Possible only in vaults with soft-delete enabled. - - Requires certificates/purge permission. Performs an irreversible deletion of the specified certificate, without - possibility for recovery. The operation is not available if the - :py:attr:`~azure.keyvault.certificates.CertificateProperties.recovery_level` does not specify 'Purgeable'. - This method is only necessary for purging a certificate before its - :py:attr:`~azure.keyvault.certificates.DeletedCertificate.scheduled_purge_date`. - - :param str certificate_name: The name of the certificate - - :return: None - :rtype: None - - :raises ~azure.core.exceptions.HttpResponseError: - """ - self._client.purge_deleted_certificate( - certificate_name=certificate_name, **kwargs - ) - - @distributed_trace - def begin_recover_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> LROPoller[KeyVaultCertificate]: - """Recover a deleted certificate to its latest version. Possible only in a vault with soft-delete enabled. - - Requires certificates/recover permission. When this method returns Key Vault has begun recovering the - certificate. Recovery may take several seconds. This method therefore returns a poller enabling you to wait for - recovery to complete. Waiting is only necessary when you want to use the recovered certificate in another - operation immediately. - - :param str certificate_name: The name of the deleted certificate to recover - - :returns: A poller for the recovery operation. The poller's `result` method returns the recovered - :class:`~azure.keyvault.certificates.KeyVaultCertificate` without waiting for recovery to complete. If you - want to use the recovered certificate immediately, call the poller's `wait` method, which blocks until the - certificate is ready to use. The `wait` method requires certificate/get permission. - :rtype: ~azure.core.polling.LROPoller[~azure.keyvault.certificates.KeyVaultCertificate] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START recover_deleted_certificate] - :end-before: [END recover_deleted_certificate] - :language: python - :caption: Recover a deleted certificate - :dedent: 8 - """ - polling_interval = kwargs.pop("_polling_interval", None) - if polling_interval is None: - polling_interval = 2 - - pipeline_response, recovered_cert_bundle = self._client.recover_deleted_certificate( - certificate_name=certificate_name, - cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), - **kwargs, - ) - recovered_certificate = KeyVaultCertificate._from_certificate_bundle(recovered_cert_bundle) - command = partial(self.get_certificate, certificate_name=certificate_name, **kwargs) - polling_method = DeleteRecoverPollingMethod( - finished=False, - pipeline_response=pipeline_response, - command=command, - final_resource=recovered_certificate, - interval=polling_interval - ) - - return KeyVaultOperationPoller(polling_method) - - @distributed_trace - def import_certificate( - self, - certificate_name: str, - certificate_bytes: bytes, - *, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - password: Optional[str] = None, - policy: Optional[CertificatePolicy] = None, - preserve_order: Optional[bool] = None, - **kwargs: Any, - ) -> KeyVaultCertificate: - """Import a certificate created externally. Requires certificates/import permission. - - Imports an existing valid certificate, containing a private key, into Azure Key Vault. The certificate to be - imported can be in either PFX or PEM format. If the certificate is in PEM format the PEM file must contain the - key as well as x509 certificates, and you must provide a ``policy`` with - :attr:`~azure.keyvault.certificates.CertificatePolicy.content_type` of - :attr:`~azure.keyvault.certificates.CertificateContentType.pem`. - - :param str certificate_name: The name of the certificate. - :param bytes certificate_bytes: Bytes of the certificate object to import. This certificate - needs to contain the private key. - - :keyword bool enabled: Whether the certificate is enabled for use. - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] - :keyword str password: If the private key in the passed in certificate is encrypted, it - is the password used for encryption. - :keyword policy: The management policy for the certificate. Required if importing a PEM-format certificate, - with :attr:`~azure.keyvault.certificates.CertificatePolicy.content_type` set to - :attr:`~azure.keyvault.certificates.CertificateContentType.pem`. - :paramtype policy: ~azure.keyvault.certificates.CertificatePolicy - :keyword bool preserve_order: Whether to preserve the order of the certificate chain. - - :returns: The imported KeyVaultCertificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.HttpResponseError: - """ - - if enabled is not None: - attributes = self._models.CertificateAttributes(enabled=enabled) - else: - attributes = None - base64_encoded_certificate = base64.b64encode(certificate_bytes).decode("utf-8") - - parameters = self._models.CertificateImportParameters( - base64_encoded_certificate=base64_encoded_certificate, - password=password, - certificate_policy=policy._to_certificate_policy_bundle() if policy else None, - certificate_attributes=attributes, - tags=tags, - preserve_cert_order=preserve_order, - ) - - bundle = self._client.import_certificate( - certificate_name=certificate_name, parameters=parameters, **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace - def get_certificate_policy(self, certificate_name: str, **kwargs: Any) -> CertificatePolicy: - """Gets the policy for a certificate. Requires certificates/get permission. - - Returns the specified certificate policy resources in the key vault. - - :param str certificate_name: The name of the certificate in a given key vault. - - :return: The certificate policy - :rtype: ~azure.keyvault.certificates.CertificatePolicy - - :raises ~azure.core.exceptions.HttpResponseError: - """ - bundle = self._client.get_certificate_policy( - certificate_name=certificate_name, **kwargs - ) - return CertificatePolicy._from_certificate_policy_bundle(certificate_policy_bundle=bundle) - - @distributed_trace - def update_certificate_policy( - self, certificate_name: str, policy: CertificatePolicy, **kwargs: Any - ) -> CertificatePolicy: - """Updates the policy for a certificate. Requires certificates/update permission. - - Set specified members in the certificate policy. Leaves others as null. - - :param str certificate_name: The name of the certificate in the given vault. - :param policy: The policy for the certificate. - :type policy: ~azure.keyvault.certificates.CertificatePolicy - - :return: The certificate policy - :rtype: ~azure.keyvault.certificates.CertificatePolicy - - :raises ~azure.core.exceptions.HttpResponseError: - """ - bundle = self._client.update_certificate_policy( - certificate_name=certificate_name, - certificate_policy=policy._to_certificate_policy_bundle(), - **kwargs - ) - return CertificatePolicy._from_certificate_policy_bundle(certificate_policy_bundle=bundle) - - @distributed_trace - def update_certificate_properties( - self, - certificate_name: str, - version: Optional[str] = None, - *, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - **kwargs: Any, - ) -> KeyVaultCertificate: - """Change a certificate's properties. Requires certificates/update permission. - - :param str certificate_name: The name of the certificate in the given key vault. - :param str version: The version of the certificate. - - :keyword bool enabled: Whether the certificate is enabled for use. - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] - - :returns: The updated KeyVaultCertificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START update_certificate] - :end-before: [END update_certificate] - :language: python - :caption: Update a certificate's attributes - :dedent: 8 - """ - - if enabled is not None: - attributes = self._models.CertificateAttributes(enabled=enabled) - else: - attributes = None - - parameters = self._models.CertificateUpdateParameters( - certificate_attributes=attributes, tags=tags - ) - - bundle = self._client.update_certificate( - certificate_name=certificate_name, - certificate_version=version or "", - parameters=parameters, - **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace - def backup_certificate(self, certificate_name: str, **kwargs: Any) -> bytes: - """Back up a certificate in a protected form useable only by Azure Key Vault. - - Requires certificates/backup permission. This is intended to allow copying a certificate from one vault to - another. Both vaults must be owned by the same Azure subscription. Also, backup / restore cannot be performed - across geopolitical boundaries. For example, a backup from a vault in a USA region cannot be restored to a vault - in an EU region. - - :param str certificate_name: The name of the certificate. - - :return: The backup blob containing the backed up certificate. - :rtype: bytes - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START backup_certificate] - :end-before: [END backup_certificate] - :language: python - :caption: Get a certificate backup - :dedent: 8 - """ - backup_result = self._client.backup_certificate( - certificate_name=certificate_name, **kwargs - ) - return backup_result.value - - @distributed_trace - def restore_certificate_backup(self, backup: bytes, **kwargs: Any) -> KeyVaultCertificate: - """Restore a certificate backup to the vault. Requires certificates/restore permission. - - This restores all versions of the certificate, with its name, attributes, and access control policies. If the - certificate's name is already in use, restoring it will fail. Also, the target vault must be owned by the same - Microsoft Azure subscription as the source vault. - - :param bytes backup: The backup blob associated with a certificate bundle. - - :return: The restored KeyVaultCertificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START restore_certificate] - :end-before: [END restore_certificate] - :language: python - :caption: Restore a certificate backup - :dedent: 8 - """ - bundle = self._client.restore_certificate( - parameters=self._models.CertificateRestoreParameters(certificate_bundle_backup=backup), - **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace - def list_deleted_certificates( - self, *, include_pending: Optional[bool] = None, **kwargs: Any - ) -> ItemPaged[DeletedCertificate]: - """Lists the currently-recoverable deleted certificates. Possible only if vault is soft-delete enabled. - - Requires certificates/get/list permission. Retrieves the certificates in the current vault which are in a - deleted state and ready for recovery or purging. This operation includes deletion-specific information. - - :keyword bool include_pending: Specifies whether to include certificates which are not completely deleted. - Only available for API versions v7.0 and up. If not provided, Key Vault treats this as False. - :paramtype include_pending: bool or None - - :return: An iterator-like instance of DeletedCertificate - :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.DeletedCertificate] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START list_deleted_certificates] - :end-before: [END list_deleted_certificates] - :language: python - :caption: List all the deleted certificates - :dedent: 8 - """ - max_page_size = kwargs.pop("max_page_size", None) - - if self.api_version == "2016-10-01": - if include_pending is not None: - raise NotImplementedError( - "The 'include_pending' parameter to `list_deleted_certificates` " - "is only available for API versions v7.0 and up" - ) - else: - kwargs.update({"include_pending": include_pending}) - - return self._client.get_deleted_certificates( - maxresults=max_page_size, - cls=lambda objs: [ - DeletedCertificate._from_deleted_certificate_item(deleted_certificate_item=x) for x in objs - ], - **kwargs - ) - - @distributed_trace - def list_properties_of_certificates( - self, *, include_pending: Optional[bool] = None, **kwargs: Any - ) -> ItemPaged[CertificateProperties]: - """List identifiers and properties of all certificates in the vault. - - Requires certificates/list permission. - - :keyword include_pending: Specifies whether to include certificates which are not completely provisioned. - Only available for API versions v7.0 and up. If not provided, Key Vault treats this as False. - :paramtype include_pending: bool or None - - :returns: An iterator-like instance of CertificateProperties - :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateProperties] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START list_properties_of_certificates] - :end-before: [END list_properties_of_certificates] - :language: python - :caption: List all certificates - :dedent: 8 - """ - max_page_size = kwargs.pop("max_page_size", None) - - if self.api_version == "2016-10-01": - if include_pending is not None: - raise NotImplementedError( - "The 'include_pending' parameter to `list_properties_of_certificates` " - "is only available for API versions v7.0 and up" - ) - else: - kwargs.update({"include_pending": include_pending}) - - return self._client.get_certificates( - maxresults=max_page_size, - cls=lambda objs: [CertificateProperties._from_certificate_item(certificate_item=x) for x in objs], - **kwargs - ) - - @distributed_trace - def list_properties_of_certificate_versions( - self, certificate_name: str, **kwargs: Any - ) -> ItemPaged[CertificateProperties]: - """List the identifiers and properties of a certificate's versions. - - Requires certificates/list permission. - - :param str certificate_name: The name of the certificate. - - :returns: An iterator-like instance of CertificateProperties - :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateProperties] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START list_properties_of_certificate_versions] - :end-before: [END list_properties_of_certificate_versions] - :language: python - :caption: List all versions of a certificate - :dedent: 8 - """ - max_page_size = kwargs.pop("max_page_size", None) - return self._client.get_certificate_versions( - certificate_name=certificate_name, - maxresults=max_page_size, - cls=lambda objs: [CertificateProperties._from_certificate_item(certificate_item=x) for x in objs], - **kwargs - ) - - @distributed_trace - def set_contacts(self, contacts: "List[CertificateContact]", **kwargs: Any) -> "List[CertificateContact]": - """Sets the certificate contacts for the key vault. Requires certificates/managecontacts permission. - - :param contacts: The contact list for the vault certificates. - :type contacts: list[~azure.keyvault.certificates.CertificateContact] - - :returns: The created list of contacts - :rtype: list[~azure.keyvault.certificates.CertificateContact] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START set_contacts] - :end-before: [END set_contacts] - :language: python - :caption: Create contacts - :dedent: 8 - """ - new_contacts = self._client.set_certificate_contacts( - contacts=self._models.Contacts(contact_list=[c._to_certificate_contacts_item() for c in contacts]), - **kwargs - ) - return [ - CertificateContact._from_certificate_contacts_item(contact_item=item) for item in new_contacts.contact_list - ] - - @distributed_trace - def get_contacts(self, **kwargs: Any) -> "List[CertificateContact]": - """Gets the certificate contacts for the key vault. Requires the certificates/managecontacts permission. - - :return: The certificate contacts for the key vault. - :rtype: list[~azure.keyvault.certificates.CertificateContact] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START get_contacts] - :end-before: [END get_contacts] - :language: python - :caption: Get contacts - :dedent: 8 - """ - contacts = self._client.get_certificate_contacts(**kwargs) - return [CertificateContact._from_certificate_contacts_item(contact_item=item) for item in contacts.contact_list] - - @distributed_trace - def delete_contacts(self, **kwargs: Any) -> "List[CertificateContact]": - """Deletes the certificate contacts for the key vault. Requires the certificates/managecontacts permission. - - :return: The deleted contacts for the key vault. - :rtype: list[~azure.keyvault.certificates.CertificateContact] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START delete_contacts] - :end-before: [END delete_contacts] - :language: python - :caption: Delete contacts - :dedent: 8 - """ - contacts = self._client.delete_certificate_contacts(**kwargs) - return [CertificateContact._from_certificate_contacts_item(contact_item=item) for item in contacts.contact_list] - - @distributed_trace - def get_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: - """Gets the creation operation of a certificate. Requires the certificates/get permission. - - :param str certificate_name: The name of the certificate. - - :returns: The created CertificateOperation - :rtype: ~azure.keyvault.certificates.CertificateOperation - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - """ - - bundle = self._client.get_certificate_operation( - certificate_name=certificate_name, **kwargs - ) - return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) - - @distributed_trace - def delete_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: - """Deletes and stops the creation operation for a specific certificate. - - Requires the certificates/update permission. - - :param str certificate_name: The name of the certificate. - - :return: The deleted CertificateOperation - :rtype: ~azure.keyvault.certificates.CertificateOperation - - :raises ~azure.core.exceptions.HttpResponseError: - """ - bundle = self._client.delete_certificate_operation( - certificate_name=certificate_name, **kwargs - ) - return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) - - @distributed_trace - def cancel_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: - """Cancels an in-progress certificate operation. Requires the certificates/update permission. - - :param str certificate_name: The name of the certificate. - - :returns: The cancelled certificate operation - :rtype: ~azure.keyvault.certificates.CertificateOperation - - :raises ~azure.core.exceptions.HttpResponseError: - """ - bundle = self._client.update_certificate_operation( - certificate_name=certificate_name, - certificate_operation=self._models.CertificateOperationUpdateParameter(cancellation_requested=True), - **kwargs - ) - return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) - - @distributed_trace - def merge_certificate( - self, - certificate_name: str, - x509_certificates: List[bytes], - *, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - **kwargs: Any, - ) -> KeyVaultCertificate: - """Merges a certificate or a certificate chain with a key pair existing on the server. - - Requires the certificates/create permission. Performs the merging of a certificate or certificate chain with a - key pair currently available in the service. Make sure when creating the certificate to merge using - :func:`begin_create_certificate` that you set its issuer to 'Unknown'. This way Key Vault knows that the - certificate will not be signed by an issuer known to it. - - :param str certificate_name: The name of the certificate - :param x509_certificates: The certificate or the certificate chain to merge. - :type x509_certificates: list[bytes] - - :keyword enabled: Whether the certificate is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - - :return: The merged certificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.HttpResponseError: - """ - - if enabled is not None: - attributes = self._models.CertificateAttributes(enabled=enabled) - else: - attributes = None - - parameters = self._models.CertificateMergeParameters( - x509_certificates=x509_certificates, certificate_attributes=attributes, tags=tags - ) - - bundle = self._client.merge_certificate( - certificate_name=certificate_name, parameters=parameters, **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace - def get_issuer(self, issuer_name: str, **kwargs: Any) -> CertificateIssuer: - """Gets the specified certificate issuer. Requires certificates/manageissuers/getissuers permission. - - :param str issuer_name: The name of the issuer. - - :return: The specified certificate issuer. - :rtype: ~azure.keyvault.certificates.CertificateIssuer - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the issuer doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START get_issuer] - :end-before: [END get_issuer] - :language: python - :caption: Get an issuer - :dedent: 8 - """ - issuer_bundle = self._client.get_certificate_issuer( - issuer_name=issuer_name, **kwargs - ) - return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) - - @distributed_trace - def create_issuer( - self, - issuer_name: str, - provider: str, - *, - enabled: Optional[bool] = None, - account_id: Optional[str] = None, - password: Optional[str] = None, - organization_id: Optional[str] = None, - admin_contacts: Optional[List[AdministratorContact]] = None, - **kwargs: Any, - ) -> CertificateIssuer: - """Sets the specified certificate issuer. Requires certificates/setissuers permission. - - :param str issuer_name: The name of the issuer. - :param str provider: The issuer provider. - - :keyword bool enabled: Whether the issuer is enabled for use. - :keyword str account_id: The user name/account name/account id. - :keyword str password: The password/secret/account key. - :keyword str organization_id: Id of the organization - :keyword admin_contacts: Contact details of the organization administrators of the - certificate issuer. - :paramtype admin_contacts: list[~azure.keyvault.certificates.AdministratorContact] - - :returns: The created CertificateIssuer - :rtype: ~azure.keyvault.certificates.CertificateIssuer - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START create_issuer] - :end-before: [END create_issuer] - :language: python - :caption: Create an issuer - :dedent: 8 - """ - - if account_id or password: - issuer_credentials = self._models.IssuerCredentials(account_id=account_id, password=password) - else: - issuer_credentials = None - if admin_contacts: - admin_details: Optional[List[Any]] = [ - self._models.AdministratorDetails( - first_name=contact.first_name, - last_name=contact.last_name, - email_address=contact.email, - phone=contact.phone, - ) - for contact in admin_contacts - ] - else: - admin_details = None - if organization_id or admin_details: - organization_details = self._models.OrganizationDetails(id=organization_id, admin_details=admin_details) - else: - organization_details = None - if enabled is not None: - issuer_attributes = self._models.IssuerAttributes(enabled=enabled) - else: - issuer_attributes = None - - parameters = self._models.CertificateIssuerSetParameters( - provider=provider, - credentials=issuer_credentials, - organization_details=organization_details, - attributes=issuer_attributes, - ) - - issuer_bundle = self._client.set_certificate_issuer( - issuer_name=issuer_name, parameter=parameters, **kwargs - ) - return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) - - @distributed_trace - def update_issuer( - self, - issuer_name: str, - *, - enabled: Optional[bool] = None, - provider: Optional[str] = None, - account_id: Optional[str] = None, - password: Optional[str] = None, - organization_id: Optional[str] = None, - admin_contacts: Optional[List[AdministratorContact]] = None, - **kwargs: Any, - ) -> CertificateIssuer: - """Updates the specified certificate issuer. Requires certificates/setissuers permission. - - :param str issuer_name: The name of the issuer. - - :keyword bool enabled: Whether the issuer is enabled for use. - :keyword str provider: The issuer provider - :keyword str account_id: The user name/account name/account id. - :keyword str password: The password/secret/account key. - :keyword str organization_id: Id of the organization - :keyword admin_contacts: Contact details of the organization administrators of the certificate issuer - :paramtype admin_contacts: list[~azure.keyvault.certificates.AdministratorContact] - - :return: The updated issuer - :rtype: ~azure.keyvault.certificates.CertificateIssuer - - :raises ~azure.core.exceptions.HttpResponseError: - """ - - if account_id or password: - issuer_credentials = self._models.IssuerCredentials(account_id=account_id, password=password) - else: - issuer_credentials = None - if admin_contacts: - admin_details: Optional[List[Any]] = [ - self._models.AdministratorDetails( - first_name=contact.first_name, - last_name=contact.last_name, - email_address=contact.email, - phone=contact.phone, - ) - for contact in admin_contacts - ] - else: - admin_details = None - if organization_id or admin_details: - organization_details = self._models.OrganizationDetails(id=organization_id, admin_details=admin_details) - else: - organization_details = None - if enabled is not None: - issuer_attributes = self._models.IssuerAttributes(enabled=enabled) - else: - issuer_attributes = None - - parameters = self._models.CertificateIssuerUpdateParameters( - provider=provider, - credentials=issuer_credentials, - organization_details=organization_details, - attributes=issuer_attributes, - ) - - issuer_bundle = self._client.update_certificate_issuer( - issuer_name=issuer_name, parameter=parameters, **kwargs - ) - return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) - - @distributed_trace - def delete_issuer(self, issuer_name: str, **kwargs: Any) -> CertificateIssuer: - """Deletes the specified certificate issuer. - - Requires certificates/manageissuers/deleteissuers permission. - - :param str issuer_name: The name of the issuer. - - :return: CertificateIssuer - :rtype: ~azure.keyvault.certificates.CertificateIssuer - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START delete_issuer] - :end-before: [END delete_issuer] - :language: python - :caption: Delete an issuer - :dedent: 8 - """ - issuer_bundle = self._client.delete_certificate_issuer( - issuer_name=issuer_name, **kwargs - ) - return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) - - @distributed_trace - def list_properties_of_issuers(self, **kwargs: Any) -> ItemPaged[IssuerProperties]: - """Lists properties of the certificate issuers for the key vault. - - Requires the certificates/manageissuers/getissuers permission. - - :return: An iterator-like instance of Issuers - :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateIssuer] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates.py - :start-after: [START list_properties_of_issuers] - :end-before: [END list_properties_of_issuers] - :language: python - :caption: List issuers of a vault - :dedent: 8 - """ - max_page_size = kwargs.pop("max_page_size", None) - return self._client.get_certificate_issuers( - maxresults=max_page_size, - cls=lambda objs: [IssuerProperties._from_issuer_item(issuer_item=x) for x in objs], - **kwargs - ) - - def __enter__(self) -> "CertificateClient": - self._client.__enter__() - return self diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_enums.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_enums.py deleted file mode 100644 index 6913db4755d7..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_enums.py +++ /dev/null @@ -1,77 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -# pylint: disable=enum-must-be-uppercase - -from enum import Enum - -from azure.core import CaseInsensitiveEnumMeta - - -class CertificatePolicyAction(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """The supported action types for the lifetime of a certificate""" - - email_contacts = "EmailContacts" - auto_renew = "AutoRenew" - - -class CertificateContentType(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Content type of the secrets as specified in Certificate Policy""" - - pkcs12 = "application/x-pkcs12" - pem = "application/x-pem-file" - - -class KeyUsageType(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """The supported types of key usages""" - - digital_signature = "digitalSignature" - non_repudiation = "nonRepudiation" - key_encipherment = "keyEncipherment" - data_encipherment = "dataEncipherment" - key_agreement = "keyAgreement" - key_cert_sign = "keyCertSign" - crl_sign = "cRLSign" - encipher_only = "encipherOnly" - decipher_only = "decipherOnly" - - -class KeyType(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Supported key types""" - - ec = "EC" #: Elliptic Curve - ec_hsm = "EC-HSM" #: Elliptic Curve with a private key which is not exportable from the HSM - rsa = "RSA" #: RSA (https://tools.ietf.org/html/rfc3447) - rsa_hsm = "RSA-HSM" #: RSA with a private key which is not exportable from the HSM - oct = "oct" #: Octet sequence (used to represent symmetric keys) - oct_hsm = "oct-HSM" #: Octet sequence with a private key which is not exportable from the HSM - - @classmethod - def _missing_(cls, value): - for member in cls: - if member.value.lower() == value.lower(): - return member - raise ValueError(f"{value} is not a valid KeyType") - - -class KeyCurveName(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Supported elliptic curves""" - - p_256 = "P-256" #: The NIST P-256 elliptic curve, AKA SECG curve SECP256R1. - p_384 = "P-384" #: The NIST P-384 elliptic curve, AKA SECG curve SECP384R1. - p_521 = "P-521" #: The NIST P-521 elliptic curve, AKA SECG curve SECP521R1. - p_256_k = "P-256K" #: The SECG SECP256K1 elliptic curve. - - -class WellKnownIssuerNames(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Collection of well-known issuer names""" - - self = "Self" #: Use this issuer for a self-signed certificate - unknown = "Unknown" - """ - If you use this issuer, you must manually get an x509 certificate from the issuer of your choice. - You must then call :func:`~azure.keyvault.certificates.CertificateClient.merge_certificate` to - merge the public x509 certificate with your key vault certificate pending object to complete creation. - """ diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_client.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_client.py index 0ab087a35122..67aabf6451b6 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_client.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_client.py @@ -7,8 +7,8 @@ # -------------------------------------------------------------------------- from copy import deepcopy +import sys from typing import Any, TYPE_CHECKING -from typing_extensions import Self from azure.core import PipelineClient from azure.core.pipeline import policies @@ -18,6 +18,11 @@ from ._operations import _KeyVaultClientOperationsMixin from ._utils.serialization import Deserializer, Serializer +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self # type: ignore + if TYPE_CHECKING: from azure.core.credentials import TokenCredential @@ -30,9 +35,10 @@ class KeyVaultClient(_KeyVaultClientOperationsMixin): :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials.TokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". - Default value is "2025-07-01". Note that overriding this default value may result in - unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_configuration.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_configuration.py index 625a7b4a35b2..c84853bf0e6a 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_configuration.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_configuration.py @@ -26,14 +26,15 @@ class KeyVaultClientConfiguration: # pylint: disable=too-many-instance-attribut :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials.TokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". - Default value is "2025-07-01". Note that overriding this default value may result in - unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ def __init__(self, vault_base_url: str, credential: "TokenCredential", **kwargs: Any) -> None: - api_version: str = kwargs.pop("api_version", "2025-07-01") + api_version: str = kwargs.pop("api_version", "2026-03-01-preview") if vault_base_url is None: raise ValueError("Parameter 'vault_base_url' must not be None.") diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_model_base.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_model_base.py deleted file mode 100644 index 3072ee252ed9..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_model_base.py +++ /dev/null @@ -1,1235 +0,0 @@ -# pylint: disable=too-many-lines -# coding=utf-8 -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -# pylint: disable=protected-access, broad-except - -import copy -import calendar -import decimal -import functools -import sys -import logging -import base64 -import re -import typing -import enum -import email.utils -from datetime import datetime, date, time, timedelta, timezone -from json import JSONEncoder -import xml.etree.ElementTree as ET -from typing_extensions import Self -import isodate -from azure.core.exceptions import DeserializationError -from azure.core import CaseInsensitiveEnumMeta -from azure.core.pipeline import PipelineResponse -from azure.core.serialization import _Null - -if sys.version_info >= (3, 9): - from collections.abc import MutableMapping -else: - from typing import MutableMapping - -_LOGGER = logging.getLogger(__name__) - -__all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"] - -TZ_UTC = timezone.utc -_T = typing.TypeVar("_T") - - -def _timedelta_as_isostr(td: timedelta) -> str: - """Converts a datetime.timedelta object into an ISO 8601 formatted string, e.g. 'P4DT12H30M05S' - - Function adapted from the Tin Can Python project: https://github.com/RusticiSoftware/TinCanPython - - :param timedelta td: The timedelta to convert - :rtype: str - :return: ISO8601 version of this timedelta - """ - - # Split seconds to larger units - seconds = td.total_seconds() - minutes, seconds = divmod(seconds, 60) - hours, minutes = divmod(minutes, 60) - days, hours = divmod(hours, 24) - - days, hours, minutes = list(map(int, (days, hours, minutes))) - seconds = round(seconds, 6) - - # Build date - date_str = "" - if days: - date_str = "%sD" % days - - if hours or minutes or seconds: - # Build time - time_str = "T" - - # Hours - bigger_exists = date_str or hours - if bigger_exists: - time_str += "{:02}H".format(hours) - - # Minutes - bigger_exists = bigger_exists or minutes - if bigger_exists: - time_str += "{:02}M".format(minutes) - - # Seconds - try: - if seconds.is_integer(): - seconds_string = "{:02}".format(int(seconds)) - else: - # 9 chars long w/ leading 0, 6 digits after decimal - seconds_string = "%09.6f" % seconds - # Remove trailing zeros - seconds_string = seconds_string.rstrip("0") - except AttributeError: # int.is_integer() raises - seconds_string = "{:02}".format(seconds) - - time_str += "{}S".format(seconds_string) - else: - time_str = "" - - return "P" + date_str + time_str - - -def _serialize_bytes(o, format: typing.Optional[str] = None) -> str: - encoded = base64.b64encode(o).decode() - if format == "base64url": - return encoded.strip("=").replace("+", "-").replace("/", "_") - return encoded - - -def _serialize_datetime(o, format: typing.Optional[str] = None): - if hasattr(o, "year") and hasattr(o, "hour"): - if format == "rfc7231": - return email.utils.format_datetime(o, usegmt=True) - if format == "unix-timestamp": - return int(calendar.timegm(o.utctimetuple())) - - # astimezone() fails for naive times in Python 2.7, so make make sure o is aware (tzinfo is set) - if not o.tzinfo: - iso_formatted = o.replace(tzinfo=TZ_UTC).isoformat() - else: - iso_formatted = o.astimezone(TZ_UTC).isoformat() - # Replace the trailing "+00:00" UTC offset with "Z" (RFC 3339: https://www.ietf.org/rfc/rfc3339.txt) - return iso_formatted.replace("+00:00", "Z") - # Next try datetime.date or datetime.time - return o.isoformat() - - -def _is_readonly(p): - try: - return p._visibility == ["read"] - except AttributeError: - return False - - -class SdkJSONEncoder(JSONEncoder): - """A JSON encoder that's capable of serializing datetime objects and bytes.""" - - def __init__(self, *args, exclude_readonly: bool = False, format: typing.Optional[str] = None, **kwargs): - super().__init__(*args, **kwargs) - self.exclude_readonly = exclude_readonly - self.format = format - - def default(self, o): # pylint: disable=too-many-return-statements - if _is_model(o): - if self.exclude_readonly: - readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)] - return {k: v for k, v in o.items() if k not in readonly_props} - return dict(o.items()) - try: - return super(SdkJSONEncoder, self).default(o) - except TypeError: - if isinstance(o, _Null): - return None - if isinstance(o, decimal.Decimal): - return float(o) - if isinstance(o, (bytes, bytearray)): - return _serialize_bytes(o, self.format) - try: - # First try datetime.datetime - return _serialize_datetime(o, self.format) - except AttributeError: - pass - # Last, try datetime.timedelta - try: - return _timedelta_as_isostr(o) - except AttributeError: - # This will be raised when it hits value.total_seconds in the method above - pass - return super(SdkJSONEncoder, self).default(o) - - -_VALID_DATE = re.compile(r"\d{4}[-]\d{2}[-]\d{2}T\d{2}:\d{2}:\d{2}" + r"\.?\d*Z?[-+]?[\d{2}]?:?[\d{2}]?") -_VALID_RFC7231 = re.compile( - r"(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s\d{2}\s" - r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT" -) - - -def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime: - """Deserialize ISO-8601 formatted string into Datetime object. - - :param str attr: response string to be deserialized. - :rtype: ~datetime.datetime - :returns: The datetime object from that input - """ - if isinstance(attr, datetime): - # i'm already deserialized - return attr - attr = attr.upper() - match = _VALID_DATE.match(attr) - if not match: - raise ValueError("Invalid datetime string: " + attr) - - check_decimal = attr.split(".") - if len(check_decimal) > 1: - decimal_str = "" - for digit in check_decimal[1]: - if digit.isdigit(): - decimal_str += digit - else: - break - if len(decimal_str) > 6: - attr = attr.replace(decimal_str, decimal_str[0:6]) - - date_obj = isodate.parse_datetime(attr) - test_utc = date_obj.utctimetuple() - if test_utc.tm_year > 9999 or test_utc.tm_year < 1: - raise OverflowError("Hit max or min date") - return date_obj - - -def _deserialize_datetime_rfc7231(attr: typing.Union[str, datetime]) -> datetime: - """Deserialize RFC7231 formatted string into Datetime object. - - :param str attr: response string to be deserialized. - :rtype: ~datetime.datetime - :returns: The datetime object from that input - """ - if isinstance(attr, datetime): - # i'm already deserialized - return attr - match = _VALID_RFC7231.match(attr) - if not match: - raise ValueError("Invalid datetime string: " + attr) - - return email.utils.parsedate_to_datetime(attr) - - -def _deserialize_datetime_unix_timestamp(attr: typing.Union[float, datetime]) -> datetime: - """Deserialize unix timestamp into Datetime object. - - :param str attr: response string to be deserialized. - :rtype: ~datetime.datetime - :returns: The datetime object from that input - """ - if isinstance(attr, datetime): - # i'm already deserialized - return attr - return datetime.fromtimestamp(attr, TZ_UTC) - - -def _deserialize_date(attr: typing.Union[str, date]) -> date: - """Deserialize ISO-8601 formatted string into Date object. - :param str attr: response string to be deserialized. - :rtype: date - :returns: The date object from that input - """ - # This must NOT use defaultmonth/defaultday. Using None ensure this raises an exception. - if isinstance(attr, date): - return attr - return isodate.parse_date(attr, defaultmonth=None, defaultday=None) # type: ignore - - -def _deserialize_time(attr: typing.Union[str, time]) -> time: - """Deserialize ISO-8601 formatted string into time object. - - :param str attr: response string to be deserialized. - :rtype: datetime.time - :returns: The time object from that input - """ - if isinstance(attr, time): - return attr - return isodate.parse_time(attr) - - -def _deserialize_bytes(attr): - if isinstance(attr, (bytes, bytearray)): - return attr - return bytes(base64.b64decode(attr)) - - -def _deserialize_bytes_base64(attr): - if isinstance(attr, (bytes, bytearray)): - return attr - padding = "=" * (3 - (len(attr) + 3) % 4) # type: ignore - attr = attr + padding # type: ignore - encoded = attr.replace("-", "+").replace("_", "/") - return bytes(base64.b64decode(encoded)) - - -def _deserialize_duration(attr): - if isinstance(attr, timedelta): - return attr - return isodate.parse_duration(attr) - - -def _deserialize_decimal(attr): - if isinstance(attr, decimal.Decimal): - return attr - return decimal.Decimal(str(attr)) - - -def _deserialize_int_as_str(attr): - if isinstance(attr, int): - return attr - return int(attr) - - -_DESERIALIZE_MAPPING = { - datetime: _deserialize_datetime, - date: _deserialize_date, - time: _deserialize_time, - bytes: _deserialize_bytes, - bytearray: _deserialize_bytes, - timedelta: _deserialize_duration, - typing.Any: lambda x: x, - decimal.Decimal: _deserialize_decimal, -} - -_DESERIALIZE_MAPPING_WITHFORMAT = { - "rfc3339": _deserialize_datetime, - "rfc7231": _deserialize_datetime_rfc7231, - "unix-timestamp": _deserialize_datetime_unix_timestamp, - "base64": _deserialize_bytes, - "base64url": _deserialize_bytes_base64, -} - - -def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None): - if annotation is int and rf and rf._format == "str": - return _deserialize_int_as_str - if rf and rf._format: - return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format) - return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore - - -def _get_type_alias_type(module_name: str, alias_name: str): - types = { - k: v - for k, v in sys.modules[module_name].__dict__.items() - if isinstance(v, typing._GenericAlias) # type: ignore - } - if alias_name not in types: - return alias_name - return types[alias_name] - - -def _get_model(module_name: str, model_name: str): - models = {k: v for k, v in sys.modules[module_name].__dict__.items() if isinstance(v, type)} - module_end = module_name.rsplit(".", 1)[0] - models.update({k: v for k, v in sys.modules[module_end].__dict__.items() if isinstance(v, type)}) - if isinstance(model_name, str): - model_name = model_name.split(".")[-1] - if model_name not in models: - return model_name - return models[model_name] - - -_UNSET = object() - - -class _MyMutableMapping(MutableMapping[str, typing.Any]): # pylint: disable=unsubscriptable-object - def __init__(self, data: typing.Dict[str, typing.Any]) -> None: - self._data = data - - def __contains__(self, key: typing.Any) -> bool: - return key in self._data - - def __getitem__(self, key: str) -> typing.Any: - return self._data.__getitem__(key) - - def __setitem__(self, key: str, value: typing.Any) -> None: - self._data.__setitem__(key, value) - - def __delitem__(self, key: str) -> None: - self._data.__delitem__(key) - - def __iter__(self) -> typing.Iterator[typing.Any]: - return self._data.__iter__() - - def __len__(self) -> int: - return self._data.__len__() - - def __ne__(self, other: typing.Any) -> bool: - return not self.__eq__(other) - - def keys(self) -> typing.KeysView[str]: - """ - :returns: a set-like object providing a view on D's keys - :rtype: ~typing.KeysView - """ - return self._data.keys() - - def values(self) -> typing.ValuesView[typing.Any]: - """ - :returns: an object providing a view on D's values - :rtype: ~typing.ValuesView - """ - return self._data.values() - - def items(self) -> typing.ItemsView[str, typing.Any]: - """ - :returns: set-like object providing a view on D's items - :rtype: ~typing.ItemsView - """ - return self._data.items() - - def get(self, key: str, default: typing.Any = None) -> typing.Any: - """ - Get the value for key if key is in the dictionary, else default. - :param str key: The key to look up. - :param any default: The value to return if key is not in the dictionary. Defaults to None - :returns: D[k] if k in D, else d. - :rtype: any - """ - try: - return self[key] - except KeyError: - return default - - @typing.overload - def pop(self, key: str) -> typing.Any: ... - - @typing.overload - def pop(self, key: str, default: _T) -> _T: ... - - @typing.overload - def pop(self, key: str, default: typing.Any) -> typing.Any: ... - - def pop(self, key: str, default: typing.Any = _UNSET) -> typing.Any: - """ - Removes specified key and return the corresponding value. - :param str key: The key to pop. - :param any default: The value to return if key is not in the dictionary - :returns: The value corresponding to the key. - :rtype: any - :raises KeyError: If key is not found and default is not given. - """ - if default is _UNSET: - return self._data.pop(key) - return self._data.pop(key, default) - - def popitem(self) -> typing.Tuple[str, typing.Any]: - """ - Removes and returns some (key, value) pair - :returns: The (key, value) pair. - :rtype: tuple - :raises KeyError: if D is empty. - """ - return self._data.popitem() - - def clear(self) -> None: - """ - Remove all items from D. - """ - self._data.clear() - - def update(self, *args: typing.Any, **kwargs: typing.Any) -> None: - """ - Updates D from mapping/iterable E and F. - :param any args: Either a mapping object or an iterable of key-value pairs. - """ - self._data.update(*args, **kwargs) - - @typing.overload - def setdefault(self, key: str, default: None = None) -> None: ... - - @typing.overload - def setdefault(self, key: str, default: typing.Any) -> typing.Any: ... - - def setdefault(self, key: str, default: typing.Any = _UNSET) -> typing.Any: - """ - Same as calling D.get(k, d), and setting D[k]=d if k not found - :param str key: The key to look up. - :param any default: The value to set if key is not in the dictionary - :returns: D[k] if k in D, else d. - :rtype: any - """ - if default is _UNSET: - return self._data.setdefault(key) - return self._data.setdefault(key, default) - - def __eq__(self, other: typing.Any) -> bool: - try: - other_model = self.__class__(other) - except Exception: - return False - return self._data == other_model._data - - def __repr__(self) -> str: - return str(self._data) - - -def _is_model(obj: typing.Any) -> bool: - return getattr(obj, "_is_model", False) - - -def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements - if isinstance(o, list): - return [_serialize(x, format) for x in o] - if isinstance(o, dict): - return {k: _serialize(v, format) for k, v in o.items()} - if isinstance(o, set): - return {_serialize(x, format) for x in o} - if isinstance(o, tuple): - return tuple(_serialize(x, format) for x in o) - if isinstance(o, (bytes, bytearray)): - return _serialize_bytes(o, format) - if isinstance(o, decimal.Decimal): - return float(o) - if isinstance(o, enum.Enum): - return o.value - if isinstance(o, int): - if format == "str": - return str(o) - return o - try: - # First try datetime.datetime - return _serialize_datetime(o, format) - except AttributeError: - pass - # Last, try datetime.timedelta - try: - return _timedelta_as_isostr(o) - except AttributeError: - # This will be raised when it hits value.total_seconds in the method above - pass - return o - - -def _get_rest_field( - attr_to_rest_field: typing.Dict[str, "_RestField"], rest_name: str -) -> typing.Optional["_RestField"]: - try: - return next(rf for rf in attr_to_rest_field.values() if rf._rest_name == rest_name) - except StopIteration: - return None - - -def _create_value(rf: typing.Optional["_RestField"], value: typing.Any) -> typing.Any: - if not rf: - return _serialize(value, None) - if rf._is_multipart_file_input: - return value - if rf._is_model: - return _deserialize(rf._type, value) - if isinstance(value, ET.Element): - value = _deserialize(rf._type, value) - return _serialize(value, rf._format) - - -class Model(_MyMutableMapping): - _is_model = True - # label whether current class's _attr_to_rest_field has been calculated - # could not see _attr_to_rest_field directly because subclass inherits it from parent class - _calculated: typing.Set[str] = set() - - def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: - class_name = self.__class__.__name__ - if len(args) > 1: - raise TypeError(f"{class_name}.__init__() takes 2 positional arguments but {len(args) + 1} were given") - dict_to_pass = { - rest_field._rest_name: rest_field._default - for rest_field in self._attr_to_rest_field.values() - if rest_field._default is not _UNSET - } - if args: # pylint: disable=too-many-nested-blocks - if isinstance(args[0], ET.Element): - existed_attr_keys = [] - model_meta = getattr(self, "_xml", {}) - - for rf in self._attr_to_rest_field.values(): - prop_meta = getattr(rf, "_xml", {}) - xml_name = prop_meta.get("name", rf._rest_name) - xml_ns = prop_meta.get("ns", model_meta.get("ns", None)) - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - - # attribute - if prop_meta.get("attribute", False) and args[0].get(xml_name) is not None: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, args[0].get(xml_name)) - continue - - # unwrapped element is array - if prop_meta.get("unwrapped", False): - # unwrapped array could either use prop items meta/prop meta - if prop_meta.get("itemsName"): - xml_name = prop_meta.get("itemsName") - xml_ns = prop_meta.get("itemNs") - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - items = args[0].findall(xml_name) # pyright: ignore - if len(items) > 0: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, items) - continue - - # text element is primitive type - if prop_meta.get("text", False): - if args[0].text is not None: - dict_to_pass[rf._rest_name] = _deserialize(rf._type, args[0].text) - continue - - # wrapped element could be normal property or array, it should only have one element - item = args[0].find(xml_name) - if item is not None: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, item) - - # rest thing is additional properties - for e in args[0]: - if e.tag not in existed_attr_keys: - dict_to_pass[e.tag] = _convert_element(e) - else: - dict_to_pass.update( - {k: _create_value(_get_rest_field(self._attr_to_rest_field, k), v) for k, v in args[0].items()} - ) - else: - non_attr_kwargs = [k for k in kwargs if k not in self._attr_to_rest_field] - if non_attr_kwargs: - # actual type errors only throw the first wrong keyword arg they see, so following that. - raise TypeError(f"{class_name}.__init__() got an unexpected keyword argument '{non_attr_kwargs[0]}'") - dict_to_pass.update( - { - self._attr_to_rest_field[k]._rest_name: _create_value(self._attr_to_rest_field[k], v) - for k, v in kwargs.items() - if v is not None - } - ) - super().__init__(dict_to_pass) - - def copy(self) -> "Model": - return Model(self.__dict__) - - def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self: - if f"{cls.__module__}.{cls.__qualname__}" not in cls._calculated: - # we know the last nine classes in mro are going to be 'Model', '_MyMutableMapping', 'MutableMapping', - # 'Mapping', 'Collection', 'Sized', 'Iterable', 'Container' and 'object' - mros = cls.__mro__[:-9][::-1] # ignore parents, and reverse the mro order - attr_to_rest_field: typing.Dict[str, _RestField] = { # map attribute name to rest_field property - k: v for mro_class in mros for k, v in mro_class.__dict__.items() if k[0] != "_" and hasattr(v, "_type") - } - annotations = { - k: v - for mro_class in mros - if hasattr(mro_class, "__annotations__") - for k, v in mro_class.__annotations__.items() - } - for attr, rf in attr_to_rest_field.items(): - rf._module = cls.__module__ - if not rf._type: - rf._type = rf._get_deserialize_callable_from_annotation(annotations.get(attr, None)) - if not rf._rest_name_input: - rf._rest_name_input = attr - cls._attr_to_rest_field: typing.Dict[str, _RestField] = dict(attr_to_rest_field.items()) - cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}") - - return super().__new__(cls) # pylint: disable=no-value-for-parameter - - def __init_subclass__(cls, discriminator: typing.Optional[str] = None) -> None: - for base in cls.__bases__: - if hasattr(base, "__mapping__"): - base.__mapping__[discriminator or cls.__name__] = cls # type: ignore - - @classmethod - def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]: - for v in cls.__dict__.values(): - if isinstance(v, _RestField) and v._is_discriminator and v._rest_name not in exist_discriminators: - return v - return None - - @classmethod - def _deserialize(cls, data, exist_discriminators): - if not hasattr(cls, "__mapping__"): - return cls(data) - discriminator = cls._get_discriminator(exist_discriminators) - if discriminator is None: - return cls(data) - exist_discriminators.append(discriminator._rest_name) - if isinstance(data, ET.Element): - model_meta = getattr(cls, "_xml", {}) - prop_meta = getattr(discriminator, "_xml", {}) - xml_name = prop_meta.get("name", discriminator._rest_name) - xml_ns = prop_meta.get("ns", model_meta.get("ns", None)) - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - - if data.get(xml_name) is not None: - discriminator_value = data.get(xml_name) - else: - discriminator_value = data.find(xml_name).text # pyright: ignore - else: - discriminator_value = data.get(discriminator._rest_name) - mapped_cls = cls.__mapping__.get(discriminator_value, cls) # pyright: ignore - return mapped_cls._deserialize(data, exist_discriminators) - - def as_dict(self, *, exclude_readonly: bool = False) -> typing.Dict[str, typing.Any]: - """Return a dict that can be turned into json using json.dump. - - :keyword bool exclude_readonly: Whether to remove the readonly properties. - :returns: A dict JSON compatible object - :rtype: dict - """ - - result = {} - readonly_props = [] - if exclude_readonly: - readonly_props = [p._rest_name for p in self._attr_to_rest_field.values() if _is_readonly(p)] - for k, v in self.items(): - if exclude_readonly and k in readonly_props: # pyright: ignore - continue - is_multipart_file_input = False - try: - is_multipart_file_input = next( - rf for rf in self._attr_to_rest_field.values() if rf._rest_name == k - )._is_multipart_file_input - except StopIteration: - pass - result[k] = v if is_multipart_file_input else Model._as_dict_value(v, exclude_readonly=exclude_readonly) - return result - - @staticmethod - def _as_dict_value(v: typing.Any, exclude_readonly: bool = False) -> typing.Any: - if v is None or isinstance(v, _Null): - return None - if isinstance(v, (list, tuple, set)): - return type(v)(Model._as_dict_value(x, exclude_readonly=exclude_readonly) for x in v) - if isinstance(v, dict): - return {dk: Model._as_dict_value(dv, exclude_readonly=exclude_readonly) for dk, dv in v.items()} - return v.as_dict(exclude_readonly=exclude_readonly) if hasattr(v, "as_dict") else v - - -def _deserialize_model(model_deserializer: typing.Optional[typing.Callable], obj): - if _is_model(obj): - return obj - return _deserialize(model_deserializer, obj) - - -def _deserialize_with_optional(if_obj_deserializer: typing.Optional[typing.Callable], obj): - if obj is None: - return obj - return _deserialize_with_callable(if_obj_deserializer, obj) - - -def _deserialize_with_union(deserializers, obj): - for deserializer in deserializers: - try: - return _deserialize(deserializer, obj) - except DeserializationError: - pass - raise DeserializationError() - - -def _deserialize_dict( - value_deserializer: typing.Optional[typing.Callable], - module: typing.Optional[str], - obj: typing.Dict[typing.Any, typing.Any], -): - if obj is None: - return obj - if isinstance(obj, ET.Element): - obj = {child.tag: child for child in obj} - return {k: _deserialize(value_deserializer, v, module) for k, v in obj.items()} - - -def _deserialize_multiple_sequence( - entry_deserializers: typing.List[typing.Optional[typing.Callable]], - module: typing.Optional[str], - obj, -): - if obj is None: - return obj - return type(obj)(_deserialize(deserializer, entry, module) for entry, deserializer in zip(obj, entry_deserializers)) - - -def _deserialize_sequence( - deserializer: typing.Optional[typing.Callable], - module: typing.Optional[str], - obj, -): - if obj is None: - return obj - if isinstance(obj, ET.Element): - obj = list(obj) - return type(obj)(_deserialize(deserializer, entry, module) for entry in obj) - - -def _sorted_annotations(types: typing.List[typing.Any]) -> typing.List[typing.Any]: - return sorted( - types, - key=lambda x: hasattr(x, "__name__") and x.__name__.lower() in ("str", "float", "int", "bool"), - ) - - -def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-return-statements, too-many-branches - annotation: typing.Any, - module: typing.Optional[str], - rf: typing.Optional["_RestField"] = None, -) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]: - if not annotation: - return None - - # is it a type alias? - if isinstance(annotation, str): - if module is not None: - annotation = _get_type_alias_type(module, annotation) - - # is it a forward ref / in quotes? - if isinstance(annotation, (str, typing.ForwardRef)): - try: - model_name = annotation.__forward_arg__ # type: ignore - except AttributeError: - model_name = annotation - if module is not None: - annotation = _get_model(module, model_name) # type: ignore - - try: - if module and _is_model(annotation): - if rf: - rf._is_model = True - - return functools.partial(_deserialize_model, annotation) # pyright: ignore - except Exception: - pass - - # is it a literal? - try: - if annotation.__origin__ is typing.Literal: # pyright: ignore - return None - except AttributeError: - pass - - # is it optional? - try: - if any(a for a in annotation.__args__ if a == type(None)): # pyright: ignore - if len(annotation.__args__) <= 2: # pyright: ignore - if_obj_deserializer = _get_deserialize_callable_from_annotation( - next(a for a in annotation.__args__ if a != type(None)), module, rf # pyright: ignore - ) - - return functools.partial(_deserialize_with_optional, if_obj_deserializer) - # the type is Optional[Union[...]], we need to remove the None type from the Union - annotation_copy = copy.copy(annotation) - annotation_copy.__args__ = [a for a in annotation_copy.__args__ if a != type(None)] # pyright: ignore - return _get_deserialize_callable_from_annotation(annotation_copy, module, rf) - except AttributeError: - pass - - # is it union? - if getattr(annotation, "__origin__", None) is typing.Union: - # initial ordering is we make `string` the last deserialization option, because it is often them most generic - deserializers = [ - _get_deserialize_callable_from_annotation(arg, module, rf) - for arg in _sorted_annotations(annotation.__args__) # pyright: ignore - ] - - return functools.partial(_deserialize_with_union, deserializers) - - try: - if annotation._name == "Dict": # pyright: ignore - value_deserializer = _get_deserialize_callable_from_annotation( - annotation.__args__[1], module, rf # pyright: ignore - ) - - return functools.partial( - _deserialize_dict, - value_deserializer, - module, - ) - except (AttributeError, IndexError): - pass - try: - if annotation._name in ["List", "Set", "Tuple", "Sequence"]: # pyright: ignore - if len(annotation.__args__) > 1: # pyright: ignore - entry_deserializers = [ - _get_deserialize_callable_from_annotation(dt, module, rf) - for dt in annotation.__args__ # pyright: ignore - ] - return functools.partial(_deserialize_multiple_sequence, entry_deserializers, module) - deserializer = _get_deserialize_callable_from_annotation( - annotation.__args__[0], module, rf # pyright: ignore - ) - - return functools.partial(_deserialize_sequence, deserializer, module) - except (TypeError, IndexError, AttributeError, SyntaxError): - pass - - def _deserialize_default( - deserializer, - obj, - ): - if obj is None: - return obj - try: - return _deserialize_with_callable(deserializer, obj) - except Exception: - pass - return obj - - if get_deserializer(annotation, rf): - return functools.partial(_deserialize_default, get_deserializer(annotation, rf)) - - return functools.partial(_deserialize_default, annotation) - - -def _deserialize_with_callable( - deserializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]], - value: typing.Any, -): # pylint: disable=too-many-return-statements - try: - if value is None or isinstance(value, _Null): - return None - if isinstance(value, ET.Element): - if deserializer is str: - return value.text or "" - if deserializer is int: - return int(value.text) if value.text else None - if deserializer is float: - return float(value.text) if value.text else None - if deserializer is bool: - return value.text == "true" if value.text else None - if deserializer is None: - return value - if deserializer in [int, float, bool]: - return deserializer(value) - if isinstance(deserializer, CaseInsensitiveEnumMeta): - try: - return deserializer(value) - except ValueError: - # for unknown value, return raw value - return value - if isinstance(deserializer, type) and issubclass(deserializer, Model): - return deserializer._deserialize(value, []) - return typing.cast(typing.Callable[[typing.Any], typing.Any], deserializer)(value) - except Exception as e: - raise DeserializationError() from e - - -def _deserialize( - deserializer: typing.Any, - value: typing.Any, - module: typing.Optional[str] = None, - rf: typing.Optional["_RestField"] = None, - format: typing.Optional[str] = None, -) -> typing.Any: - if isinstance(value, PipelineResponse): - value = value.http_response.json() - if rf is None and format: - rf = _RestField(format=format) - if not isinstance(deserializer, functools.partial): - deserializer = _get_deserialize_callable_from_annotation(deserializer, module, rf) - return _deserialize_with_callable(deserializer, value) - - -def _failsafe_deserialize( - deserializer: typing.Any, - value: typing.Any, - module: typing.Optional[str] = None, - rf: typing.Optional["_RestField"] = None, - format: typing.Optional[str] = None, -) -> typing.Any: - try: - return _deserialize(deserializer, value, module, rf, format) - except DeserializationError: - _LOGGER.warning( - "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True - ) - return None - - -def _failsafe_deserialize_xml( - deserializer: typing.Any, - value: typing.Any, -) -> typing.Any: - try: - return _deserialize_xml(deserializer, value) - except DeserializationError: - _LOGGER.warning( - "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True - ) - return None - - -class _RestField: - def __init__( - self, - *, - name: typing.Optional[str] = None, - type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin - is_discriminator: bool = False, - visibility: typing.Optional[typing.List[str]] = None, - default: typing.Any = _UNSET, - format: typing.Optional[str] = None, - is_multipart_file_input: bool = False, - xml: typing.Optional[typing.Dict[str, typing.Any]] = None, - ): - self._type = type - self._rest_name_input = name - self._module: typing.Optional[str] = None - self._is_discriminator = is_discriminator - self._visibility = visibility - self._is_model = False - self._default = default - self._format = format - self._is_multipart_file_input = is_multipart_file_input - self._xml = xml if xml is not None else {} - - @property - def _class_type(self) -> typing.Any: - return getattr(self._type, "args", [None])[0] - - @property - def _rest_name(self) -> str: - if self._rest_name_input is None: - raise ValueError("Rest name was never set") - return self._rest_name_input - - def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin - # by this point, type and rest_name will have a value bc we default - # them in __new__ of the Model class - item = obj.get(self._rest_name) - if item is None: - return item - if self._is_model: - return item - return _deserialize(self._type, _serialize(item, self._format), rf=self) - - def __set__(self, obj: Model, value) -> None: - if value is None: - # we want to wipe out entries if users set attr to None - try: - obj.__delitem__(self._rest_name) - except KeyError: - pass - return - if self._is_model: - if not _is_model(value): - value = _deserialize(self._type, value) - obj.__setitem__(self._rest_name, value) - return - obj.__setitem__(self._rest_name, _serialize(value, self._format)) - - def _get_deserialize_callable_from_annotation( - self, annotation: typing.Any - ) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]: - return _get_deserialize_callable_from_annotation(annotation, self._module, self) - - -def rest_field( - *, - name: typing.Optional[str] = None, - type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin - visibility: typing.Optional[typing.List[str]] = None, - default: typing.Any = _UNSET, - format: typing.Optional[str] = None, - is_multipart_file_input: bool = False, - xml: typing.Optional[typing.Dict[str, typing.Any]] = None, -) -> typing.Any: - return _RestField( - name=name, - type=type, - visibility=visibility, - default=default, - format=format, - is_multipart_file_input=is_multipart_file_input, - xml=xml, - ) - - -def rest_discriminator( - *, - name: typing.Optional[str] = None, - type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin - visibility: typing.Optional[typing.List[str]] = None, - xml: typing.Optional[typing.Dict[str, typing.Any]] = None, -) -> typing.Any: - return _RestField(name=name, type=type, is_discriminator=True, visibility=visibility, xml=xml) - - -def serialize_xml(model: Model, exclude_readonly: bool = False) -> str: - """Serialize a model to XML. - - :param Model model: The model to serialize. - :param bool exclude_readonly: Whether to exclude readonly properties. - :returns: The XML representation of the model. - :rtype: str - """ - return ET.tostring(_get_element(model, exclude_readonly), encoding="unicode") # type: ignore - - -def _get_element( - o: typing.Any, - exclude_readonly: bool = False, - parent_meta: typing.Optional[typing.Dict[str, typing.Any]] = None, - wrapped_element: typing.Optional[ET.Element] = None, -) -> typing.Union[ET.Element, typing.List[ET.Element]]: - if _is_model(o): - model_meta = getattr(o, "_xml", {}) - - # if prop is a model, then use the prop element directly, else generate a wrapper of model - if wrapped_element is None: - wrapped_element = _create_xml_element( - model_meta.get("name", o.__class__.__name__), - model_meta.get("prefix"), - model_meta.get("ns"), - ) - - readonly_props = [] - if exclude_readonly: - readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)] - - for k, v in o.items(): - # do not serialize readonly properties - if exclude_readonly and k in readonly_props: - continue - - prop_rest_field = _get_rest_field(o._attr_to_rest_field, k) - if prop_rest_field: - prop_meta = getattr(prop_rest_field, "_xml").copy() - # use the wire name as xml name if no specific name is set - if prop_meta.get("name") is None: - prop_meta["name"] = k - else: - # additional properties will not have rest field, use the wire name as xml name - prop_meta = {"name": k} - - # if no ns for prop, use model's - if prop_meta.get("ns") is None and model_meta.get("ns"): - prop_meta["ns"] = model_meta.get("ns") - prop_meta["prefix"] = model_meta.get("prefix") - - if prop_meta.get("unwrapped", False): - # unwrapped could only set on array - wrapped_element.extend(_get_element(v, exclude_readonly, prop_meta)) - elif prop_meta.get("text", False): - # text could only set on primitive type - wrapped_element.text = _get_primitive_type_value(v) - elif prop_meta.get("attribute", False): - xml_name = prop_meta.get("name", k) - if prop_meta.get("ns"): - ET.register_namespace(prop_meta.get("prefix"), prop_meta.get("ns")) # pyright: ignore - xml_name = "{" + prop_meta.get("ns") + "}" + xml_name # pyright: ignore - # attribute should be primitive type - wrapped_element.set(xml_name, _get_primitive_type_value(v)) - else: - # other wrapped prop element - wrapped_element.append(_get_wrapped_element(v, exclude_readonly, prop_meta)) - return wrapped_element - if isinstance(o, list): - return [_get_element(x, exclude_readonly, parent_meta) for x in o] # type: ignore - if isinstance(o, dict): - result = [] - for k, v in o.items(): - result.append( - _get_wrapped_element( - v, - exclude_readonly, - { - "name": k, - "ns": parent_meta.get("ns") if parent_meta else None, - "prefix": parent_meta.get("prefix") if parent_meta else None, - }, - ) - ) - return result - - # primitive case need to create element based on parent_meta - if parent_meta: - return _get_wrapped_element( - o, - exclude_readonly, - { - "name": parent_meta.get("itemsName", parent_meta.get("name")), - "prefix": parent_meta.get("itemsPrefix", parent_meta.get("prefix")), - "ns": parent_meta.get("itemsNs", parent_meta.get("ns")), - }, - ) - - raise ValueError("Could not serialize value into xml: " + o) - - -def _get_wrapped_element( - v: typing.Any, - exclude_readonly: bool, - meta: typing.Optional[typing.Dict[str, typing.Any]], -) -> ET.Element: - wrapped_element = _create_xml_element( - meta.get("name") if meta else None, meta.get("prefix") if meta else None, meta.get("ns") if meta else None - ) - if isinstance(v, (dict, list)): - wrapped_element.extend(_get_element(v, exclude_readonly, meta)) - elif _is_model(v): - _get_element(v, exclude_readonly, meta, wrapped_element) - else: - wrapped_element.text = _get_primitive_type_value(v) - return wrapped_element - - -def _get_primitive_type_value(v) -> str: - if v is True: - return "true" - if v is False: - return "false" - if isinstance(v, _Null): - return "" - return str(v) - - -def _create_xml_element(tag, prefix=None, ns=None): - if prefix and ns: - ET.register_namespace(prefix, ns) - if ns: - return ET.Element("{" + ns + "}" + tag) - return ET.Element(tag) - - -def _deserialize_xml( - deserializer: typing.Any, - value: str, -) -> typing.Any: - element = ET.fromstring(value) # nosec - return _deserialize(deserializer, element) - - -def _convert_element(e: ET.Element): - # dict case - if len(e.attrib) > 0 or len({child.tag for child in e}) > 1: - dict_result: typing.Dict[str, typing.Any] = {} - for child in e: - if dict_result.get(child.tag) is not None: - if isinstance(dict_result[child.tag], list): - dict_result[child.tag].append(_convert_element(child)) - else: - dict_result[child.tag] = [dict_result[child.tag], _convert_element(child)] - else: - dict_result[child.tag] = _convert_element(child) - dict_result.update(e.attrib) - return dict_result - # array case - if len(e) > 0: - array_result: typing.List[typing.Any] = [] - for child in e: - array_result.append(_convert_element(child)) - return array_result - # primitive case - return e.text diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_operations/_operations.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_operations/_operations.py index ec1ce8d2f11f..e6d1a76c29e4 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_operations/_operations.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_operations/_operations.py @@ -49,7 +49,7 @@ def build_key_vault_get_certificates_request( _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -74,7 +74,7 @@ def build_key_vault_delete_certificate_request( # pylint: disable=name-too-long _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -99,7 +99,7 @@ def build_key_vault_set_certificate_contacts_request(**kwargs: Any) -> HttpReque _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -120,7 +120,7 @@ def build_key_vault_get_certificate_contacts_request(**kwargs: Any) -> HttpReque _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -139,7 +139,7 @@ def build_key_vault_delete_certificate_contacts_request(**kwargs: Any) -> HttpRe _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -160,7 +160,7 @@ def build_key_vault_get_certificate_issuers_request( # pylint: disable=name-too _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -184,7 +184,7 @@ def build_key_vault_set_certificate_issuer_request( # pylint: disable=name-too- _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -213,7 +213,7 @@ def build_key_vault_update_certificate_issuer_request( # pylint: disable=name-t _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -241,7 +241,7 @@ def build_key_vault_get_certificate_issuer_request( # pylint: disable=name-too- _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -267,7 +267,7 @@ def build_key_vault_delete_certificate_issuer_request( # pylint: disable=name-t _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -294,7 +294,7 @@ def build_key_vault_create_certificate_request( # pylint: disable=name-too-long _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -323,7 +323,7 @@ def build_key_vault_import_certificate_request( # pylint: disable=name-too-long _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -351,7 +351,7 @@ def build_key_vault_get_certificate_versions_request( # pylint: disable=name-to _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -379,7 +379,7 @@ def build_key_vault_get_certificate_policy_request( # pylint: disable=name-too- _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -406,7 +406,7 @@ def build_key_vault_update_certificate_policy_request( # pylint: disable=name-t _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -435,7 +435,7 @@ def build_key_vault_update_certificate_request( # pylint: disable=name-too-long _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -464,7 +464,7 @@ def build_key_vault_get_certificate_request( _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -492,7 +492,7 @@ def build_key_vault_update_certificate_operation_request( # pylint: disable=nam _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -520,7 +520,7 @@ def build_key_vault_get_certificate_operation_request( # pylint: disable=name-t _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -546,7 +546,7 @@ def build_key_vault_delete_certificate_operation_request( # pylint: disable=nam _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -573,7 +573,7 @@ def build_key_vault_merge_certificate_request( # pylint: disable=name-too-long _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -601,7 +601,7 @@ def build_key_vault_backup_certificate_request( # pylint: disable=name-too-long _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -626,7 +626,7 @@ def build_key_vault_restore_certificate_request(**kwargs: Any) -> HttpRequest: _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -649,7 +649,7 @@ def build_key_vault_get_deleted_certificates_request( # pylint: disable=name-to _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -674,7 +674,7 @@ def build_key_vault_get_deleted_certificate_request( # pylint: disable=name-too _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -699,7 +699,7 @@ def build_key_vault_purge_deleted_certificate_request( # pylint: disable=name-t ) -> HttpRequest: _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) # Construct URL _url = "/deletedcertificates/{certificate-name}" path_format_arguments = { @@ -720,7 +720,7 @@ def build_key_vault_recover_deleted_certificate_request( # pylint: disable=name _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -805,7 +805,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -1250,7 +1253,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -2152,7 +2158,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -3431,7 +3440,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_operations/_patch.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_operations/_patch.py index 87676c65a8f0..ea765788358a 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_operations/_patch.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_operations/_patch.py @@ -8,7 +8,6 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ - __all__: list[str] = [] # Add all objects you want publicly available to users at this package level diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_patch.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_patch.py index 87676c65a8f0..ea765788358a 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_patch.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_patch.py @@ -8,7 +8,6 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ - __all__: list[str] = [] # Add all objects you want publicly available to users at this package level diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_serialization.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_serialization.py deleted file mode 100644 index ef86f1415163..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_serialization.py +++ /dev/null @@ -1,2050 +0,0 @@ -# pylint: disable=too-many-lines,line-too-long,useless-suppression -# -------------------------------------------------------------------------- -# -# Copyright (c) Microsoft Corporation. All rights reserved. -# -# The MIT License (MIT) -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the ""Software""), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. -# -# -------------------------------------------------------------------------- - -# pyright: reportUnnecessaryTypeIgnoreComment=false - -from base64 import b64decode, b64encode -import calendar -import datetime -import decimal -import email -from enum import Enum -import json -import logging -import re -import sys -import codecs -from typing import ( - Dict, - Any, - cast, - Optional, - Union, - AnyStr, - IO, - Mapping, - Callable, - MutableMapping, - List, -) - -try: - from urllib import quote # type: ignore -except ImportError: - from urllib.parse import quote -import xml.etree.ElementTree as ET - -import isodate # type: ignore -from typing_extensions import Self - -from azure.core.exceptions import DeserializationError, SerializationError -from azure.core.serialization import NULL as CoreNull - -_BOM = codecs.BOM_UTF8.decode(encoding="utf-8") - -JSON = MutableMapping[str, Any] - - -class RawDeserializer: - - # Accept "text" because we're open minded people... - JSON_REGEXP = re.compile(r"^(application|text)/([a-z+.]+\+)?json$") - - # Name used in context - CONTEXT_NAME = "deserialized_data" - - @classmethod - def deserialize_from_text(cls, data: Optional[Union[AnyStr, IO]], content_type: Optional[str] = None) -> Any: - """Decode data according to content-type. - - Accept a stream of data as well, but will be load at once in memory for now. - - If no content-type, will return the string version (not bytes, not stream) - - :param data: Input, could be bytes or stream (will be decoded with UTF8) or text - :type data: str or bytes or IO - :param str content_type: The content type. - :return: The deserialized data. - :rtype: object - """ - if hasattr(data, "read"): - # Assume a stream - data = cast(IO, data).read() - - if isinstance(data, bytes): - data_as_str = data.decode(encoding="utf-8-sig") - else: - # Explain to mypy the correct type. - data_as_str = cast(str, data) - - # Remove Byte Order Mark if present in string - data_as_str = data_as_str.lstrip(_BOM) - - if content_type is None: - return data - - if cls.JSON_REGEXP.match(content_type): - try: - return json.loads(data_as_str) - except ValueError as err: - raise DeserializationError("JSON is invalid: {}".format(err), err) from err - elif "xml" in (content_type or []): - try: - - try: - if isinstance(data, unicode): # type: ignore - # If I'm Python 2.7 and unicode XML will scream if I try a "fromstring" on unicode string - data_as_str = data_as_str.encode(encoding="utf-8") # type: ignore - except NameError: - pass - - return ET.fromstring(data_as_str) # nosec - except ET.ParseError as err: - # It might be because the server has an issue, and returned JSON with - # content-type XML.... - # So let's try a JSON load, and if it's still broken - # let's flow the initial exception - def _json_attemp(data): - try: - return True, json.loads(data) - except ValueError: - return False, None # Don't care about this one - - success, json_result = _json_attemp(data) - if success: - return json_result - # If i'm here, it's not JSON, it's not XML, let's scream - # and raise the last context in this block (the XML exception) - # The function hack is because Py2.7 messes up with exception - # context otherwise. - _LOGGER.critical("Wasn't XML not JSON, failing") - raise DeserializationError("XML is invalid") from err - elif content_type.startswith("text/"): - return data_as_str - raise DeserializationError("Cannot deserialize content-type: {}".format(content_type)) - - @classmethod - def deserialize_from_http_generics(cls, body_bytes: Optional[Union[AnyStr, IO]], headers: Mapping) -> Any: - """Deserialize from HTTP response. - - Use bytes and headers to NOT use any requests/aiohttp or whatever - specific implementation. - Headers will tested for "content-type" - - :param bytes body_bytes: The body of the response. - :param dict headers: The headers of the response. - :returns: The deserialized data. - :rtype: object - """ - # Try to use content-type from headers if available - content_type = None - if "content-type" in headers: - content_type = headers["content-type"].split(";")[0].strip().lower() - # Ouch, this server did not declare what it sent... - # Let's guess it's JSON... - # Also, since Autorest was considering that an empty body was a valid JSON, - # need that test as well.... - else: - content_type = "application/json" - - if body_bytes: - return cls.deserialize_from_text(body_bytes, content_type) - return None - - -_LOGGER = logging.getLogger(__name__) - -try: - _long_type = long # type: ignore -except NameError: - _long_type = int - -TZ_UTC = datetime.timezone.utc - -_FLATTEN = re.compile(r"(? None: - self.additional_properties: Optional[Dict[str, Any]] = {} - for k in kwargs: # pylint: disable=consider-using-dict-items - if k not in self._attribute_map: - _LOGGER.warning("%s is not a known attribute of class %s and will be ignored", k, self.__class__) - elif k in self._validation and self._validation[k].get("readonly", False): - _LOGGER.warning("Readonly attribute %s will be ignored in class %s", k, self.__class__) - else: - setattr(self, k, kwargs[k]) - - def __eq__(self, other: Any) -> bool: - """Compare objects by comparing all attributes. - - :param object other: The object to compare - :returns: True if objects are equal - :rtype: bool - """ - if isinstance(other, self.__class__): - return self.__dict__ == other.__dict__ - return False - - def __ne__(self, other: Any) -> bool: - """Compare objects by comparing all attributes. - - :param object other: The object to compare - :returns: True if objects are not equal - :rtype: bool - """ - return not self.__eq__(other) - - def __str__(self) -> str: - return str(self.__dict__) - - @classmethod - def enable_additional_properties_sending(cls) -> None: - cls._attribute_map["additional_properties"] = {"key": "", "type": "{object}"} - - @classmethod - def is_xml_model(cls) -> bool: - try: - cls._xml_map # type: ignore - except AttributeError: - return False - return True - - @classmethod - def _create_xml_node(cls): - """Create XML node. - - :returns: The XML node - :rtype: xml.etree.ElementTree.Element - """ - try: - xml_map = cls._xml_map # type: ignore - except AttributeError: - xml_map = {} - - return _create_xml_node(xml_map.get("name", cls.__name__), xml_map.get("prefix", None), xml_map.get("ns", None)) - - def serialize(self, keep_readonly: bool = False, **kwargs: Any) -> JSON: - """Return the JSON that would be sent to server from this model. - - This is an alias to `as_dict(full_restapi_key_transformer, keep_readonly=False)`. - - If you want XML serialization, you can pass the kwargs is_xml=True. - - :param bool keep_readonly: If you want to serialize the readonly attributes - :returns: A dict JSON compatible object - :rtype: dict - """ - serializer = Serializer(self._infer_class_models()) - return serializer._serialize( # type: ignore # pylint: disable=protected-access - self, keep_readonly=keep_readonly, **kwargs - ) - - def as_dict( - self, - keep_readonly: bool = True, - key_transformer: Callable[[str, Dict[str, Any], Any], Any] = attribute_transformer, - **kwargs: Any - ) -> JSON: - """Return a dict that can be serialized using json.dump. - - Advanced usage might optionally use a callback as parameter: - - .. code::python - - def my_key_transformer(key, attr_desc, value): - return key - - Key is the attribute name used in Python. Attr_desc - is a dict of metadata. Currently contains 'type' with the - msrest type and 'key' with the RestAPI encoded key. - Value is the current value in this object. - - The string returned will be used to serialize the key. - If the return type is a list, this is considered hierarchical - result dict. - - See the three examples in this file: - - - attribute_transformer - - full_restapi_key_transformer - - last_restapi_key_transformer - - If you want XML serialization, you can pass the kwargs is_xml=True. - - :param bool keep_readonly: If you want to serialize the readonly attributes - :param function key_transformer: A key transformer function. - :returns: A dict JSON compatible object - :rtype: dict - """ - serializer = Serializer(self._infer_class_models()) - return serializer._serialize( # type: ignore # pylint: disable=protected-access - self, key_transformer=key_transformer, keep_readonly=keep_readonly, **kwargs - ) - - @classmethod - def _infer_class_models(cls): - try: - str_models = cls.__module__.rsplit(".", 1)[0] - models = sys.modules[str_models] - client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - if cls.__name__ not in client_models: - raise ValueError("Not Autorest generated code") - except Exception: # pylint: disable=broad-exception-caught - # Assume it's not Autorest generated (tests?). Add ourselves as dependencies. - client_models = {cls.__name__: cls} - return client_models - - @classmethod - def deserialize(cls, data: Any, content_type: Optional[str] = None) -> Self: - """Parse a str using the RestAPI syntax and return a model. - - :param str data: A str using RestAPI structure. JSON by default. - :param str content_type: JSON by default, set application/xml if XML. - :returns: An instance of this model - :raises DeserializationError: if something went wrong - :rtype: Self - """ - deserializer = Deserializer(cls._infer_class_models()) - return deserializer(cls.__name__, data, content_type=content_type) # type: ignore - - @classmethod - def from_dict( - cls, - data: Any, - key_extractors: Optional[Callable[[str, Dict[str, Any], Any], Any]] = None, - content_type: Optional[str] = None, - ) -> Self: - """Parse a dict using given key extractor return a model. - - By default consider key - extractors (rest_key_case_insensitive_extractor, attribute_key_case_insensitive_extractor - and last_rest_key_case_insensitive_extractor) - - :param dict data: A dict using RestAPI structure - :param function key_extractors: A key extractor function. - :param str content_type: JSON by default, set application/xml if XML. - :returns: An instance of this model - :raises: DeserializationError if something went wrong - :rtype: Self - """ - deserializer = Deserializer(cls._infer_class_models()) - deserializer.key_extractors = ( # type: ignore - [ # type: ignore - attribute_key_case_insensitive_extractor, - rest_key_case_insensitive_extractor, - last_rest_key_case_insensitive_extractor, - ] - if key_extractors is None - else key_extractors - ) - return deserializer(cls.__name__, data, content_type=content_type) # type: ignore - - @classmethod - def _flatten_subtype(cls, key, objects): - if "_subtype_map" not in cls.__dict__: - return {} - result = dict(cls._subtype_map[key]) - for valuetype in cls._subtype_map[key].values(): - result.update(objects[valuetype]._flatten_subtype(key, objects)) # pylint: disable=protected-access - return result - - @classmethod - def _classify(cls, response, objects): - """Check the class _subtype_map for any child classes. - We want to ignore any inherited _subtype_maps. - - :param dict response: The initial data - :param dict objects: The class objects - :returns: The class to be used - :rtype: class - """ - for subtype_key in cls.__dict__.get("_subtype_map", {}).keys(): - subtype_value = None - - if not isinstance(response, ET.Element): - rest_api_response_key = cls._get_rest_key_parts(subtype_key)[-1] - subtype_value = response.get(rest_api_response_key, None) or response.get(subtype_key, None) - else: - subtype_value = xml_key_extractor(subtype_key, cls._attribute_map[subtype_key], response) - if subtype_value: - # Try to match base class. Can be class name only - # (bug to fix in Autorest to support x-ms-discriminator-name) - if cls.__name__ == subtype_value: - return cls - flatten_mapping_type = cls._flatten_subtype(subtype_key, objects) - try: - return objects[flatten_mapping_type[subtype_value]] # type: ignore - except KeyError: - _LOGGER.warning( - "Subtype value %s has no mapping, use base class %s.", - subtype_value, - cls.__name__, - ) - break - else: - _LOGGER.warning("Discriminator %s is absent or null, use base class %s.", subtype_key, cls.__name__) - break - return cls - - @classmethod - def _get_rest_key_parts(cls, attr_key): - """Get the RestAPI key of this attr, split it and decode part - :param str attr_key: Attribute key must be in attribute_map. - :returns: A list of RestAPI part - :rtype: list - """ - rest_split_key = _FLATTEN.split(cls._attribute_map[attr_key]["key"]) - return [_decode_attribute_map_key(key_part) for key_part in rest_split_key] - - -def _decode_attribute_map_key(key): - """This decode a key in an _attribute_map to the actual key we want to look at - inside the received data. - - :param str key: A key string from the generated code - :returns: The decoded key - :rtype: str - """ - return key.replace("\\.", ".") - - -class Serializer: # pylint: disable=too-many-public-methods - """Request object model serializer.""" - - basic_types = {str: "str", int: "int", bool: "bool", float: "float"} - - _xml_basic_types_serializers = {"bool": lambda x: str(x).lower()} - days = {0: "Mon", 1: "Tue", 2: "Wed", 3: "Thu", 4: "Fri", 5: "Sat", 6: "Sun"} - months = { - 1: "Jan", - 2: "Feb", - 3: "Mar", - 4: "Apr", - 5: "May", - 6: "Jun", - 7: "Jul", - 8: "Aug", - 9: "Sep", - 10: "Oct", - 11: "Nov", - 12: "Dec", - } - validation = { - "min_length": lambda x, y: len(x) < y, - "max_length": lambda x, y: len(x) > y, - "minimum": lambda x, y: x < y, - "maximum": lambda x, y: x > y, - "minimum_ex": lambda x, y: x <= y, - "maximum_ex": lambda x, y: x >= y, - "min_items": lambda x, y: len(x) < y, - "max_items": lambda x, y: len(x) > y, - "pattern": lambda x, y: not re.match(y, x, re.UNICODE), - "unique": lambda x, y: len(x) != len(set(x)), - "multiple": lambda x, y: x % y != 0, - } - - def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: - self.serialize_type = { - "iso-8601": Serializer.serialize_iso, - "rfc-1123": Serializer.serialize_rfc, - "unix-time": Serializer.serialize_unix, - "duration": Serializer.serialize_duration, - "date": Serializer.serialize_date, - "time": Serializer.serialize_time, - "decimal": Serializer.serialize_decimal, - "long": Serializer.serialize_long, - "bytearray": Serializer.serialize_bytearray, - "base64": Serializer.serialize_base64, - "object": self.serialize_object, - "[]": self.serialize_iter, - "{}": self.serialize_dict, - } - self.dependencies: Dict[str, type] = dict(classes) if classes else {} - self.key_transformer = full_restapi_key_transformer - self.client_side_validation = True - - def _serialize( # pylint: disable=too-many-nested-blocks, too-many-branches, too-many-statements, too-many-locals - self, target_obj, data_type=None, **kwargs - ): - """Serialize data into a string according to type. - - :param object target_obj: The data to be serialized. - :param str data_type: The type to be serialized from. - :rtype: str, dict - :raises SerializationError: if serialization fails. - :returns: The serialized data. - """ - key_transformer = kwargs.get("key_transformer", self.key_transformer) - keep_readonly = kwargs.get("keep_readonly", False) - if target_obj is None: - return None - - attr_name = None - class_name = target_obj.__class__.__name__ - - if data_type: - return self.serialize_data(target_obj, data_type, **kwargs) - - if not hasattr(target_obj, "_attribute_map"): - data_type = type(target_obj).__name__ - if data_type in self.basic_types.values(): - return self.serialize_data(target_obj, data_type, **kwargs) - - # Force "is_xml" kwargs if we detect a XML model - try: - is_xml_model_serialization = kwargs["is_xml"] - except KeyError: - is_xml_model_serialization = kwargs.setdefault("is_xml", target_obj.is_xml_model()) - - serialized = {} - if is_xml_model_serialization: - serialized = target_obj._create_xml_node() # pylint: disable=protected-access - try: - attributes = target_obj._attribute_map # pylint: disable=protected-access - for attr, attr_desc in attributes.items(): - attr_name = attr - if not keep_readonly and target_obj._validation.get( # pylint: disable=protected-access - attr_name, {} - ).get("readonly", False): - continue - - if attr_name == "additional_properties" and attr_desc["key"] == "": - if target_obj.additional_properties is not None: - serialized.update(target_obj.additional_properties) - continue - try: - - orig_attr = getattr(target_obj, attr) - if is_xml_model_serialization: - pass # Don't provide "transformer" for XML for now. Keep "orig_attr" - else: # JSON - keys, orig_attr = key_transformer(attr, attr_desc.copy(), orig_attr) - keys = keys if isinstance(keys, list) else [keys] - - kwargs["serialization_ctxt"] = attr_desc - new_attr = self.serialize_data(orig_attr, attr_desc["type"], **kwargs) - - if is_xml_model_serialization: - xml_desc = attr_desc.get("xml", {}) - xml_name = xml_desc.get("name", attr_desc["key"]) - xml_prefix = xml_desc.get("prefix", None) - xml_ns = xml_desc.get("ns", None) - if xml_desc.get("attr", False): - if xml_ns: - ET.register_namespace(xml_prefix, xml_ns) - xml_name = "{{{}}}{}".format(xml_ns, xml_name) - serialized.set(xml_name, new_attr) # type: ignore - continue - if xml_desc.get("text", False): - serialized.text = new_attr # type: ignore - continue - if isinstance(new_attr, list): - serialized.extend(new_attr) # type: ignore - elif isinstance(new_attr, ET.Element): - # If the down XML has no XML/Name, - # we MUST replace the tag with the local tag. But keeping the namespaces. - if "name" not in getattr(orig_attr, "_xml_map", {}): - splitted_tag = new_attr.tag.split("}") - if len(splitted_tag) == 2: # Namespace - new_attr.tag = "}".join([splitted_tag[0], xml_name]) - else: - new_attr.tag = xml_name - serialized.append(new_attr) # type: ignore - else: # That's a basic type - # Integrate namespace if necessary - local_node = _create_xml_node(xml_name, xml_prefix, xml_ns) - local_node.text = str(new_attr) - serialized.append(local_node) # type: ignore - else: # JSON - for k in reversed(keys): # type: ignore - new_attr = {k: new_attr} - - _new_attr = new_attr - _serialized = serialized - for k in keys: # type: ignore - if k not in _serialized: - _serialized.update(_new_attr) # type: ignore - _new_attr = _new_attr[k] # type: ignore - _serialized = _serialized[k] - except ValueError as err: - if isinstance(err, SerializationError): - raise - - except (AttributeError, KeyError, TypeError) as err: - msg = "Attribute {} in object {} cannot be serialized.\n{}".format(attr_name, class_name, str(target_obj)) - raise SerializationError(msg) from err - return serialized - - def body(self, data, data_type, **kwargs): - """Serialize data intended for a request body. - - :param object data: The data to be serialized. - :param str data_type: The type to be serialized from. - :rtype: dict - :raises SerializationError: if serialization fails. - :raises ValueError: if data is None - :returns: The serialized request body - """ - - # Just in case this is a dict - internal_data_type_str = data_type.strip("[]{}") - internal_data_type = self.dependencies.get(internal_data_type_str, None) - try: - is_xml_model_serialization = kwargs["is_xml"] - except KeyError: - if internal_data_type and issubclass(internal_data_type, Model): - is_xml_model_serialization = kwargs.setdefault("is_xml", internal_data_type.is_xml_model()) - else: - is_xml_model_serialization = False - if internal_data_type and not isinstance(internal_data_type, Enum): - try: - deserializer = Deserializer(self.dependencies) - # Since it's on serialization, it's almost sure that format is not JSON REST - # We're not able to deal with additional properties for now. - deserializer.additional_properties_detection = False - if is_xml_model_serialization: - deserializer.key_extractors = [ # type: ignore - attribute_key_case_insensitive_extractor, - ] - else: - deserializer.key_extractors = [ - rest_key_case_insensitive_extractor, - attribute_key_case_insensitive_extractor, - last_rest_key_case_insensitive_extractor, - ] - data = deserializer._deserialize(data_type, data) # pylint: disable=protected-access - except DeserializationError as err: - raise SerializationError("Unable to build a model: " + str(err)) from err - - return self._serialize(data, data_type, **kwargs) - - def url(self, name, data, data_type, **kwargs): - """Serialize data intended for a URL path. - - :param str name: The name of the URL path parameter. - :param object data: The data to be serialized. - :param str data_type: The type to be serialized from. - :rtype: str - :returns: The serialized URL path - :raises TypeError: if serialization fails. - :raises ValueError: if data is None - """ - try: - output = self.serialize_data(data, data_type, **kwargs) - if data_type == "bool": - output = json.dumps(output) - - if kwargs.get("skip_quote") is True: - output = str(output) - output = output.replace("{", quote("{")).replace("}", quote("}")) - else: - output = quote(str(output), safe="") - except SerializationError as exc: - raise TypeError("{} must be type {}.".format(name, data_type)) from exc - return output - - def query(self, name, data, data_type, **kwargs): - """Serialize data intended for a URL query. - - :param str name: The name of the query parameter. - :param object data: The data to be serialized. - :param str data_type: The type to be serialized from. - :rtype: str, list - :raises TypeError: if serialization fails. - :raises ValueError: if data is None - :returns: The serialized query parameter - """ - try: - # Treat the list aside, since we don't want to encode the div separator - if data_type.startswith("["): - internal_data_type = data_type[1:-1] - do_quote = not kwargs.get("skip_quote", False) - return self.serialize_iter(data, internal_data_type, do_quote=do_quote, **kwargs) - - # Not a list, regular serialization - output = self.serialize_data(data, data_type, **kwargs) - if data_type == "bool": - output = json.dumps(output) - if kwargs.get("skip_quote") is True: - output = str(output) - else: - output = quote(str(output), safe="") - except SerializationError as exc: - raise TypeError("{} must be type {}.".format(name, data_type)) from exc - return str(output) - - def header(self, name, data, data_type, **kwargs): - """Serialize data intended for a request header. - - :param str name: The name of the header. - :param object data: The data to be serialized. - :param str data_type: The type to be serialized from. - :rtype: str - :raises TypeError: if serialization fails. - :raises ValueError: if data is None - :returns: The serialized header - """ - try: - if data_type in ["[str]"]: - data = ["" if d is None else d for d in data] - - output = self.serialize_data(data, data_type, **kwargs) - if data_type == "bool": - output = json.dumps(output) - except SerializationError as exc: - raise TypeError("{} must be type {}.".format(name, data_type)) from exc - return str(output) - - def serialize_data(self, data, data_type, **kwargs): - """Serialize generic data according to supplied data type. - - :param object data: The data to be serialized. - :param str data_type: The type to be serialized from. - :raises AttributeError: if required data is None. - :raises ValueError: if data is None - :raises SerializationError: if serialization fails. - :returns: The serialized data. - :rtype: str, int, float, bool, dict, list - """ - if data is None: - raise ValueError("No value for given attribute") - - try: - if data is CoreNull: - return None - if data_type in self.basic_types.values(): - return self.serialize_basic(data, data_type, **kwargs) - - if data_type in self.serialize_type: - return self.serialize_type[data_type](data, **kwargs) - - # If dependencies is empty, try with current data class - # It has to be a subclass of Enum anyway - enum_type = self.dependencies.get(data_type, data.__class__) - if issubclass(enum_type, Enum): - return Serializer.serialize_enum(data, enum_obj=enum_type) - - iter_type = data_type[0] + data_type[-1] - if iter_type in self.serialize_type: - return self.serialize_type[iter_type](data, data_type[1:-1], **kwargs) - - except (ValueError, TypeError) as err: - msg = "Unable to serialize value: {!r} as type: {!r}." - raise SerializationError(msg.format(data, data_type)) from err - return self._serialize(data, **kwargs) - - @classmethod - def _get_custom_serializers(cls, data_type, **kwargs): # pylint: disable=inconsistent-return-statements - custom_serializer = kwargs.get("basic_types_serializers", {}).get(data_type) - if custom_serializer: - return custom_serializer - if kwargs.get("is_xml", False): - return cls._xml_basic_types_serializers.get(data_type) - - @classmethod - def serialize_basic(cls, data, data_type, **kwargs): - """Serialize basic builting data type. - Serializes objects to str, int, float or bool. - - Possible kwargs: - - basic_types_serializers dict[str, callable] : If set, use the callable as serializer - - is_xml bool : If set, use xml_basic_types_serializers - - :param obj data: Object to be serialized. - :param str data_type: Type of object in the iterable. - :rtype: str, int, float, bool - :return: serialized object - """ - custom_serializer = cls._get_custom_serializers(data_type, **kwargs) - if custom_serializer: - return custom_serializer(data) - if data_type == "str": - return cls.serialize_unicode(data) - return eval(data_type)(data) # nosec # pylint: disable=eval-used - - @classmethod - def serialize_unicode(cls, data): - """Special handling for serializing unicode strings in Py2. - Encode to UTF-8 if unicode, otherwise handle as a str. - - :param str data: Object to be serialized. - :rtype: str - :return: serialized object - """ - try: # If I received an enum, return its value - return data.value - except AttributeError: - pass - - try: - if isinstance(data, unicode): # type: ignore - # Don't change it, JSON and XML ElementTree are totally able - # to serialize correctly u'' strings - return data - except NameError: - return str(data) - return str(data) - - def serialize_iter(self, data, iter_type, div=None, **kwargs): - """Serialize iterable. - - Supported kwargs: - - serialization_ctxt dict : The current entry of _attribute_map, or same format. - serialization_ctxt['type'] should be same as data_type. - - is_xml bool : If set, serialize as XML - - :param list data: Object to be serialized. - :param str iter_type: Type of object in the iterable. - :param str div: If set, this str will be used to combine the elements - in the iterable into a combined string. Default is 'None'. - Defaults to False. - :rtype: list, str - :return: serialized iterable - """ - if isinstance(data, str): - raise SerializationError("Refuse str type as a valid iter type.") - - serialization_ctxt = kwargs.get("serialization_ctxt", {}) - is_xml = kwargs.get("is_xml", False) - - serialized = [] - for d in data: - try: - serialized.append(self.serialize_data(d, iter_type, **kwargs)) - except ValueError as err: - if isinstance(err, SerializationError): - raise - serialized.append(None) - - if kwargs.get("do_quote", False): - serialized = ["" if s is None else quote(str(s), safe="") for s in serialized] - - if div: - serialized = ["" if s is None else str(s) for s in serialized] - serialized = div.join(serialized) - - if "xml" in serialization_ctxt or is_xml: - # XML serialization is more complicated - xml_desc = serialization_ctxt.get("xml", {}) - xml_name = xml_desc.get("name") - if not xml_name: - xml_name = serialization_ctxt["key"] - - # Create a wrap node if necessary (use the fact that Element and list have "append") - is_wrapped = xml_desc.get("wrapped", False) - node_name = xml_desc.get("itemsName", xml_name) - if is_wrapped: - final_result = _create_xml_node(xml_name, xml_desc.get("prefix", None), xml_desc.get("ns", None)) - else: - final_result = [] - # All list elements to "local_node" - for el in serialized: - if isinstance(el, ET.Element): - el_node = el - else: - el_node = _create_xml_node(node_name, xml_desc.get("prefix", None), xml_desc.get("ns", None)) - if el is not None: # Otherwise it writes "None" :-p - el_node.text = str(el) - final_result.append(el_node) - return final_result - return serialized - - def serialize_dict(self, attr, dict_type, **kwargs): - """Serialize a dictionary of objects. - - :param dict attr: Object to be serialized. - :param str dict_type: Type of object in the dictionary. - :rtype: dict - :return: serialized dictionary - """ - serialization_ctxt = kwargs.get("serialization_ctxt", {}) - serialized = {} - for key, value in attr.items(): - try: - serialized[self.serialize_unicode(key)] = self.serialize_data(value, dict_type, **kwargs) - except ValueError as err: - if isinstance(err, SerializationError): - raise - serialized[self.serialize_unicode(key)] = None - - if "xml" in serialization_ctxt: - # XML serialization is more complicated - xml_desc = serialization_ctxt["xml"] - xml_name = xml_desc["name"] - - final_result = _create_xml_node(xml_name, xml_desc.get("prefix", None), xml_desc.get("ns", None)) - for key, value in serialized.items(): - ET.SubElement(final_result, key).text = value - return final_result - - return serialized - - def serialize_object(self, attr, **kwargs): # pylint: disable=too-many-return-statements - """Serialize a generic object. - This will be handled as a dictionary. If object passed in is not - a basic type (str, int, float, dict, list) it will simply be - cast to str. - - :param dict attr: Object to be serialized. - :rtype: dict or str - :return: serialized object - """ - if attr is None: - return None - if isinstance(attr, ET.Element): - return attr - obj_type = type(attr) - if obj_type in self.basic_types: - return self.serialize_basic(attr, self.basic_types[obj_type], **kwargs) - if obj_type is _long_type: - return self.serialize_long(attr) - if obj_type is str: - return self.serialize_unicode(attr) - if obj_type is datetime.datetime: - return self.serialize_iso(attr) - if obj_type is datetime.date: - return self.serialize_date(attr) - if obj_type is datetime.time: - return self.serialize_time(attr) - if obj_type is datetime.timedelta: - return self.serialize_duration(attr) - if obj_type is decimal.Decimal: - return self.serialize_decimal(attr) - - # If it's a model or I know this dependency, serialize as a Model - if obj_type in self.dependencies.values() or isinstance(attr, Model): - return self._serialize(attr) - - if obj_type == dict: - serialized = {} - for key, value in attr.items(): - try: - serialized[self.serialize_unicode(key)] = self.serialize_object(value, **kwargs) - except ValueError: - serialized[self.serialize_unicode(key)] = None - return serialized - - if obj_type == list: - serialized = [] - for obj in attr: - try: - serialized.append(self.serialize_object(obj, **kwargs)) - except ValueError: - pass - return serialized - return str(attr) - - @staticmethod - def serialize_enum(attr, enum_obj=None): - try: - result = attr.value - except AttributeError: - result = attr - try: - enum_obj(result) # type: ignore - return result - except ValueError as exc: - for enum_value in enum_obj: # type: ignore - if enum_value.value.lower() == str(attr).lower(): - return enum_value.value - error = "{!r} is not valid value for enum {!r}" - raise SerializationError(error.format(attr, enum_obj)) from exc - - @staticmethod - def serialize_bytearray(attr, **kwargs): # pylint: disable=unused-argument - """Serialize bytearray into base-64 string. - - :param str attr: Object to be serialized. - :rtype: str - :return: serialized base64 - """ - return b64encode(attr).decode() - - @staticmethod - def serialize_base64(attr, **kwargs): # pylint: disable=unused-argument - """Serialize str into base-64 string. - - :param str attr: Object to be serialized. - :rtype: str - :return: serialized base64 - """ - encoded = b64encode(attr).decode("ascii") - return encoded.strip("=").replace("+", "-").replace("/", "_") - - @staticmethod - def serialize_decimal(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Decimal object to float. - - :param decimal attr: Object to be serialized. - :rtype: float - :return: serialized decimal - """ - return float(attr) - - @staticmethod - def serialize_long(attr, **kwargs): # pylint: disable=unused-argument - """Serialize long (Py2) or int (Py3). - - :param int attr: Object to be serialized. - :rtype: int/long - :return: serialized long - """ - return _long_type(attr) - - @staticmethod - def serialize_date(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Date object into ISO-8601 formatted string. - - :param Date attr: Object to be serialized. - :rtype: str - :return: serialized date - """ - if isinstance(attr, str): - attr = isodate.parse_date(attr) - t = "{:04}-{:02}-{:02}".format(attr.year, attr.month, attr.day) - return t - - @staticmethod - def serialize_time(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Time object into ISO-8601 formatted string. - - :param datetime.time attr: Object to be serialized. - :rtype: str - :return: serialized time - """ - if isinstance(attr, str): - attr = isodate.parse_time(attr) - t = "{:02}:{:02}:{:02}".format(attr.hour, attr.minute, attr.second) - if attr.microsecond: - t += ".{:02}".format(attr.microsecond) - return t - - @staticmethod - def serialize_duration(attr, **kwargs): # pylint: disable=unused-argument - """Serialize TimeDelta object into ISO-8601 formatted string. - - :param TimeDelta attr: Object to be serialized. - :rtype: str - :return: serialized duration - """ - if isinstance(attr, str): - attr = isodate.parse_duration(attr) - return isodate.duration_isoformat(attr) - - @staticmethod - def serialize_rfc(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Datetime object into RFC-1123 formatted string. - - :param Datetime attr: Object to be serialized. - :rtype: str - :raises TypeError: if format invalid. - :return: serialized rfc - """ - try: - if not attr.tzinfo: - _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") - utc = attr.utctimetuple() - except AttributeError as exc: - raise TypeError("RFC1123 object must be valid Datetime object.") from exc - - return "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT".format( - Serializer.days[utc.tm_wday], - utc.tm_mday, - Serializer.months[utc.tm_mon], - utc.tm_year, - utc.tm_hour, - utc.tm_min, - utc.tm_sec, - ) - - @staticmethod - def serialize_iso(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Datetime object into ISO-8601 formatted string. - - :param Datetime attr: Object to be serialized. - :rtype: str - :raises SerializationError: if format invalid. - :return: serialized iso - """ - if isinstance(attr, str): - attr = isodate.parse_datetime(attr) - try: - if not attr.tzinfo: - _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") - utc = attr.utctimetuple() - if utc.tm_year > 9999 or utc.tm_year < 1: - raise OverflowError("Hit max or min date") - - microseconds = str(attr.microsecond).rjust(6, "0").rstrip("0").ljust(3, "0") - if microseconds: - microseconds = "." + microseconds - date = "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}".format( - utc.tm_year, utc.tm_mon, utc.tm_mday, utc.tm_hour, utc.tm_min, utc.tm_sec - ) - return date + microseconds + "Z" - except (ValueError, OverflowError) as err: - msg = "Unable to serialize datetime object." - raise SerializationError(msg) from err - except AttributeError as err: - msg = "ISO-8601 object must be valid Datetime object." - raise TypeError(msg) from err - - @staticmethod - def serialize_unix(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Datetime object into IntTime format. - This is represented as seconds. - - :param Datetime attr: Object to be serialized. - :rtype: int - :raises SerializationError: if format invalid - :return: serialied unix - """ - if isinstance(attr, int): - return attr - try: - if not attr.tzinfo: - _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") - return int(calendar.timegm(attr.utctimetuple())) - except AttributeError as exc: - raise TypeError("Unix time object must be valid Datetime object.") from exc - - -def rest_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument - key = attr_desc["key"] - working_data = data - - while "." in key: - # Need the cast, as for some reasons "split" is typed as list[str | Any] - dict_keys = cast(List[str], _FLATTEN.split(key)) - if len(dict_keys) == 1: - key = _decode_attribute_map_key(dict_keys[0]) - break - working_key = _decode_attribute_map_key(dict_keys[0]) - working_data = working_data.get(working_key, data) - if working_data is None: - # If at any point while following flatten JSON path see None, it means - # that all properties under are None as well - return None - key = ".".join(dict_keys[1:]) - - return working_data.get(key) - - -def rest_key_case_insensitive_extractor( # pylint: disable=unused-argument, inconsistent-return-statements - attr, attr_desc, data -): - key = attr_desc["key"] - working_data = data - - while "." in key: - dict_keys = _FLATTEN.split(key) - if len(dict_keys) == 1: - key = _decode_attribute_map_key(dict_keys[0]) - break - working_key = _decode_attribute_map_key(dict_keys[0]) - working_data = attribute_key_case_insensitive_extractor(working_key, None, working_data) - if working_data is None: - # If at any point while following flatten JSON path see None, it means - # that all properties under are None as well - return None - key = ".".join(dict_keys[1:]) - - if working_data: - return attribute_key_case_insensitive_extractor(key, None, working_data) - - -def last_rest_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument - """Extract the attribute in "data" based on the last part of the JSON path key. - - :param str attr: The attribute to extract - :param dict attr_desc: The attribute description - :param dict data: The data to extract from - :rtype: object - :returns: The extracted attribute - """ - key = attr_desc["key"] - dict_keys = _FLATTEN.split(key) - return attribute_key_extractor(dict_keys[-1], None, data) - - -def last_rest_key_case_insensitive_extractor(attr, attr_desc, data): # pylint: disable=unused-argument - """Extract the attribute in "data" based on the last part of the JSON path key. - - This is the case insensitive version of "last_rest_key_extractor" - :param str attr: The attribute to extract - :param dict attr_desc: The attribute description - :param dict data: The data to extract from - :rtype: object - :returns: The extracted attribute - """ - key = attr_desc["key"] - dict_keys = _FLATTEN.split(key) - return attribute_key_case_insensitive_extractor(dict_keys[-1], None, data) - - -def attribute_key_extractor(attr, _, data): - return data.get(attr) - - -def attribute_key_case_insensitive_extractor(attr, _, data): - found_key = None - lower_attr = attr.lower() - for key in data: - if lower_attr == key.lower(): - found_key = key - break - - return data.get(found_key) - - -def _extract_name_from_internal_type(internal_type): - """Given an internal type XML description, extract correct XML name with namespace. - - :param dict internal_type: An model type - :rtype: tuple - :returns: A tuple XML name + namespace dict - """ - internal_type_xml_map = getattr(internal_type, "_xml_map", {}) - xml_name = internal_type_xml_map.get("name", internal_type.__name__) - xml_ns = internal_type_xml_map.get("ns", None) - if xml_ns: - xml_name = "{{{}}}{}".format(xml_ns, xml_name) - return xml_name - - -def xml_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument,too-many-return-statements - if isinstance(data, dict): - return None - - # Test if this model is XML ready first - if not isinstance(data, ET.Element): - return None - - xml_desc = attr_desc.get("xml", {}) - xml_name = xml_desc.get("name", attr_desc["key"]) - - # Look for a children - is_iter_type = attr_desc["type"].startswith("[") - is_wrapped = xml_desc.get("wrapped", False) - internal_type = attr_desc.get("internalType", None) - internal_type_xml_map = getattr(internal_type, "_xml_map", {}) - - # Integrate namespace if necessary - xml_ns = xml_desc.get("ns", internal_type_xml_map.get("ns", None)) - if xml_ns: - xml_name = "{{{}}}{}".format(xml_ns, xml_name) - - # If it's an attribute, that's simple - if xml_desc.get("attr", False): - return data.get(xml_name) - - # If it's x-ms-text, that's simple too - if xml_desc.get("text", False): - return data.text - - # Scenario where I take the local name: - # - Wrapped node - # - Internal type is an enum (considered basic types) - # - Internal type has no XML/Name node - if is_wrapped or (internal_type and (issubclass(internal_type, Enum) or "name" not in internal_type_xml_map)): - children = data.findall(xml_name) - # If internal type has a local name and it's not a list, I use that name - elif not is_iter_type and internal_type and "name" in internal_type_xml_map: - xml_name = _extract_name_from_internal_type(internal_type) - children = data.findall(xml_name) - # That's an array - else: - if internal_type: # Complex type, ignore itemsName and use the complex type name - items_name = _extract_name_from_internal_type(internal_type) - else: - items_name = xml_desc.get("itemsName", xml_name) - children = data.findall(items_name) - - if len(children) == 0: - if is_iter_type: - if is_wrapped: - return None # is_wrapped no node, we want None - return [] # not wrapped, assume empty list - return None # Assume it's not there, maybe an optional node. - - # If is_iter_type and not wrapped, return all found children - if is_iter_type: - if not is_wrapped: - return children - # Iter and wrapped, should have found one node only (the wrap one) - if len(children) != 1: - raise DeserializationError( - "Tried to deserialize an array not wrapped, and found several nodes '{}'. Maybe you should declare this array as wrapped?".format( # pylint: disable=line-too-long - xml_name - ) - ) - return list(children[0]) # Might be empty list and that's ok. - - # Here it's not a itertype, we should have found one element only or empty - if len(children) > 1: - raise DeserializationError("Find several XML '{}' where it was not expected".format(xml_name)) - return children[0] - - -class Deserializer: - """Response object model deserializer. - - :param dict classes: Class type dictionary for deserializing complex types. - :ivar list key_extractors: Ordered list of extractors to be used by this deserializer. - """ - - basic_types = {str: "str", int: "int", bool: "bool", float: "float"} - - valid_date = re.compile(r"\d{4}[-]\d{2}[-]\d{2}T\d{2}:\d{2}:\d{2}\.?\d*Z?[-+]?[\d{2}]?:?[\d{2}]?") - - def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: - self.deserialize_type = { - "iso-8601": Deserializer.deserialize_iso, - "rfc-1123": Deserializer.deserialize_rfc, - "unix-time": Deserializer.deserialize_unix, - "duration": Deserializer.deserialize_duration, - "date": Deserializer.deserialize_date, - "time": Deserializer.deserialize_time, - "decimal": Deserializer.deserialize_decimal, - "long": Deserializer.deserialize_long, - "bytearray": Deserializer.deserialize_bytearray, - "base64": Deserializer.deserialize_base64, - "object": self.deserialize_object, - "[]": self.deserialize_iter, - "{}": self.deserialize_dict, - } - self.deserialize_expected_types = { - "duration": (isodate.Duration, datetime.timedelta), - "iso-8601": (datetime.datetime), - } - self.dependencies: Dict[str, type] = dict(classes) if classes else {} - self.key_extractors = [rest_key_extractor, xml_key_extractor] - # Additional properties only works if the "rest_key_extractor" is used to - # extract the keys. Making it to work whatever the key extractor is too much - # complicated, with no real scenario for now. - # So adding a flag to disable additional properties detection. This flag should be - # used if your expect the deserialization to NOT come from a JSON REST syntax. - # Otherwise, result are unexpected - self.additional_properties_detection = True - - def __call__(self, target_obj, response_data, content_type=None): - """Call the deserializer to process a REST response. - - :param str target_obj: Target data type to deserialize to. - :param requests.Response response_data: REST response object. - :param str content_type: Swagger "produces" if available. - :raises DeserializationError: if deserialization fails. - :return: Deserialized object. - :rtype: object - """ - data = self._unpack_content(response_data, content_type) - return self._deserialize(target_obj, data) - - def _deserialize(self, target_obj, data): # pylint: disable=inconsistent-return-statements - """Call the deserializer on a model. - - Data needs to be already deserialized as JSON or XML ElementTree - - :param str target_obj: Target data type to deserialize to. - :param object data: Object to deserialize. - :raises DeserializationError: if deserialization fails. - :return: Deserialized object. - :rtype: object - """ - # This is already a model, go recursive just in case - if hasattr(data, "_attribute_map"): - constants = [name for name, config in getattr(data, "_validation", {}).items() if config.get("constant")] - try: - for attr, mapconfig in data._attribute_map.items(): # pylint: disable=protected-access - if attr in constants: - continue - value = getattr(data, attr) - if value is None: - continue - local_type = mapconfig["type"] - internal_data_type = local_type.strip("[]{}") - if internal_data_type not in self.dependencies or isinstance(internal_data_type, Enum): - continue - setattr(data, attr, self._deserialize(local_type, value)) - return data - except AttributeError: - return - - response, class_name = self._classify_target(target_obj, data) - - if isinstance(response, str): - return self.deserialize_data(data, response) - if isinstance(response, type) and issubclass(response, Enum): - return self.deserialize_enum(data, response) - - if data is None or data is CoreNull: - return data - try: - attributes = response._attribute_map # type: ignore # pylint: disable=protected-access - d_attrs = {} - for attr, attr_desc in attributes.items(): - # Check empty string. If it's not empty, someone has a real "additionalProperties"... - if attr == "additional_properties" and attr_desc["key"] == "": - continue - raw_value = None - # Enhance attr_desc with some dynamic data - attr_desc = attr_desc.copy() # Do a copy, do not change the real one - internal_data_type = attr_desc["type"].strip("[]{}") - if internal_data_type in self.dependencies: - attr_desc["internalType"] = self.dependencies[internal_data_type] - - for key_extractor in self.key_extractors: - found_value = key_extractor(attr, attr_desc, data) - if found_value is not None: - if raw_value is not None and raw_value != found_value: - msg = ( - "Ignoring extracted value '%s' from %s for key '%s'" - " (duplicate extraction, follow extractors order)" - ) - _LOGGER.warning(msg, found_value, key_extractor, attr) - continue - raw_value = found_value - - value = self.deserialize_data(raw_value, attr_desc["type"]) - d_attrs[attr] = value - except (AttributeError, TypeError, KeyError) as err: - msg = "Unable to deserialize to object: " + class_name # type: ignore - raise DeserializationError(msg) from err - additional_properties = self._build_additional_properties(attributes, data) - return self._instantiate_model(response, d_attrs, additional_properties) - - def _build_additional_properties(self, attribute_map, data): - if not self.additional_properties_detection: - return None - if "additional_properties" in attribute_map and attribute_map.get("additional_properties", {}).get("key") != "": - # Check empty string. If it's not empty, someone has a real "additionalProperties" - return None - if isinstance(data, ET.Element): - data = {el.tag: el.text for el in data} - - known_keys = { - _decode_attribute_map_key(_FLATTEN.split(desc["key"])[0]) - for desc in attribute_map.values() - if desc["key"] != "" - } - present_keys = set(data.keys()) - missing_keys = present_keys - known_keys - return {key: data[key] for key in missing_keys} - - def _classify_target(self, target, data): - """Check to see whether the deserialization target object can - be classified into a subclass. - Once classification has been determined, initialize object. - - :param str target: The target object type to deserialize to. - :param str/dict data: The response data to deserialize. - :return: The classified target object and its class name. - :rtype: tuple - """ - if target is None: - return None, None - - if isinstance(target, str): - try: - target = self.dependencies[target] - except KeyError: - return target, target - - try: - target = target._classify(data, self.dependencies) # type: ignore # pylint: disable=protected-access - except AttributeError: - pass # Target is not a Model, no classify - return target, target.__class__.__name__ # type: ignore - - def failsafe_deserialize(self, target_obj, data, content_type=None): - """Ignores any errors encountered in deserialization, - and falls back to not deserializing the object. Recommended - for use in error deserialization, as we want to return the - HttpResponseError to users, and not have them deal with - a deserialization error. - - :param str target_obj: The target object type to deserialize to. - :param str/dict data: The response data to deserialize. - :param str content_type: Swagger "produces" if available. - :return: Deserialized object. - :rtype: object - """ - try: - return self(target_obj, data, content_type=content_type) - except: # pylint: disable=bare-except - _LOGGER.debug( - "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True - ) - return None - - @staticmethod - def _unpack_content(raw_data, content_type=None): - """Extract the correct structure for deserialization. - - If raw_data is a PipelineResponse, try to extract the result of RawDeserializer. - if we can't, raise. Your Pipeline should have a RawDeserializer. - - If not a pipeline response and raw_data is bytes or string, use content-type - to decode it. If no content-type, try JSON. - - If raw_data is something else, bypass all logic and return it directly. - - :param obj raw_data: Data to be processed. - :param str content_type: How to parse if raw_data is a string/bytes. - :raises JSONDecodeError: If JSON is requested and parsing is impossible. - :raises UnicodeDecodeError: If bytes is not UTF8 - :rtype: object - :return: Unpacked content. - """ - # Assume this is enough to detect a Pipeline Response without importing it - context = getattr(raw_data, "context", {}) - if context: - if RawDeserializer.CONTEXT_NAME in context: - return context[RawDeserializer.CONTEXT_NAME] - raise ValueError("This pipeline didn't have the RawDeserializer policy; can't deserialize") - - # Assume this is enough to recognize universal_http.ClientResponse without importing it - if hasattr(raw_data, "body"): - return RawDeserializer.deserialize_from_http_generics(raw_data.text(), raw_data.headers) - - # Assume this enough to recognize requests.Response without importing it. - if hasattr(raw_data, "_content_consumed"): - return RawDeserializer.deserialize_from_http_generics(raw_data.text, raw_data.headers) - - if isinstance(raw_data, (str, bytes)) or hasattr(raw_data, "read"): - return RawDeserializer.deserialize_from_text(raw_data, content_type) # type: ignore - return raw_data - - def _instantiate_model(self, response, attrs, additional_properties=None): - """Instantiate a response model passing in deserialized args. - - :param Response response: The response model class. - :param dict attrs: The deserialized response attributes. - :param dict additional_properties: Additional properties to be set. - :rtype: Response - :return: The instantiated response model. - """ - if callable(response): - subtype = getattr(response, "_subtype_map", {}) - try: - readonly = [ - k - for k, v in response._validation.items() # pylint: disable=protected-access # type: ignore - if v.get("readonly") - ] - const = [ - k - for k, v in response._validation.items() # pylint: disable=protected-access # type: ignore - if v.get("constant") - ] - kwargs = {k: v for k, v in attrs.items() if k not in subtype and k not in readonly + const} - response_obj = response(**kwargs) - for attr in readonly: - setattr(response_obj, attr, attrs.get(attr)) - if additional_properties: - response_obj.additional_properties = additional_properties # type: ignore - return response_obj - except TypeError as err: - msg = "Unable to deserialize {} into model {}. ".format(kwargs, response) # type: ignore - raise DeserializationError(msg + str(err)) from err - else: - try: - for attr, value in attrs.items(): - setattr(response, attr, value) - return response - except Exception as exp: - msg = "Unable to populate response model. " - msg += "Type: {}, Error: {}".format(type(response), exp) - raise DeserializationError(msg) from exp - - def deserialize_data(self, data, data_type): # pylint: disable=too-many-return-statements - """Process data for deserialization according to data type. - - :param str data: The response string to be deserialized. - :param str data_type: The type to deserialize to. - :raises DeserializationError: if deserialization fails. - :return: Deserialized object. - :rtype: object - """ - if data is None: - return data - - try: - if not data_type: - return data - if data_type in self.basic_types.values(): - return self.deserialize_basic(data, data_type) - if data_type in self.deserialize_type: - if isinstance(data, self.deserialize_expected_types.get(data_type, tuple())): - return data - - is_a_text_parsing_type = lambda x: x not in [ # pylint: disable=unnecessary-lambda-assignment - "object", - "[]", - r"{}", - ] - if isinstance(data, ET.Element) and is_a_text_parsing_type(data_type) and not data.text: - return None - data_val = self.deserialize_type[data_type](data) - return data_val - - iter_type = data_type[0] + data_type[-1] - if iter_type in self.deserialize_type: - return self.deserialize_type[iter_type](data, data_type[1:-1]) - - obj_type = self.dependencies[data_type] - if issubclass(obj_type, Enum): - if isinstance(data, ET.Element): - data = data.text - return self.deserialize_enum(data, obj_type) - - except (ValueError, TypeError, AttributeError) as err: - msg = "Unable to deserialize response data." - msg += " Data: {}, {}".format(data, data_type) - raise DeserializationError(msg) from err - return self._deserialize(obj_type, data) - - def deserialize_iter(self, attr, iter_type): - """Deserialize an iterable. - - :param list attr: Iterable to be deserialized. - :param str iter_type: The type of object in the iterable. - :return: Deserialized iterable. - :rtype: list - """ - if attr is None: - return None - if isinstance(attr, ET.Element): # If I receive an element here, get the children - attr = list(attr) - if not isinstance(attr, (list, set)): - raise DeserializationError("Cannot deserialize as [{}] an object of type {}".format(iter_type, type(attr))) - return [self.deserialize_data(a, iter_type) for a in attr] - - def deserialize_dict(self, attr, dict_type): - """Deserialize a dictionary. - - :param dict/list attr: Dictionary to be deserialized. Also accepts - a list of key, value pairs. - :param str dict_type: The object type of the items in the dictionary. - :return: Deserialized dictionary. - :rtype: dict - """ - if isinstance(attr, list): - return {x["key"]: self.deserialize_data(x["value"], dict_type) for x in attr} - - if isinstance(attr, ET.Element): - # Transform value into {"Key": "value"} - attr = {el.tag: el.text for el in attr} - return {k: self.deserialize_data(v, dict_type) for k, v in attr.items()} - - def deserialize_object(self, attr, **kwargs): # pylint: disable=too-many-return-statements - """Deserialize a generic object. - This will be handled as a dictionary. - - :param dict attr: Dictionary to be deserialized. - :return: Deserialized object. - :rtype: dict - :raises TypeError: if non-builtin datatype encountered. - """ - if attr is None: - return None - if isinstance(attr, ET.Element): - # Do no recurse on XML, just return the tree as-is - return attr - if isinstance(attr, str): - return self.deserialize_basic(attr, "str") - obj_type = type(attr) - if obj_type in self.basic_types: - return self.deserialize_basic(attr, self.basic_types[obj_type]) - if obj_type is _long_type: - return self.deserialize_long(attr) - - if obj_type == dict: - deserialized = {} - for key, value in attr.items(): - try: - deserialized[key] = self.deserialize_object(value, **kwargs) - except ValueError: - deserialized[key] = None - return deserialized - - if obj_type == list: - deserialized = [] - for obj in attr: - try: - deserialized.append(self.deserialize_object(obj, **kwargs)) - except ValueError: - pass - return deserialized - - error = "Cannot deserialize generic object with type: " - raise TypeError(error + str(obj_type)) - - def deserialize_basic(self, attr, data_type): # pylint: disable=too-many-return-statements - """Deserialize basic builtin data type from string. - Will attempt to convert to str, int, float and bool. - This function will also accept '1', '0', 'true' and 'false' as - valid bool values. - - :param str attr: response string to be deserialized. - :param str data_type: deserialization data type. - :return: Deserialized basic type. - :rtype: str, int, float or bool - :raises TypeError: if string format is not valid. - """ - # If we're here, data is supposed to be a basic type. - # If it's still an XML node, take the text - if isinstance(attr, ET.Element): - attr = attr.text - if not attr: - if data_type == "str": - # None or '', node is empty string. - return "" - # None or '', node with a strong type is None. - # Don't try to model "empty bool" or "empty int" - return None - - if data_type == "bool": - if attr in [True, False, 1, 0]: - return bool(attr) - if isinstance(attr, str): - if attr.lower() in ["true", "1"]: - return True - if attr.lower() in ["false", "0"]: - return False - raise TypeError("Invalid boolean value: {}".format(attr)) - - if data_type == "str": - return self.deserialize_unicode(attr) - return eval(data_type)(attr) # nosec # pylint: disable=eval-used - - @staticmethod - def deserialize_unicode(data): - """Preserve unicode objects in Python 2, otherwise return data - as a string. - - :param str data: response string to be deserialized. - :return: Deserialized string. - :rtype: str or unicode - """ - # We might be here because we have an enum modeled as string, - # and we try to deserialize a partial dict with enum inside - if isinstance(data, Enum): - return data - - # Consider this is real string - try: - if isinstance(data, unicode): # type: ignore - return data - except NameError: - return str(data) - return str(data) - - @staticmethod - def deserialize_enum(data, enum_obj): - """Deserialize string into enum object. - - If the string is not a valid enum value it will be returned as-is - and a warning will be logged. - - :param str data: Response string to be deserialized. If this value is - None or invalid it will be returned as-is. - :param Enum enum_obj: Enum object to deserialize to. - :return: Deserialized enum object. - :rtype: Enum - """ - if isinstance(data, enum_obj) or data is None: - return data - if isinstance(data, Enum): - data = data.value - if isinstance(data, int): - # Workaround. We might consider remove it in the future. - try: - return list(enum_obj.__members__.values())[data] - except IndexError as exc: - error = "{!r} is not a valid index for enum {!r}" - raise DeserializationError(error.format(data, enum_obj)) from exc - try: - return enum_obj(str(data)) - except ValueError: - for enum_value in enum_obj: - if enum_value.value.lower() == str(data).lower(): - return enum_value - # We don't fail anymore for unknown value, we deserialize as a string - _LOGGER.warning("Deserializer is not able to find %s as valid enum in %s", data, enum_obj) - return Deserializer.deserialize_unicode(data) - - @staticmethod - def deserialize_bytearray(attr): - """Deserialize string into bytearray. - - :param str attr: response string to be deserialized. - :return: Deserialized bytearray - :rtype: bytearray - :raises TypeError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - return bytearray(b64decode(attr)) # type: ignore - - @staticmethod - def deserialize_base64(attr): - """Deserialize base64 encoded string into string. - - :param str attr: response string to be deserialized. - :return: Deserialized base64 string - :rtype: bytearray - :raises TypeError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - padding = "=" * (3 - (len(attr) + 3) % 4) # type: ignore - attr = attr + padding # type: ignore - encoded = attr.replace("-", "+").replace("_", "/") - return b64decode(encoded) - - @staticmethod - def deserialize_decimal(attr): - """Deserialize string into Decimal object. - - :param str attr: response string to be deserialized. - :return: Deserialized decimal - :raises DeserializationError: if string format invalid. - :rtype: decimal - """ - if isinstance(attr, ET.Element): - attr = attr.text - try: - return decimal.Decimal(str(attr)) # type: ignore - except decimal.DecimalException as err: - msg = "Invalid decimal {}".format(attr) - raise DeserializationError(msg) from err - - @staticmethod - def deserialize_long(attr): - """Deserialize string into long (Py2) or int (Py3). - - :param str attr: response string to be deserialized. - :return: Deserialized int - :rtype: long or int - :raises ValueError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - return _long_type(attr) # type: ignore - - @staticmethod - def deserialize_duration(attr): - """Deserialize ISO-8601 formatted string into TimeDelta object. - - :param str attr: response string to be deserialized. - :return: Deserialized duration - :rtype: TimeDelta - :raises DeserializationError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - try: - duration = isodate.parse_duration(attr) - except (ValueError, OverflowError, AttributeError) as err: - msg = "Cannot deserialize duration object." - raise DeserializationError(msg) from err - return duration - - @staticmethod - def deserialize_date(attr): - """Deserialize ISO-8601 formatted string into Date object. - - :param str attr: response string to be deserialized. - :return: Deserialized date - :rtype: Date - :raises DeserializationError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - if re.search(r"[^\W\d_]", attr, re.I + re.U): # type: ignore - raise DeserializationError("Date must have only digits and -. Received: %s" % attr) - # This must NOT use defaultmonth/defaultday. Using None ensure this raises an exception. - return isodate.parse_date(attr, defaultmonth=0, defaultday=0) - - @staticmethod - def deserialize_time(attr): - """Deserialize ISO-8601 formatted string into time object. - - :param str attr: response string to be deserialized. - :return: Deserialized time - :rtype: datetime.time - :raises DeserializationError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - if re.search(r"[^\W\d_]", attr, re.I + re.U): # type: ignore - raise DeserializationError("Date must have only digits and -. Received: %s" % attr) - return isodate.parse_time(attr) - - @staticmethod - def deserialize_rfc(attr): - """Deserialize RFC-1123 formatted string into Datetime object. - - :param str attr: response string to be deserialized. - :return: Deserialized RFC datetime - :rtype: Datetime - :raises DeserializationError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - try: - parsed_date = email.utils.parsedate_tz(attr) # type: ignore - date_obj = datetime.datetime( - *parsed_date[:6], tzinfo=datetime.timezone(datetime.timedelta(minutes=(parsed_date[9] or 0) / 60)) - ) - if not date_obj.tzinfo: - date_obj = date_obj.astimezone(tz=TZ_UTC) - except ValueError as err: - msg = "Cannot deserialize to rfc datetime object." - raise DeserializationError(msg) from err - return date_obj - - @staticmethod - def deserialize_iso(attr): - """Deserialize ISO-8601 formatted string into Datetime object. - - :param str attr: response string to be deserialized. - :return: Deserialized ISO datetime - :rtype: Datetime - :raises DeserializationError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - try: - attr = attr.upper() # type: ignore - match = Deserializer.valid_date.match(attr) - if not match: - raise ValueError("Invalid datetime string: " + attr) - - check_decimal = attr.split(".") - if len(check_decimal) > 1: - decimal_str = "" - for digit in check_decimal[1]: - if digit.isdigit(): - decimal_str += digit - else: - break - if len(decimal_str) > 6: - attr = attr.replace(decimal_str, decimal_str[0:6]) - - date_obj = isodate.parse_datetime(attr) - test_utc = date_obj.utctimetuple() - if test_utc.tm_year > 9999 or test_utc.tm_year < 1: - raise OverflowError("Hit max or min date") - except (ValueError, OverflowError, AttributeError) as err: - msg = "Cannot deserialize datetime object." - raise DeserializationError(msg) from err - return date_obj - - @staticmethod - def deserialize_unix(attr): - """Serialize Datetime object into IntTime format. - This is represented as seconds. - - :param int attr: Object to be serialized. - :return: Deserialized datetime - :rtype: Datetime - :raises DeserializationError: if format invalid - """ - if isinstance(attr, ET.Element): - attr = int(attr.text) # type: ignore - try: - attr = int(attr) - date_obj = datetime.datetime.fromtimestamp(attr, TZ_UTC) - except ValueError as err: - msg = "Cannot deserialize to unix datetime object." - raise DeserializationError(msg) from err - return date_obj diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_utils/model_base.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_utils/model_base.py index 7b7f8ba67b53..4102784f9a85 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_utils/model_base.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_utils/model_base.py @@ -23,14 +23,19 @@ from json import JSONEncoder import xml.etree.ElementTree as ET from collections.abc import MutableMapping -from typing_extensions import Self import isodate from azure.core.exceptions import DeserializationError from azure.core import CaseInsensitiveEnumMeta from azure.core.pipeline import PipelineResponse from azure.core.serialization import _Null + from azure.core.rest import HttpResponse +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + _LOGGER = logging.getLogger(__name__) __all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"] @@ -595,62 +600,10 @@ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: class_name = self.__class__.__name__ if len(args) > 1: raise TypeError(f"{class_name}.__init__() takes 2 positional arguments but {len(args) + 1} were given") - dict_to_pass = { - rest_field._rest_name: rest_field._default - for rest_field in self._attr_to_rest_field.values() - if rest_field._default is not _UNSET - } - if args: # pylint: disable=too-many-nested-blocks + dict_to_pass: dict[str, typing.Any] = {} + if args: if isinstance(args[0], ET.Element): - existed_attr_keys = [] - model_meta = getattr(self, "_xml", {}) - - for rf in self._attr_to_rest_field.values(): - prop_meta = getattr(rf, "_xml", {}) - xml_name = prop_meta.get("name", rf._rest_name) - xml_ns = prop_meta.get("ns", model_meta.get("ns", None)) - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - - # attribute - if prop_meta.get("attribute", False) and args[0].get(xml_name) is not None: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, args[0].get(xml_name)) - continue - - # unwrapped element is array - if prop_meta.get("unwrapped", False): - # unwrapped array could either use prop items meta/prop meta - if prop_meta.get("itemsName"): - xml_name = prop_meta.get("itemsName") - xml_ns = prop_meta.get("itemNs") - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - items = args[0].findall(xml_name) # pyright: ignore - if len(items) > 0: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, items) - elif not rf._is_optional: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = [] - continue - - # text element is primitive type - if prop_meta.get("text", False): - if args[0].text is not None: - dict_to_pass[rf._rest_name] = _deserialize(rf._type, args[0].text) - continue - - # wrapped element could be normal property or array, it should only have one element - item = args[0].find(xml_name) - if item is not None: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, item) - - # rest thing is additional properties - for e in args[0]: - if e.tag not in existed_attr_keys: - dict_to_pass[e.tag] = _convert_element(e) + dict_to_pass.update(self._init_from_xml(args[0])) else: dict_to_pass.update( {k: _create_value(_get_rest_field(self._attr_to_rest_field, k), v) for k, v in args[0].items()} @@ -667,8 +620,79 @@ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: if v is not None } ) + # Apply client default values for fields the caller didn't set so that + # defaults are part of `_data` and therefore included during serialization. + for rf in self._attr_to_rest_field.values(): + if rf._default is _UNSET: + continue + if rf._rest_name in dict_to_pass: + continue + dict_to_pass[rf._rest_name] = _create_value(rf, rf._default) super().__init__(dict_to_pass) + def _init_from_xml(self, element: ET.Element) -> dict[str, typing.Any]: + """Deserialize an XML element into a dict mapping rest field names to values. + + :param ET.Element element: The XML element to deserialize from. + :returns: A dictionary of rest_name to deserialized value pairs. + :rtype: dict + """ + result: dict[str, typing.Any] = {} + model_meta = getattr(self, "_xml", {}) + existed_attr_keys: list[str] = [] + + for rf in self._attr_to_rest_field.values(): + prop_meta = getattr(rf, "_xml", {}) + xml_name = prop_meta.get("name", rf._rest_name) + xml_ns = _resolve_xml_ns(prop_meta, model_meta) + if xml_ns: + xml_name = "{" + xml_ns + "}" + xml_name + + # attribute + if prop_meta.get("attribute", False) and element.get(xml_name) is not None: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = _deserialize(rf._type, element.get(xml_name)) + continue + + # unwrapped element is array + if prop_meta.get("unwrapped", False): + # unwrapped array could either use prop items meta/prop meta + _items_name = prop_meta.get("itemsName") + if _items_name: + xml_name = _items_name + _items_ns = prop_meta.get("itemsNs") + if _items_ns is not None: + xml_ns = _items_ns + if xml_ns: + xml_name = "{" + xml_ns + "}" + xml_name + items = element.findall(xml_name) # pyright: ignore + if len(items) > 0: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = _deserialize(rf._type, items) + elif not rf._is_optional: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = [] + continue + + # text element is primitive type + if prop_meta.get("text", False): + if element.text is not None: + result[rf._rest_name] = _deserialize(rf._type, element.text) + continue + + # wrapped element could be normal property or array, it should only have one element + item = element.find(xml_name) + if item is not None: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = _deserialize(rf._type, item) + + # rest thing is additional properties + for e in element: + if e.tag not in existed_attr_keys: + result[e.tag] = _convert_element(e) + + return result + def copy(self) -> "Model": return Model(self.__dict__) @@ -721,7 +745,7 @@ def _deserialize(cls, data, exist_discriminators): model_meta = getattr(cls, "_xml", {}) prop_meta = getattr(discriminator, "_xml", {}) xml_name = prop_meta.get("name", discriminator._rest_name) - xml_ns = prop_meta.get("ns", model_meta.get("ns", None)) + xml_ns = _resolve_xml_ns(prop_meta, model_meta) if xml_ns: xml_name = "{" + xml_ns + "}" + xml_name @@ -1098,7 +1122,10 @@ def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin # by this point, type and rest_name will have a value bc we default # them in __new__ of the Model class # Use _data.get() directly to avoid triggering __getitem__ which clears the cache - item = obj._data.get(self._rest_name) + item = obj._data.get(self._rest_name, _UNSET) + if item is _UNSET: + # Field not set by user; return the client default if one exists, otherwise None + return self._default if self._default is not _UNSET else None if item is None: return item if self._is_model: @@ -1190,6 +1217,56 @@ def serialize_xml(model: Model, exclude_readonly: bool = False) -> str: return ET.tostring(_get_element(model, exclude_readonly), encoding="unicode") # type: ignore +def _get_xml_ns(meta: dict[str, typing.Any]) -> typing.Optional[str]: + """Return the XML namespace from a metadata dict, checking both 'ns' (old-style) and 'namespace' (DPG) keys. + + :param dict meta: The metadata dictionary to extract namespace from. + :returns: The namespace string if 'ns' or 'namespace' key is present, None otherwise. + :rtype: str or None + """ + ns = meta.get("ns") + if ns is None: + ns = meta.get("namespace") + return ns + + +def _resolve_xml_ns( + prop_meta: dict[str, typing.Any], model_meta: typing.Optional[dict[str, typing.Any]] = None +) -> typing.Optional[str]: + """Resolve XML namespace for a property, falling back to model namespace when appropriate. + + Checks the property metadata first; if no namespace is found and the model does not declare + an explicit prefix, falls back to the model-level namespace. + + :param dict prop_meta: The property metadata dictionary. + :param dict model_meta: The model metadata dictionary, used as fallback. + :returns: The resolved namespace string, or None. + :rtype: str or None + """ + ns = _get_xml_ns(prop_meta) + if ns is None and model_meta is not None and not model_meta.get("prefix"): + ns = _get_xml_ns(model_meta) + return ns + + +def _set_xml_attribute(element: ET.Element, name: str, value: typing.Any, prop_meta: dict[str, typing.Any]) -> None: + """Set an XML attribute on an element, handling namespace prefix registration. + + :param ET.Element element: The element to set the attribute on. + :param str name: The default attribute name (wire name). + :param any value: The attribute value. + :param dict prop_meta: The property metadata dictionary. + """ + xml_name = prop_meta.get("name", name) + _attr_ns = _get_xml_ns(prop_meta) + if _attr_ns: + _attr_prefix = prop_meta.get("prefix") + if _attr_prefix: + _safe_register_namespace(_attr_prefix, _attr_ns) + xml_name = "{" + _attr_ns + "}" + xml_name + element.set(xml_name, _get_primitive_type_value(value)) + + def _get_element( o: typing.Any, exclude_readonly: bool = False, @@ -1201,10 +1278,16 @@ def _get_element( # if prop is a model, then use the prop element directly, else generate a wrapper of model if wrapped_element is None: + # When serializing as an array item (parent_meta is set), check if the parent has an + # explicit itemsName. This ensures correct element names for unwrapped arrays (where + # the element tag is the property/items name, not the model type name). + _items_name = parent_meta.get("itemsName") if parent_meta is not None else None + element_name = _items_name if _items_name else (model_meta.get("name") or o.__class__.__name__) + _model_ns = _get_xml_ns(model_meta) wrapped_element = _create_xml_element( - model_meta.get("name", o.__class__.__name__), + element_name, model_meta.get("prefix"), - model_meta.get("ns"), + _model_ns, ) readonly_props = [] @@ -1226,7 +1309,9 @@ def _get_element( # additional properties will not have rest field, use the wire name as xml name prop_meta = {"name": k} - # if no ns for prop, use model's + # Propagate model namespace to properties only for old-style "ns"-keyed models. + # DPG-generated models use the "namespace" key and explicitly declare namespace on + # each property that needs it, so propagation is intentionally skipped for them. if prop_meta.get("ns") is None and model_meta.get("ns"): prop_meta["ns"] = model_meta.get("ns") prop_meta["prefix"] = model_meta.get("prefix") @@ -1238,12 +1323,7 @@ def _get_element( # text could only set on primitive type wrapped_element.text = _get_primitive_type_value(v) elif prop_meta.get("attribute", False): - xml_name = prop_meta.get("name", k) - if prop_meta.get("ns"): - ET.register_namespace(prop_meta.get("prefix"), prop_meta.get("ns")) # pyright: ignore - xml_name = "{" + prop_meta.get("ns") + "}" + xml_name # pyright: ignore - # attribute should be primitive type - wrapped_element.set(xml_name, _get_primitive_type_value(v)) + _set_xml_attribute(wrapped_element, k, v, prop_meta) else: # other wrapped prop element wrapped_element.append(_get_wrapped_element(v, exclude_readonly, prop_meta)) @@ -1252,6 +1332,7 @@ def _get_element( return [_get_element(x, exclude_readonly, parent_meta) for x in o] # type: ignore if isinstance(o, dict): result = [] + _dict_ns = _get_xml_ns(parent_meta) if parent_meta else None for k, v in o.items(): result.append( _get_wrapped_element( @@ -1259,7 +1340,7 @@ def _get_element( exclude_readonly, { "name": k, - "ns": parent_meta.get("ns") if parent_meta else None, + "ns": _dict_ns, "prefix": parent_meta.get("prefix") if parent_meta else None, }, ) @@ -1268,13 +1349,16 @@ def _get_element( # primitive case need to create element based on parent_meta if parent_meta: + _items_ns = parent_meta.get("itemsNs") + if _items_ns is None: + _items_ns = _get_xml_ns(parent_meta) return _get_wrapped_element( o, exclude_readonly, { "name": parent_meta.get("itemsName", parent_meta.get("name")), "prefix": parent_meta.get("itemsPrefix", parent_meta.get("prefix")), - "ns": parent_meta.get("itemsNs", parent_meta.get("ns")), + "ns": _items_ns, }, ) @@ -1286,8 +1370,9 @@ def _get_wrapped_element( exclude_readonly: bool, meta: typing.Optional[dict[str, typing.Any]], ) -> ET.Element: + _meta_ns = _get_xml_ns(meta) if meta else None wrapped_element = _create_xml_element( - meta.get("name") if meta else None, meta.get("prefix") if meta else None, meta.get("ns") if meta else None + meta.get("name") if meta else None, meta.get("prefix") if meta else None, _meta_ns ) if isinstance(v, (dict, list)): wrapped_element.extend(_get_element(v, exclude_readonly, meta)) @@ -1308,11 +1393,29 @@ def _get_primitive_type_value(v) -> str: return str(v) +def _safe_register_namespace(prefix: str, ns: str) -> None: + """Register an XML namespace prefix, handling reserved prefix patterns. + + Some prefixes (e.g. 'ns2') match Python's reserved 'ns\\d+' pattern used for + auto-generated prefixes, causing register_namespace to raise ValueError. + Falls back to directly registering in the internal namespace map. + + :param str prefix: The namespace prefix to register. + :param str ns: The namespace URI. + """ + try: + ET.register_namespace(prefix, ns) + except ValueError: + _ns_map = getattr(ET, "_namespace_map", None) + if _ns_map is not None: + _ns_map[ns] = prefix + + def _create_xml_element( tag: typing.Any, prefix: typing.Optional[str] = None, ns: typing.Optional[str] = None ) -> ET.Element: if prefix and ns: - ET.register_namespace(prefix, ns) + _safe_register_namespace(prefix, ns) if ns: return ET.Element("{" + ns + "}" + tag) return ET.Element(tag) diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_utils/serialization.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_utils/serialization.py index 81ec1de5922b..954bf7ebffa7 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_utils/serialization.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_utils/serialization.py @@ -39,11 +39,15 @@ import xml.etree.ElementTree as ET import isodate # type: ignore -from typing_extensions import Self from azure.core.exceptions import DeserializationError, SerializationError from azure.core.serialization import NULL as CoreNull +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + _BOM = codecs.BOM_UTF8.decode(encoding="utf-8") JSON = MutableMapping[str, Any] diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_vendor.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_vendor.py deleted file mode 100644 index 3790083b97e3..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_vendor.py +++ /dev/null @@ -1,25 +0,0 @@ -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- - -from abc import ABC -from typing import TYPE_CHECKING - -from ._configuration import KeyVaultClientConfiguration - -if TYPE_CHECKING: - from azure.core import PipelineClient - - from ._serialization import Deserializer, Serializer - - -class KeyVaultClientMixinABC(ABC): - """DO NOT use this class. It is for internal typing use only.""" - - _client: "PipelineClient" - _config: KeyVaultClientConfiguration - _serialize: "Serializer" - _deserialize: "Deserializer" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_version.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_version.py index 2e2a864184cb..c8302cef558b 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_version.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "4.11.0" +VERSION = "4.12.0b1" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_client.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_client.py index 11ddf0a7723c..9774eded4af6 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_client.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_client.py @@ -7,8 +7,8 @@ # -------------------------------------------------------------------------- from copy import deepcopy +import sys from typing import Any, Awaitable, TYPE_CHECKING -from typing_extensions import Self from azure.core import AsyncPipelineClient from azure.core.pipeline import policies @@ -18,6 +18,11 @@ from ._configuration import KeyVaultClientConfiguration from ._operations import _KeyVaultClientOperationsMixin +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self # type: ignore + if TYPE_CHECKING: from azure.core.credentials_async import AsyncTokenCredential @@ -30,9 +35,10 @@ class KeyVaultClient(_KeyVaultClientOperationsMixin): :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials_async.AsyncTokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". - Default value is "2025-07-01". Note that overriding this default value may result in - unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_configuration.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_configuration.py index 6f735c36e9a1..363c279fe1c4 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_configuration.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_configuration.py @@ -26,14 +26,15 @@ class KeyVaultClientConfiguration: # pylint: disable=too-many-instance-attribut :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials_async.AsyncTokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". - Default value is "2025-07-01". Note that overriding this default value may result in - unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ def __init__(self, vault_base_url: str, credential: "AsyncTokenCredential", **kwargs: Any) -> None: - api_version: str = kwargs.pop("api_version", "2025-07-01") + api_version: str = kwargs.pop("api_version", "2026-03-01-preview") if vault_base_url is None: raise ValueError("Parameter 'vault_base_url' must not be None.") diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_operations/_operations.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_operations/_operations.py index c6b5717fdb36..c74fd8541eab 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_operations/_operations.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_operations/_operations.py @@ -134,7 +134,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -579,7 +582,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -1481,7 +1487,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -2760,7 +2769,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_operations/_patch.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_operations/_patch.py index 87676c65a8f0..ea765788358a 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_operations/_patch.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_operations/_patch.py @@ -8,7 +8,6 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ - __all__: list[str] = [] # Add all objects you want publicly available to users at this package level diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_patch.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_patch.py index 87676c65a8f0..ea765788358a 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_patch.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_patch.py @@ -8,7 +8,6 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ - __all__: list[str] = [] # Add all objects you want publicly available to users at this package level diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_vendor.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_vendor.py deleted file mode 100644 index 2b1f525d61ea..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/aio/_vendor.py +++ /dev/null @@ -1,25 +0,0 @@ -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- - -from abc import ABC -from typing import TYPE_CHECKING - -from ._configuration import KeyVaultClientConfiguration - -if TYPE_CHECKING: - from azure.core import AsyncPipelineClient - - from .._serialization import Deserializer, Serializer - - -class KeyVaultClientMixinABC(ABC): - """DO NOT use this class. It is for internal typing use only.""" - - _client: "AsyncPipelineClient" - _config: KeyVaultClientConfiguration - _serialize: "Serializer" - _deserialize: "Deserializer" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/__init__.py index 1f8f05ec4716..150bc150a075 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/__init__.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/__init__.py @@ -44,6 +44,7 @@ KeyVaultErrorError, LifetimeAction, OrganizationDetails, + PlatformManaged, SecretProperties, SubjectAlternativeNames, Trigger, @@ -92,6 +93,7 @@ "KeyVaultErrorError", "LifetimeAction", "OrganizationDetails", + "PlatformManaged", "SecretProperties", "SubjectAlternativeNames", "Trigger", diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_enums.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_enums.py index 31ba0587e4a1..e80c953acbb3 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_enums.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_enums.py @@ -66,13 +66,13 @@ class DeletionRecoveryLevel(str, Enum, metaclass=CaseInsensitiveEnumMeta): class JsonWebKeyCurveName(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Elliptic curve name. For valid values, see JsonWebKeyCurveName.""" - P_256 = "P-256" + P256 = "P-256" """The NIST P-256 elliptic curve, AKA SECG curve SECP256R1.""" - P_384 = "P-384" + P384 = "P-384" """The NIST P-384 elliptic curve, AKA SECG curve SECP384R1.""" - P_521 = "P-521" + P521 = "P-521" """The NIST P-521 elliptic curve, AKA SECG curve SECP521R1.""" - P_256K = "P-256K" + P256_K = "P-256K" """The SECG SECP256K1 elliptic curve.""" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_models.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_models.py index b92fc61c908a..a324ccc66ab0 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_models.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_models.py @@ -724,6 +724,9 @@ class CertificatePolicy(_Model): :vartype issuer_parameters: ~azure.keyvault.certificates._generated.models.IssuerParameters :ivar attributes: The certificate attributes. :vartype attributes: ~azure.keyvault.certificates._generated.models.CertificateAttributes + :ivar platform_managed: Configuration that enables the platform to manage the certificate on + behalf of the user. This feature is currently intended for internal use only. + :vartype platform_managed: ~azure.keyvault.certificates._generated.models.PlatformManaged """ id: Optional[str] = rest_field(visibility=["read"]) @@ -752,6 +755,11 @@ class CertificatePolicy(_Model): visibility=["read", "create", "update", "delete", "query"] ) """The certificate attributes.""" + platform_managed: Optional["_models.PlatformManaged"] = rest_field( + name="platformManaged", visibility=["read", "create", "update", "delete", "query"] + ) + """Configuration that enables the platform to manage the certificate on behalf of the user. This + feature is currently intended for internal use only.""" @overload def __init__( @@ -763,6 +771,7 @@ def __init__( lifetime_actions: Optional[list["_models.LifetimeAction"]] = None, issuer_parameters: Optional["_models.IssuerParameters"] = None, attributes: Optional["_models.CertificateAttributes"] = None, + platform_managed: Optional["_models.PlatformManaged"] = None, ) -> None: ... @overload @@ -1411,6 +1420,44 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) +class PlatformManaged(_Model): + """Properties of the platform managed certificate. This feature is currently intended for internal + use only. + + :ivar certificate_usage: The intended usage of the certificate. Required. + :vartype certificate_usage: str + :ivar metadata: JSON-formatted platform managed metadata. The schema is intentionally undefined + as this feature is currently intended for internal use only. + :vartype metadata: dict[str, any] + """ + + certificate_usage: str = rest_field( + name="certificateUsage", visibility=["read", "create", "update", "delete", "query"] + ) + """The intended usage of the certificate. Required.""" + metadata: Optional[dict[str, Any]] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """JSON-formatted platform managed metadata. The schema is intentionally undefined as this feature + is currently intended for internal use only.""" + + @overload + def __init__( + self, + *, + certificate_usage: str, + metadata: Optional[dict[str, Any]] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + class SecretProperties(_Model): """Properties of the key backing a certificate. diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_patch.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_patch.py index 87676c65a8f0..ea765788358a 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_patch.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_generated/models/_patch.py @@ -8,7 +8,6 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ - __all__: list[str] = [] # Add all objects you want publicly available to users at this package level diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py deleted file mode 100644 index 6c3d3eba60f9..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py +++ /dev/null @@ -1,1560 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -# pylint: disable=too-many-lines,too-many-public-methods -from datetime import datetime -from typing import Any, Dict, Optional, Union, List - -from ._generated import models -from ._shared import parse_key_vault_id -from ._enums import ( - CertificatePolicyAction, - KeyUsageType, - KeyCurveName, - KeyType, - CertificateContentType, - WellKnownIssuerNames, -) - - -class AdministratorContact(object): - """Details of the organization administrator of the certificate issuer. - - :param first_name: First name of the issuer. - :type first_name: str or None - :param last_name: Last name of the issuer. - :type last_name: str or None - :param email: email of the issuer. - :type email: str or None - :param phone: phone number of the issuer. - :type phone: str or None - """ - - def __init__( - self, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - email: Optional[str] = None, - phone: Optional[str] = None, - ) -> None: - self._first_name = first_name - self._last_name = last_name - self._phone = phone - self._email = email - - def __repr__(self) -> str: - result = ( - f"AdministratorContact(first_name={self.first_name}, last_name={self.last_name}, " - + f"email={self.email}, phone={self.phone})" - ) - return result[:1024] - - @classmethod - def _from_admin_detail(cls, admin_detail: models.AdministratorDetails) -> "AdministratorContact": - return cls( - email=admin_detail.email_address, - first_name=admin_detail.first_name, - last_name=admin_detail.last_name, - phone=admin_detail.phone, - ) - - @property - def email(self) -> Optional[str]: - """Email address of the issuer. - - :rtype: str or None""" - return self._email - - @property - def first_name(self) -> Optional[str]: - """First name of the issuer. - - :rtype: str or None""" - return self._first_name - - @property - def last_name(self) -> Optional[str]: - """Last name of the issuer. - - :rtype: str or None""" - return self._last_name - - @property - def phone(self) -> Optional[str]: - """Phone number of the issuer. - - :rtype: str or None""" - return self._phone - - -class CertificateOperationError(object): - """The key vault server error. - - :param str code: The error code. - :param str message: The error message. - :param inner_error: The error object itself - :type inner_error: ~azure.keyvault.certificates.CertificateOperationError or None - """ - - def __init__(self, code: str, message: str, inner_error: "Optional[CertificateOperationError]" = None) -> None: - self._code = code - self._message = message - self._inner_error = inner_error - - def __repr__(self) -> str: - return f"CertificateOperationError({self.code}, {self.message}, {self.inner_error})"[:1024] - - @classmethod - def _from_error_bundle(cls, error_bundle: models.KeyVaultErrorError) -> "CertificateOperationError": - return cls( - code=error_bundle.code, # type: ignore - message=error_bundle.message, # type: ignore - inner_error=cls._from_error_bundle(error_bundle.inner_error) if error_bundle.inner_error else None, - ) - - @property - def code(self) -> str: - """The error code. - - :returns: The error code. - :rtype: str - """ - return self._code - - @property - def message(self) -> str: - """The error message. - - :returns: The error message. - :rtype: str - """ - return self._message - - @property - def inner_error(self) -> "Optional[CertificateOperationError]": - """The error itself. - - :returns: The error itself. - :rtype: ~azure.keyvault.certificates.CertificateOperationError or None - """ - return self._inner_error - - -class CertificateProperties(object): - """Certificate properties consists of a certificates metadata.""" - - def __init__(self, **kwargs: Any) -> None: - self._attributes = kwargs.pop("attributes", None) - self._id = kwargs.pop("cert_id", None) - self._vault_id = KeyVaultCertificateIdentifier(self._id) - self._x509_thumbprint = kwargs.pop("x509_thumbprint", None) - self._tags = kwargs.pop("tags", None) - self._preserve_order = kwargs.pop("preserve_order", False) - - def __repr__(self) -> str: - return f""[:1024] - - @classmethod - def _from_certificate_bundle( - cls, - certificate_bundle: Union[ - models.CertificateBundle, - models.DeletedCertificateBundle, - ], - ) -> "CertificateProperties": - return cls( - attributes=certificate_bundle.attributes, - cert_id=certificate_bundle.id, - x509_thumbprint=certificate_bundle.x509_thumbprint, - tags=certificate_bundle.tags, - preserve_order=certificate_bundle.preserve_cert_order, - ) - - @classmethod - def _from_certificate_item( - cls, - certificate_item: Union[ - models.CertificateItem, - models.DeletedCertificateItem, - ], - ) -> "CertificateProperties": - return cls( - attributes=certificate_item.attributes, - cert_id=certificate_item.id, - x509_thumbprint=certificate_item.x509_thumbprint, - tags=certificate_item.tags, - ) - - @property - def id(self) -> str: - """The certificate identifier. - - :returns: The certificate identifier. - :rtype: str - """ - return self._id - - @property - def name(self) -> str: - """The name of the certificate. - - :returns: The name of the certificate. - :rtype: str - """ - return self._vault_id.name - - @property - def enabled(self) -> Optional[bool]: - """Whether the certificate is enabled or not. - - :returns: True if the certificate is enabled; False otherwise. - :rtype: bool or None - """ - return self._attributes.enabled if self._attributes else None - - @property - def not_before(self) -> Optional[datetime]: - """The datetime before which the certificate is not valid. - - :returns: A datetime representing the point in time when the certificate becomes valid. - :rtype: ~datetime.datetime or None - """ - return self._attributes.not_before if self._attributes else None - - @property - def expires_on(self) -> Optional[datetime]: - """The datetime when the certificate expires. - - :returns: A datetime representing the point in time when the certificate expires. - :rtype: ~datetime.datetime or None - """ - return self._attributes.expires if self._attributes else None - - @property - def created_on(self) -> Optional[datetime]: - """The datetime when the certificate is created. - - :returns: A datetime representing the certificate's creation time. - :rtype: ~datetime.datetime or None - """ - return self._attributes.created if self._attributes else None - - @property - def updated_on(self) -> Optional[datetime]: - """The datetime when the certificate was last updated. - - :returns: A datetime representing the time of the certificate's most recent update. - :rtype: ~datetime.datetime or None - """ - return self._attributes.updated if self._attributes else None - - @property - def recoverable_days(self) -> Optional[int]: - """The number of days the certificate is retained before being deleted from a soft-delete enabled Key Vault. - - :returns: The number of days remaining where the certificate can be restored. - :rtype: int or None - """ - # recoverable_days was added in 7.1-preview - if self._attributes and hasattr(self._attributes, "recoverable_days"): - return self._attributes.recoverable_days - return None - - @property - def recovery_level(self) -> Optional[models.DeletionRecoveryLevel]: - """The deletion recovery level currently in effect for the certificate. - - :returns: The deletion recovery level currently in effect for the certificate. - :rtype: models.DeletionRecoveryLevel or None - """ - return self._attributes.recovery_level if self._attributes else None - - @property - def vault_url(self) -> str: - """The URL of the vault containing the certificate. - - :returns: The URL of the vault containing the certificate. - :rtype: str - """ - return self._vault_id.vault_url - - @property - def x509_thumbprint(self) -> bytes: - """The certificate's thumbprint, in bytes. - - To get the thumbprint as a hexadecimal string, call ``.hex()`` on this property. - - :return: The certificate's thumbprint, in bytes. - :rtype: bytes - """ - return self._x509_thumbprint - - @property - def tags(self) -> Optional[Dict[str, str]]: - """Application specific metadata in the form of key-value pairs. - - :returns: A dictionary of tags attached to the certificate. - :rtype: dict[str, str] or None - """ - return self._tags - - @property - def version(self) -> Optional[str]: - """The version of the certificate. - - :returns: The version of the certificate. - :rtype: str or None - """ - return self._vault_id.version - - @property - def preserve_order(self) -> Optional[bool]: - """Whether the certificate order should be preserved. - - :returns: Specifies whether the certificate chain preserves its original order. The default value is False, - which sets the leaf certificate at index 0. - :rtype: bool or None - """ - return self._preserve_order - - -class KeyVaultCertificate(object): - """Consists of a certificate and its attributes - - :param policy: The management policy for the certificate. - :type policy: ~azure.keyvault.certificates.CertificatePolicy or None - :param properties: The certificate's properties. - :type properties: ~azure.keyvault.certificates.CertificateProperties or None - :param cer: CER contents of the X509 certificate. - :type cer: bytearray or None - """ - - def __init__( - self, - policy: "Optional[CertificatePolicy]" = None, - properties: Optional[CertificateProperties] = None, - cer: Optional[bytearray] = None, - **kwargs: Any, - ) -> None: - self._properties = properties - self._key_id = kwargs.get("key_id", None) - self._secret_id = kwargs.get("secret_id", None) - self._policy = policy - self._cer = cer - - def __repr__(self) -> str: - return f""[:1024] - - @classmethod - def _from_certificate_bundle(cls, certificate_bundle: models.CertificateBundle) -> "KeyVaultCertificate": - # pylint:disable=protected-access - - if certificate_bundle.policy: - policy: Optional[CertificatePolicy] = CertificatePolicy._from_certificate_policy_bundle( - certificate_bundle.policy - ) - else: - policy = None - - return cls( - properties=CertificateProperties._from_certificate_bundle(certificate_bundle), - key_id=certificate_bundle.kid, - secret_id=certificate_bundle.sid, - policy=policy, - cer=certificate_bundle.cer, # type: ignore - ) - - @property - def id(self) -> Optional[str]: - """The certificate identifier. - - :returns: The certificate identifier. - :rtype: str or None - """ - return self._properties.id if self._properties else None - - @property - def name(self) -> Optional[str]: - """The name of the certificate. - - :returns: The name of the certificate. - :rtype: str or None - """ - return self._properties.name if self._properties else None - - @property - def properties(self) -> Optional[CertificateProperties]: - """The certificate's properties. - - :returns: The certificate's properties. - :rtype: ~azure.keyvault.certificates.CertificateProperties or None - """ - return self._properties - - @property - def key_id(self) -> Optional[str]: - """The ID of the key associated with the certificate. - - :returns: The ID of the key associated with the certificate. - :rtype: str or None - """ - return self._key_id - - @property - def secret_id(self) -> Optional[str]: - """The ID of the secret associated with the certificate. - - :returns: The ID of the secret associated with the certificate. - :rtype: str or None - """ - return self._secret_id - - @property - def policy(self) -> "Optional[CertificatePolicy]": - """The management policy of the certificate. - - :returns: The management policy of the certificate. - :rtype: ~azure.keyvault.certificates.CertificatePolicy or None - """ - return self._policy - - @property - def cer(self) -> Optional[bytearray]: - """The CER contents of the certificate. - - :returns: The CER contents of the certificate. - :rtype: bytearray or None - """ - return self._cer - - -class KeyVaultCertificateIdentifier(object): - """Information about a KeyVaultCertificate parsed from a certificate ID. - - :param str source_id: the full original identifier of a certificate - - :raises ValueError: if the certificate ID is improperly formatted - - Example: - .. literalinclude:: ../tests/test_parse_id.py - :start-after: [START parse_key_vault_certificate_id] - :end-before: [END parse_key_vault_certificate_id] - :language: python - :caption: Parse a certificate's ID - :dedent: 8 - """ - - def __init__(self, source_id: str) -> None: - self._resource_id = parse_key_vault_id(source_id) - - @property - def source_id(self) -> str: - return self._resource_id.source_id - - @property - def vault_url(self) -> str: - return self._resource_id.vault_url - - @property - def name(self) -> str: - return self._resource_id.name - - @property - def version(self) -> Optional[str]: - return self._resource_id.version - - -class CertificateOperation(object): - # pylint:disable=too-many-instance-attributes - """A certificate operation is returned in case of long running requests. - - :param cert_operation_id: The certificate id. - :type cert_operation_id: str or None - :param issuer_name: Name of the operation's issuer object or reserved names. - :type issuer_name: str or ~azure.keyvault.certificates.WellKnownIssuerNames or None - :param certificate_type: Type of certificate requested from the issuer provider. - :type certificate_type: str or None - :param certificate_transparency: Indicates if the certificate this operation is running for is published to - certificate transparency logs. Defaults to False. - :type certificate_transparency: bool or None - :param csr: The certificate signing request (CSR) that is being used in the certificate operation. - :type csr: bytes or None - :param cancellation_requested: Indicates if cancellation was requested on the certificate operation. Defaults - to False. - :type cancellation_requested: bool or None - :param status: Status of the certificate operation. - :type status: str or None - :param status_details: The status details of the certificate operation - :type status_details: str or None - :param error: Error encountered, if any, during the certificate operation. - :type error: ~azure.keyvault.certificates.CertificateOperationError or None - :param target: Location which contains the result of the certificate operation. - :type target: str or None - :param request_id: Identifier for the certificate operation. - :type request_id: str or None - :param bool preserve_order: Specifies whether the certificate chain preserves its original order. The default - value is False, which sets the leaf certificate at index 0. - """ - - def __init__( - self, - cert_operation_id: Optional[str] = None, - issuer_name: Optional[Union[str, WellKnownIssuerNames]] = None, - certificate_type: Optional[str] = None, - certificate_transparency: Optional[bool] = False, - csr: Optional[bytes] = None, - cancellation_requested: Optional[bool] = False, - status: Optional[str] = None, - status_details: Optional[str] = None, - error: Optional[CertificateOperationError] = None, - target: Optional[str] = None, - request_id: Optional[str] = None, - preserve_order: Optional[bool] = False, - ) -> None: - self._id = cert_operation_id - self._vault_id = parse_key_vault_id(cert_operation_id) if cert_operation_id else None - self._issuer_name = issuer_name - self._certificate_type = certificate_type - self._certificate_transparency = certificate_transparency - self._csr = csr - self._cancellation_requested = cancellation_requested - self._status = status - self._status_details = status_details - self._error = error - self._target = target - self._request_id = request_id - self._preserve_order = preserve_order - - def __repr__(self) -> str: - return f""[:1024] - - @classmethod - def _from_certificate_operation_bundle( - cls, certificate_operation_bundle: models.CertificateOperation - ) -> "CertificateOperation": - - issuer_parameters = certificate_operation_bundle.issuer_parameters - return cls( - cert_operation_id=certificate_operation_bundle.id, - issuer_name=issuer_parameters.name if issuer_parameters else None, - certificate_type=( - certificate_operation_bundle.issuer_parameters.certificate_type - if certificate_operation_bundle.issuer_parameters - else None - ), - # 2016-10-01 IssuerParameters doesn't have certificate_transparency - certificate_transparency=getattr(issuer_parameters, "certificate_transparency", None), - csr=certificate_operation_bundle.csr, - cancellation_requested=certificate_operation_bundle.cancellation_requested, - status=certificate_operation_bundle.status, - status_details=certificate_operation_bundle.status_details, - error=( - CertificateOperationError._from_error_bundle( # pylint: disable=protected-access - certificate_operation_bundle.error - ) - if certificate_operation_bundle.error - else None - ), - target=certificate_operation_bundle.target, - request_id=certificate_operation_bundle.request_id, - preserve_order=certificate_operation_bundle.preserve_cert_order, - ) - - @property - def id(self) -> Optional[str]: - """The certificate ID. - - :returns: The certificate ID. - :rtype: str or None - """ - return self._id - - @property - def name(self) -> Optional[str]: - """The certificate name. - - :returns: The certificate name. - :rtype: str or None - """ - return self._vault_id.name if self._vault_id else None - - @property - def vault_url(self) -> Optional[str]: - """URL of the vault performing the certificate operation. - - :returns: URL of the vault performing the certificate operation. - :rtype: str or None - """ - return self._vault_id.vault_url if self._vault_id else None - - @property - def issuer_name(self) -> Union[str, WellKnownIssuerNames, None]: - """The name of the certificate issuer. - - :returns: The name of the certificate issuer. - :rtype: str or ~azure.keyvault.certificates.WellKnownIssuerNames or None - """ - return self._issuer_name - - @property - def certificate_type(self) -> Optional[str]: - """Type of certificate to be requested from the issuer provider. - - :returns: Type of certificate to be requested from the issuer provider. - :rtype: str or None - """ - return self._certificate_type - - @property - def certificate_transparency(self) -> Optional[bool]: - """Whether certificates generated under this policy should be published to certificate transparency logs. - - :returns: True if the certificates should be published to transparency logs; False otherwise. - :rtype: bool or None - """ - return self._certificate_transparency - - @property - def csr(self) -> Optional[bytes]: - """The certificate signing request that is being used in this certificate operation. - - :returns: The certificate signing request that is being used in this certificate operation. - :rtype: bytes or None - """ - return self._csr - - @property - def cancellation_requested(self) -> Optional[bool]: - """Whether cancellation was requested on the certificate operation. - - :returns: True if cancellation was requested; False otherwise. - :rtype: bool or None - """ - return self._cancellation_requested - - @property - def status(self) -> Optional[str]: - """The operation status. - - :returns: The operation status. - :rtype: str or None - """ - return self._status - - @property - def status_details(self) -> Optional[str]: - """Details of the operation status. - - :returns: Details of the operation status. - :rtype: str or None - """ - return self._status_details - - @property - def error(self) -> Optional[CertificateOperationError]: - """Any error associated with the certificate operation. - - :returns: Any error associated with the operation, as a - :class:`~azure.keyvault.certificates.CertificateOperationError`. - :rtype: ~azure.keyvault.certificates.CertificateOperationError or None""" - return self._error - - @property - def target(self) -> Optional[str]: - """Location which contains the result of the certificate operation. - - :returns: Location which contains the result of the certificate operation. - :rtype: str or None - """ - return self._target - - @property - def request_id(self) -> Optional[str]: - """Identifier for the certificate operation. - - :returns: Identifier for the certificate operation. - :rtype: str or None - """ - return self._request_id - - @property - def preserve_order(self) -> Optional[bool]: - """Whether the certificate order should be preserved. - - :returns: Specifies whether the certificate chain preserves its original order. The default value is False, - which sets the leaf certificate at index 0. - :rtype: bool or None - """ - return self._preserve_order - - -class CertificatePolicy(object): - """Management policy for a certificate. - - :param issuer_name: Optional. Name of the referenced issuer object or reserved names; for example, - :attr:`~azure.keyvault.certificates.WellKnownIssuerNames.self` or - :attr:`~azure.keyvault.certificates.WellKnownIssuerNames.unknown` - :type issuer_name: str or None - - :keyword subject: The subject name of the certificate. Should be a valid X509 distinguished name. Either subject or - one of the subject alternative name parameters are required for creating a certificate. This will be ignored - when importing a certificate; the subject will be parsed from the imported certificate. - :paramtype subject: str or None - :keyword san_emails: Subject alternative emails of the X509 object. Either subject or one of the subject alternative - name parameters are required for creating a certificate. - :paramtype san_emails: list[str] or None - :keyword san_dns_names: Subject alternative DNS names of the X509 object. Either subject or one of the subject - alternative name parameters are required for creating a certificate. - :paramtype san_dns_names: list[str] or None - :keyword san_user_principal_names: Subject alternative user principal names of the X509 object. Either subject or - one of the subject alternative name parameters are required for creating a certificate. - :paramtype san_user_principal_names: list[str] or None - :keyword san_ip_addresses: Subject alternative IP addresses of the X509 object. Supports IPv4 and IPv6. Either - subject or one of the subject alternative name parameters are required for creating a certificate. - :paramtype san_ip_addresses: list[str] or None - :keyword san_uris: Subject alternative URIs of the X509 object. Either subject or one of the subject alternative - name parameters are required for creating a certificate. - :paramtype san_uris: list[str] or None - :keyword exportable: Indicates if the private key can be exported. For valid values, see KeyType. - :paramtype exportable: bool or None - :keyword key_type: The type of key pair to be used for the certificate. - :paramtype key_type: str or ~azure.keyvault.certificates.KeyType or None - :keyword key_size: The key size in bits. For example: 2048, 3072, or 4096 for RSA. - :paramtype key_size: int or None - :keyword reuse_key: Indicates if the same key pair will be used on certificate renewal. - :paramtype reuse_key: bool or None - :keyword key_curve_name: Elliptic curve name. For valid values, see KeyCurveName. - :paramtype key_curve_name: str or ~azure.keyvault.certificates.KeyCurveName or None - :keyword enhanced_key_usage: The extended ways the key of the certificate can be used. - :paramtype enhanced_key_usage: list[str] or None - :keyword key_usage: List of key usages. - :paramtype key_usage: list[str or ~azure.keyvault.certificates.KeyUsageType] or None - :keyword content_type: The media type (MIME type) of the secret backing the certificate. If not specified, - :attr:`CertificateContentType.pkcs12` is assumed. - :paramtype content_type: str or ~azure.keyvault.certificates.CertificateContentType or None - :keyword validity_in_months: The duration that the certificate is valid in months. - :paramtype validity_in_months: int or None - :keyword lifetime_actions: Actions that will be performed by Key Vault over the lifetime of a certificate. - :paramtype lifetime_actions: list[~azure.keyvault.certificates.LifetimeAction] or None - :keyword certificate_type: Type of certificate to be requested from the issuer provider. - :paramtype certificate_type: str or None - :keyword certificate_transparency: Indicates if the certificates generated under this policy should be - published to certificate transparency logs. - :paramtype certificate_transparency: bool or None - """ - - # pylint:disable=too-many-instance-attributes - def __init__( - self, - issuer_name: Optional[str] = None, - **kwargs: Any, - ) -> None: - self._issuer_name = issuer_name - self._subject = kwargs.pop("subject", None) - self._attributes = kwargs.pop("attributes", None) - self._exportable = kwargs.pop("exportable", None) - self._key_type = kwargs.pop("key_type", None) - self._key_size = kwargs.pop("key_size", None) - self._reuse_key = kwargs.pop("reuse_key", None) - self._key_curve_name = kwargs.pop("key_curve_name", None) - self._enhanced_key_usage = kwargs.pop("enhanced_key_usage", None) - self._key_usage = kwargs.pop("key_usage", None) - self._content_type = kwargs.pop("content_type", None) - self._validity_in_months = kwargs.pop("validity_in_months", None) - self._lifetime_actions = kwargs.pop("lifetime_actions", None) - self._certificate_type = kwargs.pop("certificate_type", None) - self._certificate_transparency = kwargs.pop("certificate_transparency", None) - self._san_emails = kwargs.pop("san_emails", None) or None - self._san_dns_names = kwargs.pop("san_dns_names", None) or None - self._san_user_principal_names = kwargs.pop("san_user_principal_names", None) or None - self._san_ip_addresses = kwargs.pop("san_ip_addresses", None) or None - self._san_uris = kwargs.pop("san_uris", None) or None - - @classmethod - def get_default(cls) -> "CertificatePolicy": - return cls(issuer_name=WellKnownIssuerNames.self, subject="CN=DefaultPolicy") - - def __repr__(self) -> str: - return f""[:1024] - - def _to_certificate_policy_bundle(self) -> models.CertificatePolicy: - if self.issuer_name or self.certificate_type or self.certificate_transparency: - issuer_parameters: Optional[models.IssuerParameters] = models.IssuerParameters( - name=self.issuer_name, - certificate_type=self.certificate_type, - certificate_transparency=self.certificate_transparency, # 2016-10-01 model will ignore this - ) - else: - issuer_parameters = None - - if self.enabled is not None or self.created_on is not None or self.updated_on is not None: - attributes = models.CertificateAttributes( - { - "enabled": self.enabled, - "created": self.created_on, - "updated": self.updated_on, - } - ) - else: - attributes = None - - if self.lifetime_actions: - lifetime_actions = [] - for lifetime_action in self.lifetime_actions: - lifetime_actions.append( - models.LifetimeAction( - trigger=models.Trigger( - lifetime_percentage=lifetime_action.lifetime_percentage, - days_before_expiry=lifetime_action.days_before_expiry, - ), - action=models.Action(action_type=lifetime_action.action), - ) - ) - else: - lifetime_actions = None # type: ignore - - # pylint:disable=too-many-boolean-expressions - if ( - self.subject - or self.enhanced_key_usage - or self.key_usage - or self.san_emails - or self.san_user_principal_names - or self.san_dns_names - or self.san_ip_addresses - or self.san_uris - or self.validity_in_months - ): - if self.key_usage: - key_usage: Optional[List[Union[str, KeyUsageType]]] = [ - k.value if not isinstance(k, str) else k for k in self.key_usage - ] - else: - key_usage = None - - x509_properties: Optional[models.X509CertificateProperties] = models.X509CertificateProperties( - subject=self.subject, - ekus=self.enhanced_key_usage, - subject_alternative_names=models.SubjectAlternativeNames( - emails=self.san_emails, - upns=self.san_user_principal_names, - dns_names=self.san_dns_names, - ip_addresses=self.san_ip_addresses, - uris=self.san_uris, - ), - key_usage=key_usage, - validity_in_months=self.validity_in_months, - ) - else: - x509_properties = None - - if self.exportable or self.key_type or self.key_size or self.reuse_key or self.key_curve_name: - key_properties: Optional[models.KeyProperties] = models.KeyProperties( - exportable=self.exportable, - key_type=self.key_type, - key_size=self.key_size, - reuse_key=self.reuse_key, - curve=self.key_curve_name, - ) - else: - key_properties = None - - if self.content_type: - secret_properties: Optional[models.SecretProperties] = models.SecretProperties( - content_type=self.content_type - ) - else: - secret_properties = None - - policy_bundle = models.CertificatePolicy( - key_properties=key_properties, - secret_properties=secret_properties, - x509_certificate_properties=x509_properties, - lifetime_actions=lifetime_actions, - issuer_parameters=issuer_parameters, - attributes=attributes, - ) - return policy_bundle - - @classmethod - def _from_certificate_policy_bundle( - cls, certificate_policy_bundle: Optional[models.CertificatePolicy] - ) -> "CertificatePolicy": - if certificate_policy_bundle is None: - return cls() - - if certificate_policy_bundle.lifetime_actions: - lifetime_actions: Optional[List[LifetimeAction]] = [ - LifetimeAction( - action=CertificatePolicyAction(item.action.action_type) if item.action else None, - lifetime_percentage=item.trigger.lifetime_percentage if item.trigger else None, - days_before_expiry=item.trigger.days_before_expiry if item.trigger else None, - ) - for item in certificate_policy_bundle.lifetime_actions - ] - else: - lifetime_actions = None - x509_certificate_properties = certificate_policy_bundle.x509_certificate_properties - if x509_certificate_properties and x509_certificate_properties.key_usage: - key_usage: Optional[List[KeyUsageType]] = [KeyUsageType(k) for k in x509_certificate_properties.key_usage] - else: - key_usage = None - key_properties = certificate_policy_bundle.key_properties - curve_name = getattr(key_properties, "curve", None) # missing from 2016-10-01 KeyProperties - if curve_name: - curve_name = KeyCurveName(curve_name) - - issuer_parameters = certificate_policy_bundle.issuer_parameters - return cls( - issuer_name=issuer_parameters.name if issuer_parameters else None, - subject=(x509_certificate_properties.subject if x509_certificate_properties else None), - certificate_type=issuer_parameters.certificate_type if issuer_parameters else None, - # 2016-10-01 IssuerParameters doesn't have certificate_transparency - certificate_transparency=getattr(issuer_parameters, "certificate_transparency", None), - lifetime_actions=lifetime_actions, - exportable=key_properties.exportable if key_properties else None, - key_type=KeyType(key_properties.key_type) if key_properties and key_properties.key_type else None, - key_size=key_properties.key_size if key_properties else None, - reuse_key=key_properties.reuse_key if key_properties else None, - key_curve_name=curve_name, - enhanced_key_usage=x509_certificate_properties.ekus if x509_certificate_properties else None, - key_usage=key_usage, - content_type=( - CertificateContentType(certificate_policy_bundle.secret_properties.content_type) - if certificate_policy_bundle.secret_properties - and certificate_policy_bundle.secret_properties.content_type - else None - ), - attributes=certificate_policy_bundle.attributes, - san_emails=( - x509_certificate_properties.subject_alternative_names.emails - if x509_certificate_properties and x509_certificate_properties.subject_alternative_names - else None - ), - san_user_principal_names=( - x509_certificate_properties.subject_alternative_names.upns - if x509_certificate_properties and x509_certificate_properties.subject_alternative_names - else None - ), - san_dns_names=( - x509_certificate_properties.subject_alternative_names.dns_names - if x509_certificate_properties and x509_certificate_properties.subject_alternative_names - else None - ), - san_ip_addresses=( - x509_certificate_properties.subject_alternative_names.ip_addresses - if x509_certificate_properties and x509_certificate_properties.subject_alternative_names - else None - ), - san_uris=( - x509_certificate_properties.subject_alternative_names.uris - if x509_certificate_properties and x509_certificate_properties.subject_alternative_names - else None - ), - validity_in_months=( - x509_certificate_properties.validity_in_months if x509_certificate_properties else None - ), - ) - - @property - def exportable(self) -> Optional[bool]: - """Whether the private key can be exported. - - :returns: True if the private key can be exported; False otherwise. - :rtype: bool or None - """ - return self._exportable - - @property - def key_type(self) -> Optional[KeyType]: - """The type of key pair to be used for the certificate. - - :returns: The type of key pair to be used for the certificate. - :rtype: ~azure.keyvault.certificates.KeyType or None - """ - return self._key_type - - @property - def key_size(self) -> Optional[int]: - """The key size in bits. - - :returns: The key size in bits. - :rtype: int or None - """ - return self._key_size - - @property - def reuse_key(self) -> Optional[bool]: - """Whether the same key pair will be used on certificate renewal. - - :returns: True if the same key pair will be used on certificate renewal; False otherwise. - :rtype: bool or None - """ - return self._reuse_key - - @property - def key_curve_name(self) -> Optional[KeyCurveName]: - """Elliptic curve name. - - :returns: Elliptic curve name. - :rtype: ~azure.keyvault.certificates.KeyCurveName or None - """ - return self._key_curve_name - - @property - def enhanced_key_usage(self) -> Optional[List[str]]: - """The enhanced key usage. - - :returns: The enhanced key usage. - :rtype: list[str] or None - """ - return self._enhanced_key_usage - - @property - def key_usage(self) -> Optional[List[KeyUsageType]]: - """List of key usages. - - :returns: List of key usages. - :rtype: list[~azure.keyvault.certificates.KeyUsageType] or None - """ - return self._key_usage - - @property - def content_type(self) -> Optional[CertificateContentType]: - """The media type (MIME type). - - :returns: The media type (MIME type). - :rtype: ~azure.keyvault.certificates.CertificateContentType or None - """ - return self._content_type - - @property - def subject(self) -> Optional[str]: - """The subject name of the certificate. - - :returns: The subject name of the certificate. - :rtype: str or None - """ - return self._subject - - @property - def san_emails(self) -> Optional[List[str]]: - """The subject alternative email addresses. - - :returns: The subject alternative email addresses, as a list. - :rtype: list[str] or None - """ - return self._san_emails - - @property - def san_dns_names(self) -> Optional[List[str]]: - """The subject alternative domain names. - - :returns: The subject alternative domain names, as a list. - :rtype: list[str] or None - """ - return self._san_dns_names - - @property - def san_user_principal_names(self) -> Optional[List[str]]: - """The subject alternative user principal names. - - :returns: The subject alternative user principal names, as a list. - :rtype: list[str] or None - """ - return self._san_user_principal_names - - @property - def san_ip_addresses(self) -> Optional[List[str]]: - """The subject alternative IP addresses. Supports IPv4 and IPv6. - - :returns: The subject alternative IP addresses, as a list. - :rtype: list[str] or None - """ - return self._san_ip_addresses - - @property - def san_uris(self) -> Optional[List[str]]: - """The subject alternative URIs. - - :returns: The subject alternative URIs, as a list. - :rtype: list[str] or None - """ - return self._san_uris - - @property - def validity_in_months(self) -> Optional[int]: - """The duration that the certificate is valid for in months. - - :returns: The duration that the certificate is valid for in months. - :rtype: int or None - """ - return self._validity_in_months - - @property - def lifetime_actions(self) -> "Optional[List[LifetimeAction]]": - """Actions and their triggers that will be performed by Key Vault over the lifetime of the certificate. - - :returns: Actions and their triggers that will be performed by Key Vault over the lifetime of the certificate. - :rtype: list[~azure.keyvault.certificates.LifetimeAction] or None - """ - return self._lifetime_actions - - @property - def issuer_name(self) -> Optional[str]: - """Name of the referenced issuer object or reserved names for the issuer of the certificate. - - :returns: Name of the referenced issuer object or reserved names for the issuer of the certificate. - :rtype: str or None - """ - return self._issuer_name - - @property - def certificate_type(self) -> Optional[str]: - """Type of certificate requested from the issuer provider. - - :returns: Type of certificate requested from the issuer provider. - :rtype: str or None - """ - return self._certificate_type - - @property - def certificate_transparency(self) -> Optional[bool]: - """Whether the certificates generated under this policy should be published to certificate transparency logs. - - :returns: True if the certificates should be published to transparency logs; False otherwise. - :rtype: bool or None - """ - return self._certificate_transparency - - @property - def enabled(self) -> Optional[bool]: - """Whether the certificate is enabled or not. - - :returns: True if the certificate is enabled; False otherwise. - :rtype: bool or None - """ - return self._attributes.enabled if self._attributes else None - - @property - def created_on(self) -> Optional[datetime]: - """The datetime when the certificate is created. - - :returns: The datetime when the certificate is created. - :rtype: ~datetime.datetime or None - """ - return self._attributes.created if self._attributes else None - - @property - def updated_on(self) -> Optional[datetime]: - """The datetime when the certificate was last updated. - - :returns: The datetime when the certificate was last updated. - :rtype: ~datetime.datetime or None - """ - return self._attributes.updated if self._attributes else None - - -class CertificateContact(object): - """The contact information for the vault certificates. - - :param email: Email address of a contact for the certificate. - :type email: str or None - :param name: Name of a contact for the certificate. - :type name: str or None - :param phone: phone number of a contact for the certificate. - :type phone: str or None - """ - - def __init__(self, email: Optional[str] = None, name: Optional[str] = None, phone: Optional[str] = None) -> None: - self._email = email - self._name = name - self._phone = phone - - def __repr__(self) -> str: - return f"CertificateContact(email={self.email}, name={self.name}, phone={self.phone})"[:1024] - - def _to_certificate_contacts_item(self) -> models.Contact: - return models.Contact(email_address=self.email, name=self.name, phone=self.phone) - - @classmethod - def _from_certificate_contacts_item(cls, contact_item: models.Contact) -> "CertificateContact": - return cls(email=contact_item.email_address, name=contact_item.name, phone=contact_item.phone) - - @property - def email(self) -> Optional[str]: - """Email address of a contact for the certificate. - - :rtype: str or None""" - return self._email - - @property - def name(self) -> Optional[str]: - """Name of a contact for the certificate. - - :rtype: str or None""" - return self._name - - @property - def phone(self) -> Optional[str]: - """Phone number of a contact for the certificate. - - :rtype: str or None""" - return self._phone - - -class IssuerProperties(object): - """The properties of an issuer containing the issuer metadata. - - :param provider: The issuer provider. - :type provider: str or None - """ - - def __init__(self, provider: Optional[str] = None, **kwargs: Any) -> None: - self._id = kwargs.pop("issuer_id", None) - self._vault_id = parse_key_vault_id(self._id) - self._provider = provider - - def __repr__(self) -> str: - return f"IssuerProperties(issuer_id={self.id}, provider={self.provider})"[:1024] - - @classmethod - def _from_issuer_item( - cls, issuer_item: Union[models.CertificateIssuerItem, models.IssuerBundle] - ) -> "IssuerProperties": - return cls(issuer_id=issuer_item.id, provider=issuer_item.provider) - - @property - def id(self) -> Optional[str]: - """The issuer ID. - - :returns: The issuer ID. - :rtype: str or None - """ - return self._id - - @property - def name(self) -> Optional[str]: - """The issuer name. - - :returns: The issuer name. - :rtype: str or None - """ - # Issuer name is listed under version under vault_id - return self._vault_id.version - - @property - def provider(self) -> Optional[str]: - """The issuer provider. - - :returns: The issuer provider. - :rtype: str or None - """ - return self._provider - - -class CertificateIssuer(object): - """The issuer for a Key Vault certificate. - - :param provider: The issuer provider - :type provider: str or None - :param attributes: The issuer attributes. - :type attributes: ~azure.keyvault.certificates._generated.models.IssuerAttributes or None - :param account_id: The username / account name / account id. - :type account_id: str or None - :param password: The password / secret / account key. - :type password: str or None - :param organization_id: The ID of the organization. - :type organization_id: str or None - :param admin_contacts: Details of the organization administrator. - :type admin_contacts: list[~azure.keyvault.certificates.AdministratorContact] or None - """ - - def __init__( - self, - provider: Optional[str], - attributes: Optional[models.IssuerAttributes] = None, - account_id: Optional[str] = None, - # [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Typedef, not string.")] - password: Optional[str] = None, - organization_id: Optional[str] = None, - admin_contacts: Optional[List[AdministratorContact]] = None, - **kwargs: Any, - ) -> None: - self._provider = provider - self._attributes = attributes - self._account_id = account_id - self._password = password - self._organization_id = organization_id - self._admin_contacts = admin_contacts - self._id = kwargs.pop("issuer_id", None) - self._vault_id = parse_key_vault_id(self._id) - - def __repr__(self) -> str: - return f""[:1024] - - @classmethod - def _from_issuer_bundle(cls, issuer_bundle: models.IssuerBundle) -> "CertificateIssuer": - admin_contacts = [] - admin_details = issuer_bundle.organization_details.admin_details if issuer_bundle.organization_details else None - if admin_details: - # pylint:disable=protected-access - for admin_detail in admin_details: - admin_contacts.append(AdministratorContact._from_admin_detail(admin_detail)) - return cls( - provider=IssuerProperties._from_issuer_item(issuer_bundle).provider, # pylint: disable=protected-access - attributes=issuer_bundle.attributes, - account_id=issuer_bundle.credentials.account_id if issuer_bundle.credentials else None, - password=issuer_bundle.credentials.password if issuer_bundle.credentials else None, - organization_id=issuer_bundle.organization_details.id if issuer_bundle.organization_details else None, - admin_contacts=admin_contacts, - issuer_id=issuer_bundle.id, - ) - - @property - def id(self) -> Optional[str]: - """The issuer ID. - - :returns: The issuer ID. - :rtype: str or None - """ - return self._id - - @property - def name(self) -> Optional[str]: - """The issuer name. - - :returns: The issuer name. - :rtype: str or None - """ - # Issuer name is listed under version under vault_id. - # This is because the id we pass to parse_key_vault_id has an extra segment, so where most cases the version of - # the general pattern is certificates/name/version, but here we have certificates/issuers/name/version. - # Issuers are not versioned. - return self._vault_id.version - - @property - def provider(self) -> Optional[str]: - """The issuer provider. - - :returns: The issuer provider. - :rtype: str or None - """ - return self._provider - - @property - def enabled(self) -> Optional[bool]: - """Whether the certificate is enabled or not. - - :returns: True if the certificate is enabled; False otherwise. - :rtype: bool or None - """ - return self._attributes.enabled if self._attributes else None - - @property - def created_on(self) -> Optional[datetime]: - """The datetime when the certificate is created. - - :returns: The datetime when the certificate is created. - :rtype: ~datetime.datetime or None - """ - return self._attributes.created if self._attributes else None - - @property - def updated_on(self) -> Optional[datetime]: - """The datetime when the certificate was last updated. - - :returns: The datetime when the certificate was last updated. - :rtype: ~datetime.datetime or None - """ - return self._attributes.updated if self._attributes else None - - @property - def account_id(self) -> Optional[str]: - """The username / account name / account id. - - :returns: The username / account name / account id. - :rtype: str or None - """ - return self._account_id - - @property - def password(self) -> Optional[str]: - """The password / secret / account key. - - :returns: The password / secret / account key. - :rtype: str or None - """ - return self._password - - @property - def organization_id(self) -> Optional[str]: - """The issuer organization ID. - - :returns: The issuer organization ID. - :rtype: str or None - """ - return self._organization_id - - @property - def admin_contacts(self) -> Optional[List[AdministratorContact]]: - """Contact details of the organization administrator(s) of this issuer. - - :returns: Contact details of the organization administrator(s) of this issuer. - :rtype: list[~azure.keyvault.certificates.AdministratorContact] or None - """ - return self._admin_contacts - - -class LifetimeAction(object): - """Action and its trigger that will be performed by certificate Vault over the lifetime of a certificate. - - :param action: The type of the action. For valid values, see CertificatePolicyAction - :type action: str or ~azure.keyvault.certificates.CertificatePolicyAction or None - :param lifetime_percentage: Percentage of lifetime at which to trigger. Value should be between 1 and 99. - :type lifetime_percentage: int or None - :param days_before_expiry: Days before expiry to attempt renewal. Value should be between 1 and - `validity_in_months` multiplied by 27. I.e., if validity_in_months is 36, then value should be between 1 and 972 - (36 * 27). - :type days_before_expiry: int or None - """ - - def __init__( - self, - action: Union[str, CertificatePolicyAction, None], - lifetime_percentage: Optional[int] = None, - days_before_expiry: Optional[int] = None, - ) -> None: - self._lifetime_percentage = lifetime_percentage - self._days_before_expiry = days_before_expiry - self._action = action - - def __repr__(self) -> str: - result = ( - f"LifetimeAction(action={self.action}, lifetime_percentage={self.lifetime_percentage}, " - + f"days_before_expiry={self.days_before_expiry})" - ) - return result[:1024] - - @property - def lifetime_percentage(self) -> Optional[int]: - """Percentage of lifetime at which to trigger. - - :returns: Percentage of lifetime at which to trigger. - :rtype: int or None - """ - return self._lifetime_percentage - - @property - def days_before_expiry(self) -> Optional[int]: - """Days before expiry to attempt renewal. - - :returns: Days before expiry to attempt renewal. - :rtype: int or None - """ - return self._days_before_expiry - - @property - def action(self) -> Union[str, CertificatePolicyAction, None]: - """The type of action that will be executed; see :class:`~azure.keyvault.certificates.CertificatePolicyAction`. - - :returns: The type of action that will be executed; see - :class:`~azure.keyvault.certificates.CertificatePolicyAction`. - :rtype: str or ~azure.keyvault.certificates.CertificatePolicyAction or None - """ - return self._action - - -class DeletedCertificate(KeyVaultCertificate): - """A deleted Certificate consisting of its previous ID, attributes, tags, and information on when it will be purged. - - :param properties: Properties of the deleted certificate. - :type properties: ~azure.keyvault.certificates.CertificateProperties - :param policy: The management policy of the deleted certificate. - :type policy: ~azure.keyvault.certificates.CertificatePolicy or None - :param cer: CER contents of the X509 certificate. - :type cer: bytearray or None - - :keyword deleted_on: The time when the certificate was deleted, in UTC. - :paramtype deleted_on: ~datetime.datetime or None - :keyword recovery_id: The url of the recovery object, used to identify and recover the deleted certificate. - :paramtype recovery_id: str or None - :keyword scheduled_purge_date: The time when the certificate is scheduled to be purged, in UTC. - :paramtype scheduled_purge_date: ~datetime.datetime or None - """ - - def __init__( - self, - properties: Optional[CertificateProperties] = None, - policy: Optional[CertificatePolicy] = None, - cer: Optional[bytearray] = None, - **kwargs: Any, - ) -> None: - super(DeletedCertificate, self).__init__(properties=properties, policy=policy, cer=cer, **kwargs) - self._deleted_on = kwargs.get("deleted_on", None) - self._recovery_id = kwargs.get("recovery_id", None) - self._scheduled_purge_date = kwargs.get("scheduled_purge_date", None) - - def __repr__(self) -> str: - return f""[:1024] - - @classmethod - def _from_deleted_certificate_item( - cls, deleted_certificate_item: models.DeletedCertificateItem - ) -> "DeletedCertificate": - return cls( - properties=CertificateProperties._from_certificate_item( # pylint: disable=protected-access - deleted_certificate_item - ), - key_id=None, - secret_id=None, - policy=None, - cer=None, - deleted_on=deleted_certificate_item.deleted_date, - recovery_id=deleted_certificate_item.recovery_id, - scheduled_purge_date=deleted_certificate_item.scheduled_purge_date, - ) - - @classmethod - def _from_deleted_certificate_bundle( - cls, deleted_certificate_bundle: models.DeletedCertificateBundle - ) -> "DeletedCertificate": - # pylint:disable=protected-access - return cls( - properties=CertificateProperties._from_certificate_bundle(deleted_certificate_bundle), - key_id=deleted_certificate_bundle.kid, - secret_id=deleted_certificate_bundle.sid, - policy=CertificatePolicy._from_certificate_policy_bundle(deleted_certificate_bundle.policy), - cer=deleted_certificate_bundle.cer, # type: ignore - deleted_on=deleted_certificate_bundle.deleted_date, - recovery_id=deleted_certificate_bundle.recovery_id, - scheduled_purge_date=deleted_certificate_bundle.scheduled_purge_date, - ) - - @property - def deleted_on(self) -> Optional[datetime]: - """The datetime when the certificate was deleted. - - :returns: The datetime when the certificate was deleted. - :rtype: ~datetime.datetime or None - """ - return self._deleted_on - - @property - def recovery_id(self) -> Optional[str]: - """The URL of the recovery object, used to identify and recover the deleted certificate. - - :returns: The URL of the recovery object, used to identify and recover the deleted certificate. - :rtype: str or None - """ - return self._recovery_id - - @property - def scheduled_purge_date(self) -> Optional[datetime]: - """The datetime when the certificate is scheduled to be purged. - - :returns: The datetime when the certificate is scheduled to be purged. - :rtype: ~datetime.datetime or None - """ - return self._scheduled_purge_date diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_polling.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_polling.py deleted file mode 100644 index 7d1f955f3313..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_polling.py +++ /dev/null @@ -1,56 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import Any, Callable, cast, Optional, Union - -from azure.core.pipeline import PipelineResponse -from azure.core.pipeline.transport import HttpTransport -from azure.core.polling import PollingMethod -from azure.keyvault.certificates._models import KeyVaultCertificate, CertificateOperation - - -class CreateCertificatePoller(PollingMethod): - def __init__( - self, pipeline_response: PipelineResponse, get_certificate_command: Callable, interval: int = 5 - ) -> None: - self._pipeline_response = pipeline_response - self._command: Optional[Callable] = None - self._resource: Optional[Union[CertificateOperation, KeyVaultCertificate]] = None - self._pending_certificate_op: Optional[CertificateOperation] = None - self._get_certificate_command = get_certificate_command - self._polling_interval = interval - - def _update_status(self) -> None: - self._pending_certificate_op = self._command() if self._command else None - - def initialize(self, client: Any, initial_response: Any, _: Any) -> None: - self._command = client - self._pending_certificate_op = initial_response - - def run(self) -> None: - while not self.finished(): - self._update_status() - if not self.finished(): - # We should always ask the client's transport to sleep, instead of sleeping directly - transport: HttpTransport = cast(HttpTransport, self._pipeline_response.context.transport) - transport.sleep(self._polling_interval) - operation = self._pending_certificate_op - if operation and operation.status and operation.status.lower() == "completed": - self._resource = self._get_certificate_command() - else: - self._resource = self._pending_certificate_op - - def finished(self) -> bool: - operation = self._pending_certificate_op - if operation and operation.issuer_name and operation.issuer_name.lower() == "unknown": - # Because we've finished, self._resource won't be set by the run method; set it here so we don't return None - self._resource = self._pending_certificate_op - return True - return self._pending_certificate_op.status.lower() != "inprogress" # type: ignore - - def resource(self) -> Union[KeyVaultCertificate, CertificateOperation]: - return self._resource # type: ignore - - def status(self) -> str: - return self._pending_certificate_op.status.lower() # type: ignore diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_sdk_moniker.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_sdk_moniker.py deleted file mode 100644 index f7ac28c79e91..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_sdk_moniker.py +++ /dev/null @@ -1,7 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from ._version import VERSION - -SDK_MONIKER = f"keyvault-certificates/{VERSION}" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py deleted file mode 100644 index cb088e31ad23..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py +++ /dev/null @@ -1,78 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import Optional -from urllib import parse - -from .challenge_auth_policy import ChallengeAuthPolicy -from .client_base import KeyVaultClientBase -from .http_challenge import HttpChallenge -from . import http_challenge_cache - -HttpChallengeCache = http_challenge_cache # to avoid aliasing pylint error (C4745) - - -__all__ = [ - "ChallengeAuthPolicy", - "HttpChallenge", - "HttpChallengeCache", - "KeyVaultClientBase", -] - - -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: str, - vault_url: str, - name: str, - version: "Optional[str]" = None, - ) -> None: - self.source_id = source_id - self.vault_url = vault_url - self.name = name - self.version = version - - -def parse_key_vault_id(source_id: str) -> KeyVaultResourceId: - try: - parsed_uri = parse.urlparse(source_id) - except Exception as exc: - raise ValueError(f"'{source_id}' is not a valid ID") from exc - if not (parsed_uri.scheme and parsed_uri.hostname): - raise ValueError(f"'{source_id}' is not a valid ID") - - path = list(filter(None, parsed_uri.path.split("/"))) - - if len(path) < 2 or len(path) > 3: - raise ValueError(f"'{source_id}' is not a valid ID") - - vault_url = f"{parsed_uri.scheme}://{parsed_uri.hostname}" - if parsed_uri.port: - vault_url += f":{parsed_uri.port}" - - return KeyVaultResourceId( - source_id=source_id, - vault_url=vault_url, - name=path[1], - version=path[2] if len(path) == 3 else None, - ) - - -try: - # pylint:disable=unused-import - from .async_challenge_auth_policy import AsyncChallengeAuthPolicy - from .async_client_base import AsyncKeyVaultClientBase - - __all__.extend(["AsyncChallengeAuthPolicy", "AsyncKeyVaultClientBase"]) -except (SyntaxError, ImportError): - pass diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling.py deleted file mode 100644 index ff0c398bba6d..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling.py +++ /dev/null @@ -1,136 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import threading -import uuid -from typing import Any, Callable, cast, Optional - -from azure.core.exceptions import ResourceNotFoundError, HttpResponseError -from azure.core.pipeline import PipelineResponse -from azure.core.pipeline.transport import HttpTransport -from azure.core.polling import PollingMethod, LROPoller, NoPolling - -from azure.core.tracing.decorator import distributed_trace -from azure.core.tracing.common import with_current_context - - -class KeyVaultOperationPoller(LROPoller): - """Poller for long running operations where calling result() doesn't wait for operation to complete. - - :param polling_method: The poller's polling method. - :type polling_method: ~azure.core.polling.PollingMethod - """ - - def __init__(self, polling_method: PollingMethod) -> None: - super(KeyVaultOperationPoller, self).__init__(None, None, lambda *_: None, NoPolling()) - self._polling_method = polling_method - - # pylint: disable=arguments-differ - def result(self) -> "Any": # type: ignore - """Returns a representation of the final resource without waiting for the operation to complete. - - :returns: The deserialized resource of the long running operation - :rtype: Any - - :raises ~azure.core.exceptions.HttpResponseError: Server problem with the query. - """ - return self._polling_method.resource() - - @distributed_trace - def wait(self, timeout: Optional[float] = None) -> None: - """Wait on the long running operation for a number of seconds. - - You can check if this call has ended with timeout with the "done()" method. - - :param float timeout: Period of time to wait for the long running operation to complete (in seconds). - - :raises ~azure.core.exceptions.HttpResponseError: Server problem with the query. - """ - - if not self._polling_method.finished(): - self._done = threading.Event() - self._thread = threading.Thread( - target=with_current_context(self._start), name=f"KeyVaultOperationPoller({uuid.uuid4()})" - ) - self._thread.daemon = True - self._thread.start() - - if self._thread is None: - return - self._thread.join(timeout=timeout) - try: - # Let's handle possible None in forgiveness here - raise self._exception # type: ignore - except TypeError: # Was None - pass - - -class DeleteRecoverPollingMethod(PollingMethod): - """Poller for deleting resources, and recovering deleted resources, in vaults with soft-delete enabled. - - This works by polling for the existence of the deleted or recovered resource. When a resource is deleted, Key Vault - immediately removes it from its collection. However, the resource will not immediately appear in the deleted - collection. Key Vault will therefore respond 404 to GET requests for the deleted resource; when it responds 2xx, - the resource exists in the deleted collection i.e. its deletion is complete. - - Similarly, while recovering a deleted resource, Key Vault will respond 404 to GET requests for the non-deleted - resource; when it responds 2xx, the resource exists in the non-deleted collection, i.e. its recovery is complete. - - :param pipeline_response: The operation's original pipeline response. - :type pipeline_response: PipelineResponse - :param command: A callable to invoke when polling. - :type command: Callable - :param final_resource: The final resource returned by the polling operation. - :type final_resource: Any - :param bool finished: Whether or not the polling operation is completed. - :param int interval: The polling interval, in seconds. - """ - - def __init__( - self, - pipeline_response: PipelineResponse, - command: Callable, - final_resource: Any, - finished: bool, - interval: int = 2, - ) -> None: - self._pipeline_response = pipeline_response - self._command = command - self._resource = final_resource - self._polling_interval = interval - self._finished = finished - - def _update_status(self) -> None: - try: - self._command() - self._finished = True - except ResourceNotFoundError: - pass - except HttpResponseError as e: - # If we are polling on get_deleted_* and we don't have get permissions, we will get - # ResourceNotFoundError until the resource is recovered, at which point we'll get a 403. - if e.status_code == 403: - self._finished = True - else: - raise - - def initialize(self, client: Any, initial_response: Any, deserialization_callback: Callable) -> None: - pass - - def run(self) -> None: - while not self.finished(): - self._update_status() - if not self.finished(): - # We should always ask the client's transport to sleep, instead of sleeping directly - transport: HttpTransport = cast(HttpTransport, self._pipeline_response.context.transport) - transport.sleep(self._polling_interval) - - def finished(self) -> bool: - return self._finished - - def resource(self) -> Any: - return self._resource - - def status(self) -> str: - return "finished" if self._finished else "polling" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling_async.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling_async.py deleted file mode 100644 index 16168229af08..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling_async.py +++ /dev/null @@ -1,80 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import Any, Callable, cast - -from azure.core.exceptions import ResourceNotFoundError, HttpResponseError -from azure.core.pipeline import PipelineResponse -from azure.core.pipeline.transport import AsyncHttpTransport -from azure.core.polling import AsyncPollingMethod - - -class AsyncDeleteRecoverPollingMethod(AsyncPollingMethod): - """Poller for deleting resources, and recovering deleted resources, in vaults with soft-delete enabled. - - This works by polling for the existence of the deleted or recovered resource. When a resource is deleted, Key Vault - immediately removes it from its collection. However, the resource will not immediately appear in the deleted - collection. Key Vault will therefore respond 404 to GET requests for the deleted resource; when it responds 2xx, - the resource exists in the deleted collection i.e. its deletion is complete. - - Similarly, while recovering a deleted resource, Key Vault will respond 404 to GET requests for the non-deleted - resource; when it responds 2xx, the resource exists in the non-deleted collection, i.e. its recovery is complete. - - :param pipeline_response: The operation's original pipeline response. - :type pipeline_response: PipelineResponse - :param command: An awaitable to invoke when polling. - :type command: Callable - :param final_resource: The final resource returned by the polling operation. - :type final_resource: Any - :param bool finished: Whether or not the polling operation is completed. - :param int interval: The polling interval, in seconds. - """ - - def __init__( - self, - pipeline_response: PipelineResponse, - command: Callable, - final_resource: Any, - finished: bool, - interval: int = 2, - ) -> None: - self._pipeline_response = pipeline_response - self._command = command - self._resource = final_resource - self._polling_interval = interval - self._finished = finished - - def initialize(self, client, initial_response, deserialization_callback): - pass - - async def _update_status(self) -> None: - try: - await self._command() - self._finished = True - except ResourceNotFoundError: - pass - except HttpResponseError as e: - # If we are polling on get_deleted_* and we don't have get permissions, we will get - # ResourceNotFoundError until the resource is recovered, at which point we'll get a 403. - if e.status_code == 403: - self._finished = True - else: - raise - - async def run(self) -> None: - while not self.finished(): - await self._update_status() - if not self.finished(): - # We should always ask the client's transport to sleep, instead of sleeping directly - transport: AsyncHttpTransport = cast(AsyncHttpTransport, self._pipeline_response.context.transport) - await transport.sleep(self._polling_interval) - - def finished(self) -> bool: - return self._finished - - def resource(self) -> Any: - return self._resource - - def status(self) -> str: - return "finished" if self._finished else "polling" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py deleted file mode 100644 index 0f84607e3ccd..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py +++ /dev/null @@ -1,262 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -"""Policy implementing Key Vault's challenge authentication protocol. - -Normally the protocol is only used for the client's first service request, upon which: -1. The challenge authentication policy sends a copy of the request, without authorization or content. -2. Key Vault responds 401 with a header (the 'challenge') detailing how the client should authenticate such a request. -3. The policy authenticates according to the challenge and sends the original request with authorization. - -The policy caches the challenge and thus knows how to authenticate future requests. However, authentication -requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the -protocol again. -""" - -from copy import deepcopy -import sys -import time -from typing import Any, Callable, cast, Optional, overload, TypeVar, Union -from urllib.parse import urlparse - -from typing_extensions import ParamSpec - -from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions -from azure.core.credentials_async import AsyncSupportsTokenInfo, AsyncTokenCredential, AsyncTokenProvider -from azure.core.pipeline import PipelineRequest, PipelineResponse -from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy -from azure.core.rest import AsyncHttpResponse, HttpRequest - -from .http_challenge import HttpChallenge -from . import http_challenge_cache as ChallengeCache -from .challenge_auth_policy import _enforce_tls, _has_claims, _update_challenge - -if sys.version_info < (3, 9): - from typing import Awaitable -else: - from collections.abc import Awaitable - - -P = ParamSpec("P") -T = TypeVar("T") - - -@overload -async def await_result(func: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwargs) -> T: ... - - -@overload -async def await_result(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: ... - - -async def await_result(func: Callable[P, Union[T, Awaitable[T]]], *args: P.args, **kwargs: P.kwargs) -> T: - """If func returns an awaitable, await it. - - :param func: The function to run. - :type func: callable - :param args: The positional arguments to pass to the function. - :type args: list - :rtype: any - :return: The result of the function - """ - result = func(*args, **kwargs) - if isinstance(result, Awaitable): - return await result - return result - - - -class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): - """Policy for handling HTTP authentication challenges. - - :param credential: An object which can provide an access token for the vault, such as a credential from - :mod:`azure.identity.aio` - :type credential: ~azure.core.credentials_async.AsyncTokenProvider - """ - - def __init__(self, credential: AsyncTokenProvider, *scopes: str, **kwargs: Any) -> None: - # Pass `enable_cae` so `enable_cae=True` is always passed through self.authorize_request - super().__init__(credential, *scopes, enable_cae=True, **kwargs) - self._credential: AsyncTokenProvider = credential - self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None - self._verify_challenge_resource = kwargs.pop("verify_challenge_resource", True) - self._request_copy: Optional[HttpRequest] = None - - async def send( - self, request: PipelineRequest[HttpRequest] - ) -> PipelineResponse[HttpRequest, AsyncHttpResponse]: - """Authorize request with a bearer token and send it to the next policy. - - We implement this method to account for the valid scenario where a Key Vault authentication challenge is - immediately followed by a CAE claims challenge. The base class's implementation would return the second 401 to - the caller, but we should handle that second challenge as well (and only return any third 401 response). - - :param request: The pipeline request object - :type request: ~azure.core.pipeline.PipelineRequest - :return: The pipeline response object - :rtype: ~azure.core.pipeline.PipelineResponse - """ - await await_result(self.on_request, request) - response: PipelineResponse[HttpRequest, AsyncHttpResponse] - try: - response = await self.next.send(request) - except Exception: # pylint:disable=broad-except - await await_result(self.on_exception, request) - raise - await await_result(self.on_response, request, response) - - if response.http_response.status_code == 401: - return await self.handle_challenge_flow(request, response) - return response - - async def handle_challenge_flow( - self, - request: PipelineRequest[HttpRequest], - response: PipelineResponse[HttpRequest, AsyncHttpResponse], - consecutive_challenge: bool = False, - ) -> PipelineResponse[HttpRequest, AsyncHttpResponse]: - """Handle the challenge flow of Key Vault and CAE authentication. - - :param request: The pipeline request object - :type request: ~azure.core.pipeline.PipelineRequest - :param response: The pipeline response object - :type response: ~azure.core.pipeline.PipelineResponse - :param bool consecutive_challenge: Whether the challenge is arriving immediately after another challenge. - Consecutive challenges can only be valid if a Key Vault challenge is followed by a CAE claims challenge. - True if the preceding challenge was a Key Vault challenge; False otherwise. - - :return: The pipeline response object - :rtype: ~azure.core.pipeline.PipelineResponse - """ - self._token = None # any cached token is invalid - if "WWW-Authenticate" in response.http_response.headers: - # If the previous challenge was a KV challenge and this one is too, return the 401 - claims_challenge = _has_claims(response.http_response.headers["WWW-Authenticate"]) - if consecutive_challenge and not claims_challenge: - return response - - request_authorized = await self.on_challenge(request, response) - if request_authorized: - # if we receive a challenge response, we retrieve a new token - # which matches the new target. In this case, we don't want to remove - # token from the request so clear the 'insecure_domain_change' tag - request.context.options.pop("insecure_domain_change", False) - try: - response = await self.next.send(request) - except Exception: # pylint:disable=broad-except - await await_result(self.on_exception, request) - raise - - # If consecutive_challenge == True, this could be a third consecutive 401 - if response.http_response.status_code == 401 and not consecutive_challenge: - # If the previous challenge wasn't from CAE, we can try this function one more time - if not claims_challenge: - return await self.handle_challenge_flow(request, response, consecutive_challenge=True) - await await_result(self.on_response, request, response) - return response - - - async def on_request(self, request: PipelineRequest) -> None: - _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if challenge: - # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. - if self._need_new_token(): - # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource - scope = challenge.get_scope() or challenge.get_resource() + "/.default" - await self._request_kv_token(scope, challenge) - - bearer_token = cast(Union[AccessToken, AccessTokenInfo], self._token).token - request.http_request.headers["Authorization"] = f"Bearer {bearer_token}" - return - - # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, - # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. - # on_challenge will parse that challenge, use the original request including the body, authorize the - # request, and tell super to send it again. - if request.http_request.content: - self._request_copy = request.http_request - bodiless_request = HttpRequest( - method=request.http_request.method, - url=request.http_request.url, - headers=deepcopy(request.http_request.headers), - ) - bodiless_request.headers["Content-Length"] = "0" - request.http_request = bodiless_request - - async def on_challenge(self, request: PipelineRequest, response: PipelineResponse) -> bool: - try: - # CAE challenges may not include a scope or tenant; cache from the previous challenge to use if necessary - old_scope: Optional[str] = None - old_tenant: Optional[str] = None - cached_challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if cached_challenge: - old_scope = cached_challenge.get_scope() or cached_challenge.get_resource() + "/.default" - old_tenant = cached_challenge.tenant_id - - challenge = _update_challenge(request, response) - # CAE challenges may not include a scope or tenant; use the previous challenge's values if necessary - if challenge.claims and old_scope: - challenge._parameters["scope"] = old_scope # pylint:disable=protected-access - challenge.tenant_id = old_tenant - # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource - scope = challenge.get_scope() or challenge.get_resource() + "/.default" - except ValueError: - return False - - if self._verify_challenge_resource: - resource_domain = urlparse(scope).netloc - if not resource_domain: - raise ValueError(f"The challenge contains invalid scope '{scope}'.") - - request_domain = urlparse(request.http_request.url).netloc - if not request_domain.lower().endswith(f".{resource_domain.lower()}"): - raise ValueError( - f"The challenge resource '{resource_domain}' does not match the requested domain. Pass " - "`verify_challenge_resource=False` to your client's constructor to disable this verification. " - "See https://aka.ms/azsdk/blog/vault-uri for more information." - ) - - # If we had created a request copy in on_request, use it now to send along the original body content - if self._request_copy: - request.http_request = self._request_copy - - # The tenant parsed from AD FS challenges is "adfs"; we don't actually need a tenant for AD FS authentication - # For AD FS we skip cross-tenant authentication per https://github.com/Azure/azure-sdk-for-python/issues/28648 - if challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs"): - await self.authorize_request(request, scope, claims=challenge.claims) - else: - await self.authorize_request( - request, scope, claims=challenge.claims, tenant_id=challenge.tenant_id - ) - - return True - - def _need_new_token(self) -> bool: - now = time.time() - refresh_on = getattr(self._token, "refresh_on", None) - return not self._token or (refresh_on and refresh_on <= now) or self._token.expires_on - now < 300 - - async def _request_kv_token(self, scope: str, challenge: HttpChallenge) -> None: - """Implementation of BearerTokenCredentialPolicy's _request_token method, but specific to Key Vault. - - :param str scope: The scope for which to request a token. - :param challenge: The challenge for the request being made. - :type challenge: HttpChallenge - """ - # Exclude tenant for AD FS authentication - exclude_tenant = challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs") - # The AsyncSupportsTokenInfo protocol needs TokenRequestOptions for token requests instead of kwargs - if hasattr(self._credential, "get_token_info"): - options: TokenRequestOptions = {"enable_cae": True} - if challenge.tenant_id and not exclude_tenant: - options["tenant_id"] = challenge.tenant_id - self._token = await cast(AsyncSupportsTokenInfo, self._credential).get_token_info(scope, options=options) - else: - if exclude_tenant: - self._token = await self._credential.get_token(scope, enable_cae=True) - else: - self._token = await cast(AsyncTokenCredential, self._credential).get_token( - scope, tenant_id=challenge.tenant_id, enable_cae=True - ) diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_client_base.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_client_base.py deleted file mode 100644 index 3e1a2bec8fc4..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_client_base.py +++ /dev/null @@ -1,117 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import sys -from typing import Any - -from azure.core.credentials_async import AsyncTokenCredential -from azure.core.pipeline.policies import HttpLoggingPolicy -from azure.core.rest import AsyncHttpResponse, HttpRequest -from azure.core.tracing.decorator_async import distributed_trace_async - -from . import AsyncChallengeAuthPolicy -from .client_base import ApiVersion, DEFAULT_VERSION, _format_api_version, _SERIALIZER -from .._sdk_moniker import SDK_MONIKER -from .._generated.aio import KeyVaultClient as _KeyVaultClient -from .._generated import models as _models - -if sys.version_info < (3, 9): - from typing import Awaitable -else: - from collections.abc import Awaitable - - -class AsyncKeyVaultClientBase(object): - # pylint:disable=protected-access - def __init__(self, vault_url: str, credential: AsyncTokenCredential, **kwargs: Any) -> None: - if not credential: - raise ValueError( - "credential should be an object supporting the AsyncTokenCredential 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") - - try: - self.api_version = kwargs.pop("api_version", DEFAULT_VERSION) - # If API version was provided as an enum value, need to make a plain string for 3.11 compatibility - if hasattr(self.api_version, "value"): - self.api_version = self.api_version.value - self._vault_url = vault_url.strip(" /") - - client = kwargs.get("generated_client") - if client: - # caller provided a configured client -> only models left to initialize - self._client = client - models = kwargs.get("generated_models") - self._models = models or _models - return - - http_logging_policy = HttpLoggingPolicy(**kwargs) - http_logging_policy.allowed_header_names.update( - {"x-ms-keyvault-network-info", "x-ms-keyvault-region", "x-ms-keyvault-service-version"} - ) - - verify_challenge = kwargs.pop("verify_challenge_resource", True) - self._client = _KeyVaultClient( - credential=credential, - vault_base_url=self._vault_url, - api_version=self.api_version, - authentication_policy=AsyncChallengeAuthPolicy(credential, verify_challenge_resource=verify_challenge), - sdk_moniker=SDK_MONIKER, - http_logging_policy=http_logging_policy, - **kwargs, - ) - self._models = _models - except ValueError as exc: - # Ignore pyright error that comes from not identifying ApiVersion as an iterable enum - raise NotImplementedError( - f"This package doesn't support API version '{self.api_version}'. " - + "Supported versions: " - + f"{', '.join(v.value for v in ApiVersion)}" # pyright: ignore[reportGeneralTypeIssues] - ) from exc - - @property - def vault_url(self) -> str: - return self._vault_url - - async def __aenter__(self) -> "AsyncKeyVaultClientBase": - await self._client.__aenter__() - return self - - async def __aexit__(self, *args: Any) -> None: - await self._client.__aexit__(*args) - - async def close(self) -> None: - """Close sockets opened by the client. - - Calling this method is unnecessary when using the client as a context manager. - """ - await self._client.close() - - @distributed_trace_async - def send_request( - self, request: HttpRequest, *, stream: bool = False, **kwargs: Any - ) -> Awaitable[AsyncHttpResponse]: - """Runs a network request using the client's existing pipeline. - - The request URL can be relative to the vault URL. The service API version used for the request is the same as - the client's unless otherwise specified. This method does not raise if the response is an error; to raise an - exception, call `raise_for_status()` on the returned response object. For more information about how to send - custom requests with this method, see https://aka.ms/azsdk/dpcodegen/python/send_request. - - :param request: The network request you want to make. - :type request: ~azure.core.rest.HttpRequest - - :keyword bool stream: Whether the response payload will be streamed. Defaults to False. - - :return: The response of your network call. Does not do error handling on your response. - :rtype: ~azure.core.rest.AsyncHttpResponse - """ - request_copy = _format_api_version(request, self.api_version) - path_format_arguments = { - "vaultBaseUrl": _SERIALIZER.url("vault_base_url", self._vault_url, "str", skip_quote=True), - } - request_copy.url = self._client._client.format_url(request_copy.url, **path_format_arguments) - return self._client._client.send_request(request_copy, stream=stream, **kwargs) diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py deleted file mode 100644 index eb4073d0e699..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py +++ /dev/null @@ -1,270 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -"""Policy implementing Key Vault's challenge authentication protocol. - -Normally the protocol is only used for the client's first service request, upon which: -1. The challenge authentication policy sends a copy of the request, without authorization or content. -2. Key Vault responds 401 with a header (the 'challenge') detailing how the client should authenticate such a request. -3. The policy authenticates according to the challenge and sends the original request with authorization. - -The policy caches the challenge and thus knows how to authenticate future requests. However, authentication -requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the -protocol again. -""" - -from copy import deepcopy -import time -from typing import Any, cast, Optional, Union -from urllib.parse import urlparse - -from azure.core.credentials import ( - AccessToken, - AccessTokenInfo, - TokenCredential, - TokenProvider, - TokenRequestOptions, - SupportsTokenInfo, -) -from azure.core.exceptions import ServiceRequestError -from azure.core.pipeline import PipelineRequest, PipelineResponse -from azure.core.pipeline.policies import BearerTokenCredentialPolicy -from azure.core.rest import HttpRequest, HttpResponse - -from .http_challenge import HttpChallenge -from . import http_challenge_cache as ChallengeCache - - -def _enforce_tls(request: PipelineRequest) -> None: - if not request.http_request.url.lower().startswith("https"): - raise ServiceRequestError( - "Bearer token authentication is not permitted for non-TLS protected (non-https) URLs." - ) - - -def _has_claims(challenge: str) -> bool: - """Check if a challenge header contains claims. - - :param challenge: The challenge header to check. - :type challenge: str - - :returns: True if the challenge contains claims; False otherwise. - :rtype: bool - """ - # Split the challenge into its scheme and parameters, then check if any parameter contains claims - split_challenge = challenge.strip().split(" ", 1) - return any("claims=" in item for item in split_challenge[1].split(",")) - - -def _update_challenge(request: PipelineRequest, challenger: PipelineResponse) -> HttpChallenge: - """Parse challenge from a challenge response, cache it, and return it. - - :param request: The pipeline request that prompted the challenge response. - :type request: ~azure.core.pipeline.PipelineRequest - :param challenger: The pipeline response containing the authentication challenge. - :type challenger: ~azure.core.pipeline.PipelineResponse - - :returns: An HttpChallenge object representing the authentication challenge. - :rtype: HttpChallenge - """ - - challenge = HttpChallenge( - request.http_request.url, - challenger.http_response.headers.get("WWW-Authenticate"), - response_headers=challenger.http_response.headers, - ) - ChallengeCache.set_challenge_for_url(request.http_request.url, challenge) - return challenge - - -class ChallengeAuthPolicy(BearerTokenCredentialPolicy): - """Policy for handling HTTP authentication challenges. - - :param credential: An object which can provide an access token for the vault, such as a credential from - :mod:`azure.identity` - :type credential: ~azure.core.credentials.TokenProvider - :param str scopes: Lets you specify the type of access needed. - """ - - def __init__(self, credential: TokenProvider, *scopes: str, **kwargs: Any) -> None: - # Pass `enable_cae` so `enable_cae=True` is always passed through self.authorize_request - super(ChallengeAuthPolicy, self).__init__(credential, *scopes, enable_cae=True, **kwargs) - self._credential: TokenProvider = credential - self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None - self._verify_challenge_resource = kwargs.pop("verify_challenge_resource", True) - self._request_copy: Optional[HttpRequest] = None - - def send(self, request: PipelineRequest[HttpRequest]) -> PipelineResponse[HttpRequest, HttpResponse]: - """Authorize request with a bearer token and send it to the next policy. - - We implement this method to account for the valid scenario where a Key Vault authentication challenge is - immediately followed by a CAE claims challenge. The base class's implementation would return the second 401 to - the caller, but we should handle that second challenge as well (and only return any third 401 response). - - :param request: The pipeline request object - :type request: ~azure.core.pipeline.PipelineRequest - - :return: The pipeline response object - :rtype: ~azure.core.pipeline.PipelineResponse - """ - self.on_request(request) - try: - response = self.next.send(request) - except Exception: # pylint:disable=broad-except - self.on_exception(request) - raise - - self.on_response(request, response) - if response.http_response.status_code == 401: - return self.handle_challenge_flow(request, response) - return response - - def handle_challenge_flow( - self, - request: PipelineRequest[HttpRequest], - response: PipelineResponse[HttpRequest, HttpResponse], - consecutive_challenge: bool = False, - ) -> PipelineResponse[HttpRequest, HttpResponse]: - """Handle the challenge flow of Key Vault and CAE authentication. - - :param request: The pipeline request object - :type request: ~azure.core.pipeline.PipelineRequest - :param response: The pipeline response object - :type response: ~azure.core.pipeline.PipelineResponse - :param bool consecutive_challenge: Whether the challenge is arriving immediately after another challenge. - Consecutive challenges can only be valid if a Key Vault challenge is followed by a CAE claims challenge. - True if the preceding challenge was a Key Vault challenge; False otherwise. - - :return: The pipeline response object - :rtype: ~azure.core.pipeline.PipelineResponse - """ - self._token = None # any cached token is invalid - if "WWW-Authenticate" in response.http_response.headers: - # If the previous challenge was a KV challenge and this one is too, return the 401 - claims_challenge = _has_claims(response.http_response.headers["WWW-Authenticate"]) - if consecutive_challenge and not claims_challenge: - return response - - request_authorized = self.on_challenge(request, response) - if request_authorized: - # if we receive a challenge response, we retrieve a new token - # which matches the new target. In this case, we don't want to remove - # token from the request so clear the 'insecure_domain_change' tag - request.context.options.pop("insecure_domain_change", False) - try: - response = self.next.send(request) - except Exception: # pylint:disable=broad-except - self.on_exception(request) - raise - - # If consecutive_challenge == True, this could be a third consecutive 401 - if response.http_response.status_code == 401 and not consecutive_challenge: - # If the previous challenge wasn't from CAE, we can try this function one more time - if not claims_challenge: - return self.handle_challenge_flow(request, response, consecutive_challenge=True) - self.on_response(request, response) - return response - - def on_request(self, request: PipelineRequest) -> None: - _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if challenge: - # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. - if self._need_new_token: - # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource - scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._request_kv_token(scope, challenge) - - bearer_token = cast(Union["AccessToken", "AccessTokenInfo"], self._token).token - request.http_request.headers["Authorization"] = f"Bearer {bearer_token}" - return - - # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, - # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. - # on_challenge will parse that challenge, use the original request including the body, authorize the - # request, and tell super to send it again. - if request.http_request.content: - self._request_copy = request.http_request - bodiless_request = HttpRequest( - method=request.http_request.method, - url=request.http_request.url, - headers=deepcopy(request.http_request.headers), - ) - bodiless_request.headers["Content-Length"] = "0" - request.http_request = bodiless_request - - def on_challenge(self, request: PipelineRequest, response: PipelineResponse) -> bool: - try: - # CAE challenges may not include a scope or tenant; cache from the previous challenge to use if necessary - old_scope: Optional[str] = None - old_tenant: Optional[str] = None - cached_challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if cached_challenge: - old_scope = cached_challenge.get_scope() or cached_challenge.get_resource() + "/.default" - old_tenant = cached_challenge.tenant_id - - challenge = _update_challenge(request, response) - # CAE challenges may not include a scope or tenant; use the previous challenge's values if necessary - if challenge.claims and old_scope: - challenge._parameters["scope"] = old_scope # pylint:disable=protected-access - challenge.tenant_id = old_tenant - # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource - scope = challenge.get_scope() or challenge.get_resource() + "/.default" - except ValueError: - return False - - if self._verify_challenge_resource: - resource_domain = urlparse(scope).netloc - if not resource_domain: - raise ValueError(f"The challenge contains invalid scope '{scope}'.") - - request_domain = urlparse(request.http_request.url).netloc - if not request_domain.lower().endswith(f".{resource_domain.lower()}"): - raise ValueError( - f"The challenge resource '{resource_domain}' does not match the requested domain. Pass " - "`verify_challenge_resource=False` to your client's constructor to disable this verification. " - "See https://aka.ms/azsdk/blog/vault-uri for more information." - ) - - # If we had created a request copy in on_request, use it now to send along the original body content - if self._request_copy: - request.http_request = self._request_copy - - # The tenant parsed from AD FS challenges is "adfs"; we don't actually need a tenant for AD FS authentication - # For AD FS we skip cross-tenant authentication per https://github.com/Azure/azure-sdk-for-python/issues/28648 - if challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs"): - self.authorize_request(request, scope, claims=challenge.claims) - else: - self.authorize_request(request, scope, claims=challenge.claims, tenant_id=challenge.tenant_id) - - return True - - @property - def _need_new_token(self) -> bool: - now = time.time() - refresh_on = getattr(self._token, "refresh_on", None) - return not self._token or (refresh_on and refresh_on <= now) or self._token.expires_on - now < 300 - - def _request_kv_token(self, scope: str, challenge: HttpChallenge) -> None: - """Implementation of BearerTokenCredentialPolicy's _request_token method, but specific to Key Vault. - - :param str scope: The scope for which to request a token. - :param challenge: The challenge for the request being made. - :type challenge: HttpChallenge - """ - # Exclude tenant for AD FS authentication - exclude_tenant = challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs") - # The SupportsTokenInfo protocol needs TokenRequestOptions for token requests instead of kwargs - if hasattr(self._credential, "get_token_info"): - options: TokenRequestOptions = {"enable_cae": True} - if challenge.tenant_id and not exclude_tenant: - options["tenant_id"] = challenge.tenant_id - self._token = cast(SupportsTokenInfo, self._credential).get_token_info(scope, options=options) - else: - if exclude_tenant: - self._token = self._credential.get_token(scope, enable_cae=True) - else: - self._token = cast(TokenCredential, self._credential).get_token( - scope, tenant_id=challenge.tenant_id, enable_cae=True - ) diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/client_base.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/client_base.py deleted file mode 100644 index 9e721ef6cfe2..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/client_base.py +++ /dev/null @@ -1,162 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from copy import deepcopy -from enum import Enum -from typing import Any -from urllib.parse import urlparse - -from azure.core import CaseInsensitiveEnumMeta -from azure.core.credentials import TokenCredential -from azure.core.pipeline.policies import HttpLoggingPolicy -from azure.core.rest import HttpRequest, HttpResponse -from azure.core.tracing.decorator import distributed_trace - -from . import ChallengeAuthPolicy -from .._generated import KeyVaultClient as _KeyVaultClient -from .._generated import models as _models -from .._generated._utils.serialization import Serializer -from .._sdk_moniker import SDK_MONIKER - - -class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Key Vault API versions supported by this package""" - - #: this is the default version - V2025_07_01 = "2025-07-01" - V7_6 = "7.6" - V7_5 = "7.5" - V7_4 = "7.4" - V7_3 = "7.3" - V7_2 = "7.2" - V7_1 = "7.1" - V7_0 = "7.0" - V2016_10_01 = "2016-10-01" - - -DEFAULT_VERSION = ApiVersion.V2025_07_01 - -_SERIALIZER = Serializer() -_SERIALIZER.client_side_validation = False - - -def _format_api_version(request: HttpRequest, api_version: str) -> HttpRequest: - """Returns a request copy that includes an api-version query parameter if one wasn't originally present. - - :param request: The HTTP request being sent. - :type request: ~azure.core.rest.HttpRequest - :param str api_version: The service API version that the request should include. - - :returns: A copy of the request that includes an api-version query parameter. - :rtype: azure.core.rest.HttpRequest - """ - request_copy = deepcopy(request) - params = {"api-version": api_version} # By default, we want to use the client's API version - query = urlparse(request_copy.url).query - - if query: - request_copy.url = request_copy.url.partition("?")[0] - existing_params = {p[0]: p[-1] for p in [p.partition("=") for p in query.split("&")]} - params.update(existing_params) # If an api-version was provided, this will overwrite our default - - # Reconstruct the query parameters onto the URL - query_params = [] - for k, v in params.items(): - query_params.append("{}={}".format(k, v)) - query = "?" + "&".join(query_params) - request_copy.url = request_copy.url + query - return request_copy - - -class KeyVaultClientBase(object): - # pylint:disable=protected-access - def __init__(self, vault_url: str, credential: TokenCredential, **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") - - try: - self.api_version = kwargs.pop("api_version", DEFAULT_VERSION) - # If API version was provided as an enum value, need to make a plain string for 3.11 compatibility - if hasattr(self.api_version, "value"): - self.api_version = self.api_version.value - self._vault_url = vault_url.strip(" /") - - client = kwargs.get("generated_client") - if client: - # caller provided a configured client -> only models left to initialize - self._client = client - models = kwargs.get("generated_models") - self._models = models or _models - return - - http_logging_policy = HttpLoggingPolicy(**kwargs) - http_logging_policy.allowed_header_names.update( - {"x-ms-keyvault-network-info", "x-ms-keyvault-region", "x-ms-keyvault-service-version"} - ) - - verify_challenge = kwargs.pop("verify_challenge_resource", True) - self._client = _KeyVaultClient( - credential=credential, - vault_base_url=self._vault_url, - api_version=self.api_version, - authentication_policy=ChallengeAuthPolicy(credential, verify_challenge_resource=verify_challenge), - sdk_moniker=SDK_MONIKER, - http_logging_policy=http_logging_policy, - **kwargs, - ) - self._models = _models - except ValueError as exc: - # Ignore pyright error that comes from not identifying ApiVersion as an iterable enum - raise NotImplementedError( - f"This package doesn't support API version '{self.api_version}'. " - + "Supported versions: " - + f"{', '.join(v.value for v in ApiVersion)}" # pyright: ignore[reportGeneralTypeIssues] - ) from exc - - @property - def vault_url(self) -> str: - return self._vault_url - - def __enter__(self) -> "KeyVaultClientBase": - self._client.__enter__() - return self - - def __exit__(self, *args: Any) -> None: - self._client.__exit__(*args) - - def close(self) -> None: - """Close sockets opened by the client. - - Calling this method is unnecessary when using the client as a context manager. - """ - self._client.close() - - @distributed_trace - def send_request(self, request: HttpRequest, *, stream: bool = False, **kwargs: Any) -> HttpResponse: - """Runs a network request using the client's existing pipeline. - - The request URL can be relative to the vault URL. The service API version used for the request is the same as - the client's unless otherwise specified. This method does not raise if the response is an error; to raise an - exception, call `raise_for_status()` on the returned response object. For more information about how to send - custom requests with this method, see https://aka.ms/azsdk/dpcodegen/python/send_request. - - :param request: The network request you want to make. - :type request: ~azure.core.rest.HttpRequest - - :keyword bool stream: Whether the response payload will be streamed. Defaults to False. - - :return: The response of your network call. Does not do error handling on your response. - :rtype: ~azure.core.rest.HttpResponse - """ - request_copy = _format_api_version(request, self.api_version) - path_format_arguments = { - "vaultBaseUrl": _SERIALIZER.url("vault_base_url", self._vault_url, "str", skip_quote=True), - } - request_copy.url = self._client._client.format_url(request_copy.url, **path_format_arguments) - return self._client._client.send_request(request_copy, stream=stream, **kwargs) diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py deleted file mode 100644 index 8b14b999de78..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py +++ /dev/null @@ -1,186 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import base64 -from typing import Dict, MutableMapping, Optional -from urllib import parse - - -class HttpChallenge(object): - """An object representing the content of a Key Vault authentication challenge. - - :param str request_uri: The URI of the HTTP request that prompted this challenge. - :param str challenge: The WWW-Authenticate header of the challenge response. - :param response_headers: Optional. The headers attached to the challenge response. - :type response_headers: MutableMapping[str, str] or None - """ - - def __init__( - self, request_uri: str, challenge: str, response_headers: "Optional[MutableMapping[str, str]]" = None - ) -> None: - """Parses an HTTP WWW-Authentication Bearer challenge from a server. - - Example challenge with claims: - Bearer authorization="https://login.windows-ppe.net/", error="invalid_token", - error_description="User session has been revoked", - claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwMzc0MjgwMCJ9fX0=" - """ - self.source_authority = self._validate_request_uri(request_uri) - self.source_uri = request_uri - self._parameters: "Dict[str, str]" = {} - - # get the scheme of the challenge and remove from the challenge string - trimmed_challenge = self._validate_challenge(challenge) - split_challenge = trimmed_challenge.split(" ", 1) - self.scheme = split_challenge[0] - trimmed_challenge = split_challenge[1] - - self.claims = None - # split trimmed challenge into comma-separated name=value pairs. Values are expected - # to be surrounded by quotes which are stripped here. - for item in trimmed_challenge.split(","): - # Special case for claims, which can contain = symbols as padding. Assume at most one claim per challenge - if "claims=" in item: - encoded_claims = item[item.index("=") + 1 :].strip(" \"'") - padding_needed = -len(encoded_claims) % 4 - try: - decoded_claims = base64.urlsafe_b64decode(encoded_claims + "=" * padding_needed).decode() - self.claims = decoded_claims - except Exception: # pylint:disable=broad-except - continue - # process name=value pairs - else: - comps = item.split("=") - if len(comps) == 2: - key = comps[0].strip(' "') - value = comps[1].strip(' "') - if key: - self._parameters[key] = value - - # minimum set of parameters - if not self._parameters: - raise ValueError("Invalid challenge parameters") - - # must specify authorization or authorization_uri - if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: - raise ValueError("Invalid challenge parameters") - - authorization_uri = self.get_authorization_server() - # the authorization server URI should look something like https://login.windows.net/tenant-id - raw_uri_path = str(parse.urlparse(authorization_uri).path) - uri_path = raw_uri_path.lstrip("/") - self.tenant_id = uri_path.split("/", maxsplit=1)[0] or None - - # if the response headers were supplied - if response_headers: - # get the message signing key and message key encryption key from the headers - self.server_signature_key = response_headers.get("x-ms-message-signing-key", None) - self.server_encryption_key = response_headers.get("x-ms-message-encryption-key", None) - - def is_bearer_challenge(self) -> bool: - """Tests whether the HttpChallenge is a Bearer challenge. - - :returns: True if the challenge is a Bearer challenge; False otherwise. - :rtype: bool - """ - if not self.scheme: - return False - - return self.scheme.lower() == "bearer" - - def is_pop_challenge(self) -> bool: - """Tests whether the HttpChallenge is a proof of possession challenge. - - :returns: True if the challenge is a proof of possession challenge; False otherwise. - :rtype: bool - """ - if not self.scheme: - return False - - return self.scheme.lower() == "pop" - - def get_value(self, key: str) -> "Optional[str]": - return self._parameters.get(key) - - def get_authorization_server(self) -> str: - """Returns the URI for the authorization server if present, otherwise an empty string. - - :returns: The URI for the authorization server if present, otherwise an empty string. - :rtype: str - """ - value = "" - for key in ["authorization_uri", "authorization"]: - value = self.get_value(key) or "" - if value: - break - return value - - def get_resource(self) -> str: - """Returns the resource if present, otherwise an empty string. - - :returns: The challenge resource if present, otherwise an empty string. - :rtype: str - """ - return self.get_value("resource") or "" - - def get_scope(self) -> str: - """Returns the scope if present, otherwise an empty string. - - :returns: The challenge scope if present, otherwise an empty string. - :rtype: str - """ - return self.get_value("scope") or "" - - def supports_pop(self) -> bool: - """Returns True if the challenge supports proof of possession token auth; False otherwise. - - :returns: True if the challenge supports proof of possession token auth; False otherwise. - :rtype: bool - """ - return self._parameters.get("supportspop", "").lower() == "true" - - def supports_message_protection(self) -> bool: - """Returns True if the challenge vault supports message protection; False otherwise. - - :returns: True if the challenge vault supports message protection; False otherwise. - :rtype: bool - """ - return self.supports_pop() and self.server_encryption_key and self.server_signature_key # type: ignore - - def _validate_challenge( - self, challenge: str - ) -> str: # pylint:disable=bad-option-value,useless-option-value,no-self-use - """Verifies that the challenge is a valid auth challenge and returns the key=value pairs. - - :param str challenge: The WWW-Authenticate header of the challenge response. - - :returns: The challenge key/value pairs, with whitespace removed, as a string. - :rtype: str - """ - if not challenge: - raise ValueError("Challenge cannot be empty") - - return challenge.strip() - - def _validate_request_uri( - self, uri: str - ) -> str: # pylint:disable=bad-option-value,useless-option-value,no-self-use - """Extracts the host authority from the given URI. - - :param str uri: The URI of the HTTP request that prompted the challenge. - - :returns: The challenge host authority. - :rtype: str - """ - if not uri: - raise ValueError("request_uri cannot be empty") - - parsed = parse.urlparse(uri) - if not parsed.netloc: - raise ValueError("request_uri must be an absolute URI") - - if parsed.scheme.lower() not in ["http", "https"]: - raise ValueError("request_uri must be HTTP or HTTPS") - - return parsed.netloc diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge_cache.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge_cache.py deleted file mode 100644 index 99f32091e24b..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge_cache.py +++ /dev/null @@ -1,93 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import threading -from typing import Dict, Optional -from urllib import parse - -from .http_challenge import HttpChallenge - - -_cache: "Dict[str, HttpChallenge]" = {} -_lock = threading.Lock() - - -def get_challenge_for_url(url: str) -> "Optional[HttpChallenge]": - """Gets the challenge for the cached URL. - - :param str url: the URL the challenge is cached for. - - :returns: The challenge for the cached request URL, or None if the request URL isn't cached. - :rtype: HttpChallenge or None - """ - - if not url: - raise ValueError("URL cannot be None") - - key = _get_cache_key(url) - - with _lock: - return _cache.get(key.lower()) - - -def _get_cache_key(url: str) -> str: - """Use the URL's netloc as cache key except when the URL specifies the default port for its scheme. In that case - use the netloc without the port. That is to say, https://foo.bar and https://foo.bar:443 are considered equivalent. - - This equivalency prevents an unnecessary challenge when using Key Vault's paging API. The Key Vault client doesn't - specify ports, but Key Vault's next page links do, so a redundant challenge would otherwise be executed when the - client requests the next page. - - :param str url: The HTTP request URL. - - :returns: The URL's `netloc`, minus any port attached to the URL. - :rtype: str - """ - - parsed = parse.urlparse(url) - if parsed.scheme == "https" and parsed.port == 443: - return parsed.netloc[:-4] - return parsed.netloc - - -def remove_challenge_for_url(url: str) -> None: - """Removes the cached challenge for the specified URL. - - :param str url: the URL for which to remove the cached challenge - """ - if not url: - raise ValueError("URL cannot be empty") - - key = _get_cache_key(url) - with _lock: - del _cache[key.lower()] - - -def set_challenge_for_url(url: str, challenge: "HttpChallenge") -> None: - """Caches the challenge for the specified URL. - - :param str url: the URL for which to cache the challenge - :param challenge: the challenge to cache - :type challenge: HttpChallenge - """ - if not url: - raise ValueError("URL cannot be empty") - - if not challenge: - raise ValueError("Challenge cannot be empty") - - src_url = parse.urlparse(url) - if src_url.netloc.lower() != challenge.source_authority.lower(): - raise ValueError("Source URL and Challenge URL do not match") - - key = _get_cache_key(url) - with _lock: - _cache[key.lower()] = challenge - - -def clear() -> None: - """Clears the cache.""" - - with _lock: - _cache.clear() diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_version.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_version.py deleted file mode 100644 index f526d23db20e..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_version.py +++ /dev/null @@ -1,6 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -VERSION = "4.11.2" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/__init__.py deleted file mode 100644 index ba1c15a8d7ae..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -from ._client import CertificateClient - -__all__ = ["CertificateClient"] diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_client.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_client.py deleted file mode 100644 index 96efea0105c8..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_client.py +++ /dev/null @@ -1,1077 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -# pylint:disable=too-many-lines,too-many-public-methods -import base64 -from typing import Any, Dict, List, Optional, Union -from functools import partial - -from azure.core.polling import AsyncLROPoller -from azure.core.tracing.decorator import distributed_trace -from azure.core.tracing.decorator_async import distributed_trace_async -from azure.core.async_paging import AsyncItemPaged - -from .. import ( - AdministratorContact, - KeyVaultCertificate, - CertificateOperation, - CertificatePolicy, - DeletedCertificate, - CertificateProperties, - CertificateContact, - CertificateIssuer, - IssuerProperties, -) -from ._polling_async import CreateCertificatePollerAsync -from .._client import NO_SAN_OR_SUBJECT -from .._shared import AsyncKeyVaultClientBase -from .._shared._polling_async import AsyncDeleteRecoverPollingMethod - - -class CertificateClient(AsyncKeyVaultClientBase): - """A high-level asynchronous interface for managing a vault's certificates. - - :param str vault_url: URL of the vault the client will access. This is also called the vault's "DNS Name". - You should validate that this URL references a valid Key Vault resource. See https://aka.ms/azsdk/blog/vault-uri - for details. - :param credential: An object which can provide an access token for the vault, such as a credential from - :mod:`azure.identity.aio` - :type credential: ~azure.core.credentials_async.AsyncTokenCredential - - :keyword api_version: Version of the service API to use. Defaults to the most recent. - :paramtype api_version: ~azure.keyvault.certificates.ApiVersion or str - :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key - Vault domain. Defaults to True. - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START create_certificate_client] - :end-before: [END create_certificate_client] - :language: python - :dedent: 4 - :caption: Creates a new instance of the Certificate client - """ - - # pylint:disable=protected-access - @distributed_trace_async - async def create_certificate( - self, - certificate_name: str, - policy: CertificatePolicy, - *, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - preserve_order: Optional[bool] = None, - **kwargs: Any, - ) -> Union[KeyVaultCertificate, CertificateOperation]: - """Creates a new certificate. - - If this is the first version, the certificate resource is created. This operation requires the - certificates/create permission. The poller requires the certificates/get permission, otherwise raises an - :class:`~azure.core.exceptions.HttpResponseError`. - - :param str certificate_name: The name of the certificate. - :param policy: The management policy for the certificate. Either subject or one of the subject alternative - name properties are required. - :type policy: ~azure.keyvault.certificates.CertificatePolicy - - :keyword bool enabled: Whether the certificate is enabled for use. - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] - :keyword bool preserve_order: Whether to preserve the order of the certificate chain. - - :returns: A coroutine for the creation of the certificate. Awaiting the coroutine returns the created - KeyVaultCertificate if creation is successful, or the CertificateOperation if not. - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate or ~azure.keyvault.certificates.CertificateOperation - - :raises ValueError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate policy is invalid; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START create_certificate] - :end-before: [END create_certificate] - :language: python - :caption: Create a certificate - :dedent: 8 - """ - if not ( - policy.san_emails - or policy.san_user_principal_names - or policy.san_dns_names - or policy.san_ip_addresses - or policy.san_uris - or policy.subject - ): - raise ValueError(NO_SAN_OR_SUBJECT) - - polling_interval = kwargs.pop("_polling_interval", None) - if polling_interval is None: - polling_interval = 5 - - if enabled is not None: - attributes = self._models.CertificateAttributes(enabled=enabled) - else: - attributes = None - - parameters = self._models.CertificateCreateParameters( - certificate_policy=policy._to_certificate_policy_bundle(), - certificate_attributes=attributes, - tags=tags, - preserve_cert_order=preserve_order, - ) - - pipeline_response, cert_bundle = await self._client.create_certificate( - certificate_name=certificate_name, - parameters=parameters, - cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), - **kwargs - ) - - create_certificate_operation = CertificateOperation._from_certificate_operation_bundle(cert_bundle) - - command = partial(self.get_certificate_operation, certificate_name=certificate_name, **kwargs) - - get_certificate_command = partial(self.get_certificate, certificate_name=certificate_name, **kwargs) - - create_certificate_polling = CreateCertificatePollerAsync( - pipeline_response=pipeline_response, - get_certificate_command=get_certificate_command, - interval=polling_interval, - ) - def no_op(*_, **__) -> Any: # The deserialization callback is ignored based on polling implementation - pass - return await AsyncLROPoller(command, create_certificate_operation, no_op, create_certificate_polling) - - @distributed_trace_async - async def get_certificate(self, certificate_name: str, **kwargs: Any) -> KeyVaultCertificate: - """Gets a certificate with its management policy attached. Requires certificates/get permission. - - Does not accept the version of the certificate as a parameter. To get a specific version of the - certificate, call :func:`get_certificate_version`. - - :param str certificate_name: The name of the certificate in the given vault. - - :returns: An instance of KeyVaultCertificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START get_certificate] - :end-before: [END get_certificate] - :language: python - :caption: Get a certificate - :dedent: 8 - """ - bundle = await self._client.get_certificate( - certificate_name=certificate_name, - certificate_version="", - **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace_async - async def get_certificate_version( - self, certificate_name: str, version: str, **kwargs: Any - ) -> KeyVaultCertificate: - """Gets a specific version of a certificate without returning its management policy. - - Requires certificates/get permission. To get the latest version of the certificate, or to get the certificate's - policy as well, call :func:`get_certificate`. - - :param str certificate_name: The name of the certificate in the given vault. - :param str version: The version of the certificate. - - :returns: An instance of KeyVaultCertificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START get_certificate_version] - :end-before: [END get_certificate_version] - :language: python - :caption: Get a certificate with a specific version - :dedent: 8 - """ - bundle = await self._client.get_certificate( - certificate_name=certificate_name, - certificate_version=version, - **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace_async - async def delete_certificate(self, certificate_name: str, **kwargs: Any) -> DeletedCertificate: - """Delete all versions of a certificate. Requires certificates/delete permission. - - If the vault has soft-delete enabled, deletion may take several seconds to complete. - - :param str certificate_name: The name of the certificate. - - :returns: The deleted certificate - :rtype: ~azure.keyvault.certificates.DeletedCertificate - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START delete_certificate] - :end-before: [END delete_certificate] - :language: python - :caption: Delete a certificate - :dedent: 8 - """ - polling_interval = kwargs.pop("_polling_interval", None) - if polling_interval is None: - polling_interval = 2 - pipeline_response, deleted_cert_bundle = await self._client.delete_certificate( - certificate_name=certificate_name, - cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), - **kwargs, - ) - deleted_certificate = DeletedCertificate._from_deleted_certificate_bundle(deleted_cert_bundle) - - polling_method = AsyncDeleteRecoverPollingMethod( - # no recovery ID means soft-delete is disabled, in which case we initialize the poller as finished - finished=deleted_certificate.recovery_id is None, - pipeline_response=pipeline_response, - command=partial(self.get_deleted_certificate, certificate_name=certificate_name, **kwargs), - final_resource=deleted_certificate, - interval=polling_interval, - ) - await polling_method.run() - - return polling_method.resource() - - @distributed_trace_async - async def get_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> DeletedCertificate: - """Get a deleted certificate. Possible only in a vault with soft-delete enabled. - - Requires certificates/get permission. Retrieves the deleted certificate information plus its attributes, such as - retention interval, scheduled permanent deletion, and the current deletion recovery level. - - :param str certificate_name: The name of the certificate. - - :return: The deleted certificate - :rtype: ~azure.keyvault.certificates.DeletedCertificate - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START get_deleted_certificate] - :end-before: [END get_deleted_certificate] - :language: python - :caption: Get a deleted certificate - :dedent: 8 - """ - bundle = await self._client.get_deleted_certificate( - certificate_name=certificate_name, **kwargs - ) - return DeletedCertificate._from_deleted_certificate_bundle(deleted_certificate_bundle=bundle) - - @distributed_trace_async - async def purge_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> None: - """Permanently deletes a deleted certificate. Possible only in vaults with soft-delete enabled. - - Requires certificates/purge permission. Performs an irreversible deletion of the specified certificate, without - possibility for recovery. The operation is not available if the - :py:attr:`~azure.keyvault.certificates.CertificateProperties.recovery_level` does not specify 'Purgeable'. - This method is only necessary for purging a certificate before its - :py:attr:`~azure.keyvault.certificates.DeletedCertificate.scheduled_purge_date`. - - :param str certificate_name: The name of the certificate - - :return: None - :rtype: None - - :raises ~azure.core.exceptions.HttpResponseError: - """ - await self._client.purge_deleted_certificate( - certificate_name=certificate_name, **kwargs - ) - - @distributed_trace_async - async def recover_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> KeyVaultCertificate: - """Recover a deleted certificate to its latest version. Possible only in a vault with soft-delete enabled. - - Requires certificates/recover permission. If the vault does not have soft-delete enabled, - :func:`delete_certificate` is permanent, and this method will raise an error. Attempting to recover a - non-deleted certificate will also raise an error. - - :param str certificate_name: The name of the deleted certificate - - :returns: The recovered certificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START recover_deleted_certificate] - :end-before: [END recover_deleted_certificate] - :language: python - :caption: Recover a deleted certificate - :dedent: 8 - """ - polling_interval = kwargs.pop("_polling_interval", None) - if polling_interval is None: - polling_interval = 2 - pipeline_response, recovered_cert_bundle = await self._client.recover_deleted_certificate( - certificate_name=certificate_name, - cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), - **kwargs, - ) - recovered_certificate = KeyVaultCertificate._from_certificate_bundle(recovered_cert_bundle) - - command = partial(self.get_certificate, certificate_name=certificate_name, **kwargs) - polling_method = AsyncDeleteRecoverPollingMethod( - pipeline_response=pipeline_response, - command=command, - final_resource=recovered_certificate, - finished=False, - interval=polling_interval - ) - await polling_method.run() - - return polling_method.resource() - - @distributed_trace_async - async def import_certificate( - self, - certificate_name: str, - certificate_bytes: bytes, - *, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - password: Optional[str] = None, - policy: Optional[CertificatePolicy] = None, - preserve_order: Optional[bool] = None, - **kwargs: Any, - ) -> KeyVaultCertificate: - """Import a certificate created externally. Requires certificates/import permission. - - Imports an existing valid certificate, containing a private key, into Azure Key Vault. The certificate to be - imported can be in either PFX or PEM format. If the certificate is in PEM format the PEM file must contain the - key as well as x509 certificates, and you must provide a ``policy`` with - :attr:`~azure.keyvault.certificates.CertificatePolicy.content_type` of - :attr:`~azure.keyvault.certificates.CertificateContentType.pem`. - - :param str certificate_name: The name of the certificate. - :param bytes certificate_bytes: Bytes of the certificate object to import. - This certificate needs to contain the private key. - - :keyword bool enabled: Whether the certificate is enabled for use. - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] - :keyword str password: If the private key in the passed in certificate is encrypted, it - is the password used for encryption. - :keyword policy: The management policy for the certificate. Required if importing a PEM-format certificate, - with :attr:`~azure.keyvault.certificates.CertificatePolicy.content_type` set to - :attr:`~azure.keyvault.certificates.CertificateContentType.pem`. - :paramtype policy: ~azure.keyvault.certificates.CertificatePolicy - :keyword bool preserve_order: Whether to preserve the order of the certificate chain. - - :returns: The imported KeyVaultCertificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.HttpResponseError: - """ - - if enabled is not None: - attributes = self._models.CertificateAttributes(enabled=enabled) - else: - attributes = None - base64_encoded_certificate = base64.b64encode(certificate_bytes).decode("utf-8") - - parameters = self._models.CertificateImportParameters( - base64_encoded_certificate=base64_encoded_certificate, - password=password, - certificate_policy=policy._to_certificate_policy_bundle() if policy else None, - certificate_attributes=attributes, - tags=tags, - preserve_cert_order=preserve_order, - ) - - bundle = await self._client.import_certificate( - certificate_name=certificate_name, - parameters=parameters, - **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace_async - async def get_certificate_policy(self, certificate_name: str, **kwargs: Any) -> CertificatePolicy: - """Gets the policy for a certificate. Requires certificates/get permission. - - Returns the specified certificate policy resources in the key vault. - - :param str certificate_name: The name of the certificate in a given key vault. - - :return: The certificate policy - :rtype: ~azure.keyvault.certificates.CertificatePolicy - - :raises ~azure.core.exceptions.HttpResponseError: - """ - bundle = await self._client.get_certificate_policy( - certificate_name=certificate_name, **kwargs - ) - return CertificatePolicy._from_certificate_policy_bundle(certificate_policy_bundle=bundle) - - @distributed_trace_async - async def update_certificate_policy( - self, certificate_name: str, policy: CertificatePolicy, **kwargs: Any - ) -> CertificatePolicy: - """Updates the policy for a certificate. Requires certificates/update permission. - - Set specified members in the certificate policy. Leaves others as null. - - :param str certificate_name: The name of the certificate in the given vault. - :param policy: The policy for the certificate. - :type policy: ~azure.keyvault.certificates.CertificatePolicy - - :return: The certificate policy - :rtype: ~azure.keyvault.certificates.CertificatePolicy - - :raises ~azure.core.exceptions.HttpResponseError: - """ - bundle = await self._client.update_certificate_policy( - certificate_name=certificate_name, - certificate_policy=policy._to_certificate_policy_bundle(), - **kwargs - ) - return CertificatePolicy._from_certificate_policy_bundle(certificate_policy_bundle=bundle) - - @distributed_trace_async - async def update_certificate_properties( - self, - certificate_name: str, - version: Optional[str] = None, - *, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - **kwargs: Any, - ) -> KeyVaultCertificate: - """Change a certificate's properties. Requires certificates/update permission. - - :param str certificate_name: The name of the certificate in the given key vault. - :param str version: The version of the certificate. - - :keyword bool enabled: Whether the certificate is enabled for use. - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] - - :returns: The updated KeyVaultCertificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START update_certificate] - :end-before: [END update_certificate] - :language: python - :caption: Update a certificate's attributes - :dedent: 8 - """ - - if enabled is not None: - attributes = self._models.CertificateAttributes(enabled=enabled) - else: - attributes = None - - parameters = self._models.CertificateUpdateParameters( - certificate_attributes=attributes, tags=tags - ) - - bundle = await self._client.update_certificate( - certificate_name=certificate_name, - certificate_version=version or "", - parameters=parameters, - **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace_async - async def backup_certificate(self, certificate_name: str, **kwargs: Any) -> bytes: - """Back up a certificate in a protected form useable only by Azure Key Vault. - - Requires certificates/backup permission. This is intended to allow copying a certificate from one vault to - another. Both vaults must be owned by the same Azure subscription. Also, backup / restore cannot be performed - across geopolitical boundaries. For example, a backup from a vault in a USA region cannot be restored to a vault - in an EU region. - - :param str certificate_name: The name of the certificate. - - :return: The backup blob containing the backed up certificate. - :rtype: bytes - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START backup_certificate] - :end-before: [END backup_certificate] - :language: python - :caption: Get a certificate backup - :dedent: 8 - """ - backup_result = await self._client.backup_certificate( - certificate_name=certificate_name, **kwargs - ) - return backup_result.value - - @distributed_trace_async - async def restore_certificate_backup(self, backup: bytes, **kwargs: Any) -> KeyVaultCertificate: - """Restore a certificate backup to the vault. Requires certificates/restore permission. - - This restores all versions of the certificate, with its name, attributes, and access control policies. If the - certificate's name is already in use, restoring it will fail. Also, the target vault must be owned by the same - Microsoft Azure subscription as the source vault. - - :param bytes backup: The backup blob associated with a certificate bundle. - - :return: The restored KeyVaultCertificate - :rtype: ~azure.keyvault.certificates.KeyVaultCertificate - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START restore_certificate] - :end-before: [END restore_certificate] - :language: python - :caption: Restore a certificate backup - :dedent: 8 - """ - bundle = await self._client.restore_certificate( - parameters=self._models.CertificateRestoreParameters(certificate_bundle_backup=backup), - **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace - def list_deleted_certificates( - self, *, include_pending: Optional[bool] = None, **kwargs: Any - ) -> AsyncItemPaged[DeletedCertificate]: - """Lists the currently-recoverable deleted certificates. Possible only if vault is soft-delete enabled. - - Requires certificates/get/list permission. Retrieves the certificates in the current vault which are in a - deleted state and ready for recovery or purging. This operation includes deletion-specific information. - - :keyword bool include_pending: Specifies whether to include certificates which are not completely deleted. - Only available for API versions v7.0 and up. If not provided, Key Vault treats this as False. - :paramtype include_pending: bool or None - - :return: An iterator-like instance of DeletedCertificate - :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.DeletedCertificate] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START list_deleted_certificates] - :end-before: [END list_deleted_certificates] - :language: python - :caption: List all the deleted certificates - :dedent: 8 - """ - max_page_size = kwargs.pop("max_page_size", None) - - if self.api_version == "2016-10-01": - if include_pending is not None: - raise NotImplementedError( - "The 'include_pending' parameter to `list_deleted_certificates` " - "is only available for API versions v7.0 and up" - ) - else: - kwargs.update({"include_pending": include_pending}) - - return self._client.get_deleted_certificates( - maxresults=max_page_size, - cls=lambda objs: [DeletedCertificate._from_deleted_certificate_item(x) for x in objs], - **kwargs - ) - - @distributed_trace - def list_properties_of_certificates( - self, *, include_pending: Optional[bool] = None, **kwargs: Any - ) -> AsyncItemPaged[CertificateProperties]: - """List identifiers and properties of all certificates in the vault. - - Requires certificates/list permission. - - :keyword bool include_pending: Specifies whether to include certificates which are not completely provisioned. - Only available for API versions v7.0 and up. If not provided, Key Vault treats this as False. - :paramtype include_pending: bool or None - - :returns: An iterator-like instance of CertificateProperties - :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateProperties] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START list_properties_of_certificates] - :end-before: [END list_properties_of_certificates] - :language: python - :caption: List all certificates - :dedent: 8 - """ - max_page_size = kwargs.pop("max_page_size", None) - - if self.api_version == "2016-10-01": - if include_pending is not None: - raise NotImplementedError( - "The 'include_pending' parameter to `list_properties_of_certificates` " - "is only available for API versions v7.0 and up" - ) - else: - kwargs.update({"include_pending": include_pending}) - - return self._client.get_certificates( - maxresults=max_page_size, - cls=lambda objs: [CertificateProperties._from_certificate_item(x) for x in objs], - **kwargs - ) - - @distributed_trace - def list_properties_of_certificate_versions( - self, certificate_name: str, **kwargs: Any - ) -> AsyncItemPaged[CertificateProperties]: - """List the identifiers and properties of a certificate's versions. - - Requires certificates/list permission. - - :param str certificate_name: The name of the certificate. - - :returns: An iterator-like instance of CertificateProperties - :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateProperties] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START list_properties_of_certificate_versions] - :end-before: [END list_properties_of_certificate_versions] - :language: python - :caption: List all versions of a certificate - :dedent: 8 - """ - max_page_size = kwargs.pop("max_page_size", None) - return self._client.get_certificate_versions( - certificate_name=certificate_name, - maxresults=max_page_size, - cls=lambda objs: [CertificateProperties._from_certificate_item(x) for x in objs], - **kwargs - ) - - @distributed_trace_async - async def set_contacts(self, contacts: List[CertificateContact], **kwargs: Any) -> List[CertificateContact]: - """Sets the certificate contacts for the key vault. Requires certificates/managecontacts permission. - - :param contacts: The contact list for the vault certificates. - :type contacts: list[~azure.keyvault.certificates.CertificateContact] - - :returns: The created list of contacts - :rtype: list[~azure.keyvault.certificates.CertificateContact] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START set_contacts] - :end-before: [END set_contacts] - :language: python - :caption: Create contacts - :dedent: 8 - """ - new_contacts = await self._client.set_certificate_contacts( - contacts=self._models.Contacts(contact_list=[c._to_certificate_contacts_item() for c in contacts]), - **kwargs - ) - return [ - CertificateContact._from_certificate_contacts_item(contact_item=item) for item in new_contacts.contact_list - ] - - @distributed_trace_async - async def get_contacts(self, **kwargs: Any) -> List[CertificateContact]: - """Gets the certificate contacts for the key vault. Requires the certificates/managecontacts permission. - - :return: The certificate contacts for the key vault. - :rtype: list[azure.keyvault.certificates.CertificateContact] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START get_contacts] - :end-before: [END get_contacts] - :language: python - :caption: Get contacts - :dedent: 8 - """ - contacts = await self._client.get_certificate_contacts( **kwargs - ) - return [CertificateContact._from_certificate_contacts_item(contact_item=item) for item in contacts.contact_list] - - @distributed_trace_async - async def delete_contacts(self, **kwargs: Any) -> List[CertificateContact]: - """Deletes the certificate contacts for the key vault. Requires the certificates/managecontacts permission. - - :return: The deleted contacts for the key vault. - :rtype: list[~azure.keyvault.certificates.CertificateContact] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START delete_contacts] - :end-before: [END delete_contacts] - :language: python - :caption: Delete contacts - :dedent: 8 - """ - contacts = await self._client.delete_certificate_contacts( - **kwargs - ) - return [CertificateContact._from_certificate_contacts_item(contact_item=item) for item in contacts.contact_list] - - @distributed_trace_async - async def get_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: - """Gets the creation operation of a certificate. Requires the certificates/get permission. - - :param str certificate_name: The name of the certificate. - - :returns: The created CertificateOperation - :rtype: ~azure.keyvault.certificates.CertificateOperation - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the certificate doesn't exist; the latter for other errors - """ - - bundle = await self._client.get_certificate_operation( - certificate_name=certificate_name, **kwargs - ) - return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) - - @distributed_trace_async - async def delete_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: - """Deletes and stops the creation operation for a specific certificate. - - Requires the certificates/update permission. - - :param str certificate_name: The name of the certificate. - - :return: The deleted CertificateOperation - :rtype: ~azure.keyvault.certificates.CertificateOperation - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the operation doesn't exist; the latter for other errors - """ - bundle = await self._client.delete_certificate_operation( - certificate_name=certificate_name, **kwargs - ) - return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) - - @distributed_trace_async - async def cancel_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: - """Cancels an in-progress certificate operation. Requires the certificates/update permission. - - :param str certificate_name: The name of the certificate. - - :returns: The cancelled certificate operation - :rtype: ~azure.keyvault.certificates.CertificateOperation - - :raises ~azure.core.exceptions.HttpResponseError: - """ - bundle = await self._client.update_certificate_operation( - certificate_name=certificate_name, - certificate_operation=self._models.CertificateOperationUpdateParameter(cancellation_requested=True), - **kwargs - ) - return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) - - @distributed_trace_async - async def merge_certificate( - self, - certificate_name: str, - x509_certificates: List[bytes], - *, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - **kwargs: Any, - ) -> KeyVaultCertificate: - """Merges a certificate or a certificate chain with a key pair existing on the server. - - Requires the certificates/create permission. Performs the merging of a certificate or certificate chain with a - key pair currently available in the service. Make sure when creating the certificate to merge using - :func:`create_certificate` that you set its issuer to 'Unknown'. This way Key Vault knows that the - certificate will not be signed by an issuer known to it. - - :param str certificate_name: The name of the certificate - :param x509_certificates: The certificate or the certificate chain to merge. - :type x509_certificates: list[bytes] - - :keyword bool enabled: Whether the certificate is enabled for use. - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] - - :return: The merged certificate operation - :rtype: ~azure.keyvault.certificates.CertificateOperation - - :raises ~azure.core.exceptions.HttpResponseError: - """ - - if enabled is not None: - attributes = self._models.CertificateAttributes(enabled=enabled) - else: - attributes = None - - parameters = self._models.CertificateMergeParameters( - x509_certificates=x509_certificates, certificate_attributes=attributes, tags=tags - ) - - bundle = await self._client.merge_certificate( - certificate_name=certificate_name, - parameters=parameters, - **kwargs - ) - return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) - - @distributed_trace_async - async def get_issuer(self, issuer_name: str, **kwargs: Any) -> CertificateIssuer: - """Gets the specified certificate issuer. Requires certificates/manageissuers/getissuers permission. - - :param str issuer_name: The name of the issuer. - - :return: The specified certificate issuer. - :rtype: ~azure.keyvault.certificates.CertificateIssuer - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the issuer doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START get_issuer] - :end-before: [END get_issuer] - :language: python - :caption: Get an issuer - :dedent: 8 - """ - issuer_bundle = await self._client.get_certificate_issuer( - issuer_name=issuer_name, **kwargs - ) - return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) - - @distributed_trace_async - async def create_issuer( - self, - issuer_name: str, - provider: str, - *, - enabled: Optional[bool] = None, - account_id: Optional[str] = None, - password: Optional[str] = None, - organization_id: Optional[str] = None, - admin_contacts: Optional[List[AdministratorContact]] = None, - **kwargs: Any, - ) -> CertificateIssuer: - """Sets the specified certificate issuer. Requires certificates/setissuers permission. - - :param str issuer_name: The name of the issuer. - :param str provider: The issuer provider. - - :keyword bool enabled: Whether the issuer is enabled for use. - :keyword str account_id: The user name/account name/account id. - :keyword str password: The password/secret/account key. - :keyword str organization_id: Id of the organization - :keyword admin_contacts: Contact details of the organization administrators of the - certificate issuer. - :paramtype admin_contacts: list[~azure.keyvault.certificates.AdministratorContact] - - :returns: The created CertificateIssuer - :rtype: ~azure.keyvault.certificates.CertificateIssuer - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START create_issuer] - :end-before: [END create_issuer] - :language: python - :caption: Create an issuer - :dedent: 8 - """ - - if account_id or password: - issuer_credentials = self._models.IssuerCredentials(account_id=account_id, password=password) - else: - issuer_credentials = None - if admin_contacts: - admin_details: Optional[List[Any]] = [ - self._models.AdministratorDetails( - first_name=contact.first_name, - last_name=contact.last_name, - email_address=contact.email, - phone=contact.phone, - ) - for contact in admin_contacts - ] - else: - admin_details = None - if organization_id or admin_details: - organization_details = self._models.OrganizationDetails(id=organization_id, admin_details=admin_details) - else: - organization_details = None - if enabled is not None: - issuer_attributes = self._models.IssuerAttributes(enabled=enabled) - else: - issuer_attributes = None - - parameters = self._models.CertificateIssuerSetParameters( - provider=provider, - credentials=issuer_credentials, - organization_details=organization_details, - attributes=issuer_attributes, - ) - - issuer_bundle = await self._client.set_certificate_issuer( - issuer_name=issuer_name, parameter=parameters, **kwargs - ) - return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) - - @distributed_trace_async - async def update_issuer( - self, - issuer_name: str, - *, - enabled: Optional[bool] = None, - provider: Optional[str] = None, - account_id: Optional[str] = None, - password: Optional[str] = None, - organization_id: Optional[str] = None, - admin_contacts: Optional[List[AdministratorContact]] = None, - **kwargs: Any, - ) -> CertificateIssuer: - """Updates the specified certificate issuer. Requires certificates/setissuers permission. - - :param str issuer_name: The name of the issuer. - - :keyword bool enabled: Whether the issuer is enabled for use. - :keyword str provider: The issuer provider - :keyword str account_id: The user name/account name/account id. - :keyword str password: The password/secret/account key. - :keyword str organization_id: Id of the organization - :keyword admin_contacts: Contact details of the organization administrators of - the certificate issuer - :paramtype admin_contacts: list[~azure.keyvault.certificates.AdministratorContact] - - :return: The updated issuer - :rtype: ~azure.keyvault.certificates.CertificateIssuer - - :raises ~azure.core.exceptions.HttpResponseError: - """ - - if account_id or password: - issuer_credentials = self._models.IssuerCredentials(account_id=account_id, password=password) - else: - issuer_credentials = None - if admin_contacts: - admin_details: Optional[List[Any]] = list( - self._models.AdministratorDetails( - first_name=contact.first_name, - last_name=contact.last_name, - email_address=contact.email, - phone=contact.phone, - ) - for contact in admin_contacts - ) - else: - admin_details = None - if organization_id or admin_details: - organization_details = self._models.OrganizationDetails(id=organization_id, admin_details=admin_details) - else: - organization_details = None - if enabled is not None: - issuer_attributes = self._models.IssuerAttributes(enabled=enabled) - else: - issuer_attributes = None - - parameters = self._models.CertificateIssuerUpdateParameters( - provider=provider, - credentials=issuer_credentials, - organization_details=organization_details, - attributes=issuer_attributes, - ) - - issuer_bundle = await self._client.update_certificate_issuer( - issuer_name=issuer_name, parameter=parameters, **kwargs - ) - return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) - - @distributed_trace_async - async def delete_issuer(self, issuer_name: str, **kwargs: Any) -> CertificateIssuer: - """Deletes the specified certificate issuer. - - Requires certificates/manageissuers/deleteissuers permission. - - :param str issuer_name: The name of the issuer. - - :return: CertificateIssuer - :rtype: ~azure.keyvault.certificates.CertificateIssuer - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START delete_issuer] - :end-before: [END delete_issuer] - :language: python - :caption: Delete an issuer - :dedent: 8 - """ - issuer_bundle = await self._client.delete_certificate_issuer( - issuer_name=issuer_name, **kwargs - ) - return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) - - @distributed_trace - def list_properties_of_issuers(self, **kwargs: Any) -> AsyncItemPaged[IssuerProperties]: - """Lists properties of the certificate issuers for the key vault. - - Requires the certificates/manageissuers/getissuers permission. - - :return: An iterator-like instance of Issuers - :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateIssuer] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_examples_certificates_async.py - :start-after: [START list_properties_of_issuers] - :end-before: [END list_properties_of_issuers] - :language: python - :caption: List issuers of a vault - :dedent: 8 - """ - max_page_size = kwargs.pop("max_page_size", None) - return self._client.get_certificate_issuers( - maxresults=max_page_size, - cls=lambda objs: [IssuerProperties._from_issuer_item(x) for x in objs], - **kwargs - ) - - async def __aenter__(self) -> "CertificateClient": - await self._client.__aenter__() - return self diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_polling_async.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_polling_async.py deleted file mode 100644 index dbfe2f4bd0e3..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_polling_async.py +++ /dev/null @@ -1,57 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import Any, Callable, cast, Optional, Union - -from azure.core.pipeline import PipelineResponse -from azure.core.pipeline.transport import AsyncHttpTransport -from azure.core.polling import AsyncPollingMethod - -from .._models import KeyVaultCertificate, CertificateOperation - - -class CreateCertificatePollerAsync(AsyncPollingMethod): - def __init__( - self, pipeline_response: PipelineResponse, get_certificate_command: Callable, interval: int = 5 - ) -> None: - self._pipeline_response = pipeline_response - self._command: Optional[Callable] = None - self._resource: Optional[Union[CertificateOperation, KeyVaultCertificate]] = None - self._pending_certificate_op: Optional[CertificateOperation] = None - self._get_certificate_command = get_certificate_command - self._polling_interval = interval - - async def _update_status(self) -> None: - self._pending_certificate_op = await self._command() if self._command else None - - def initialize(self, client: Any, initial_response: Any, _: Callable) -> None: - self._command = client - self._pending_certificate_op = initial_response - - async def run(self) -> None: - while not self.finished(): - await self._update_status() - if not self.finished(): - # We should always ask the client's transport to sleep, instead of sleeping directly - transport: AsyncHttpTransport = cast(AsyncHttpTransport, self._pipeline_response.context.transport) - await transport.sleep(self._polling_interval) - operation = self._pending_certificate_op - if operation and operation.status and operation.status.lower() == "completed": - self._resource = await self._get_certificate_command() - else: - self._resource = self._pending_certificate_op - - def finished(self) -> bool: - operation = self._pending_certificate_op - if operation and operation.issuer_name and operation.issuer_name.lower() == "unknown": - # Because we've finished, self._resource won't be set by the run method; set it here so we don't return None - self._resource = self._pending_certificate_op - return True - return self._pending_certificate_op.status.lower() != "inprogress" # type: ignore - - def resource(self) -> Union[KeyVaultCertificate, CertificateOperation]: - return self._resource # type: ignore - - def status(self) -> str: - return self._pending_certificate_op.status.lower() # type: ignore diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/py.typed b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/py.typed deleted file mode 100644 index e5aff4f83af8..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/py.typed +++ /dev/null @@ -1 +0,0 @@ -# Marker file for PEP 561. \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations.py b/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations.py index c6e83e410383..5f9eff53f9ef 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations_async.py index 8ed40e3b8210..e85cd9d5967c 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/contacts.py b/sdk/keyvault/azure-keyvault-certificates/samples/contacts.py index 0c518e230ecf..2cb45aa52862 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/contacts.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/contacts.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/contacts_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/contacts_async.py index 994b7db23004..fc05c0f3ebe4 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/contacts_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/contacts_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/hello_world.py b/sdk/keyvault/azure-keyvault-certificates/samples/hello_world.py index b2df23b1a491..baeaabff1e42 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/hello_world.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/hello_world.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/hello_world_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/hello_world_async.py index d712b51e5b82..826fbba557d3 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/hello_world_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/hello_world_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate.py b/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate.py index 3819b769b131..a886ba8f099f 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate_async.py index 150d071788d1..de8b3619d08b 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/issuers.py b/sdk/keyvault/azure-keyvault-certificates/samples/issuers.py index 57936e4f917f..e7babf5e0658 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/issuers.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/issuers.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/issuers_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/issuers_async.py index 9aed2e1af2fe..393c1ffbcaef 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/issuers_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/issuers_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/list_operations.py b/sdk/keyvault/azure-keyvault-certificates/samples/list_operations.py index 8345036baedc..21ac303178e6 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/list_operations.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/list_operations.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/list_operations_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/list_operations_async.py index 54d21eb47342..21d92d58ed7b 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/list_operations_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/list_operations_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate.py b/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate.py index a062bf199127..f38aaf1c87d4 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate_async.py index 6e9065b66684..5d3dd7b3521a 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations.py b/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations.py index ae17348dbdd7..6750e7695282 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations_async.py index 9c8ceb14ccb4..edd06b98d307 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/sdk_packaging.toml b/sdk/keyvault/azure-keyvault-certificates/sdk_packaging.toml deleted file mode 100644 index e7687fdae93b..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/sdk_packaging.toml +++ /dev/null @@ -1,2 +0,0 @@ -[packaging] -auto_update = false \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault-certificates/setup.py b/sdk/keyvault/azure-keyvault-certificates/setup.py new file mode 100644 index 000000000000..7343c53938ad --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/setup.py @@ -0,0 +1,71 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# Code generated by Microsoft (R) Python Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. +# -------------------------------------------------------------------------- + + +import os +import re +from setuptools import setup, find_packages + +PACKAGE_NAME = "azure-keyvault-certificates" +PACKAGE_PPRINT_NAME = "Key Vault Certificates" +PACKAGE_NAMESPACE = "azure.keyvault.certificates._generated" + +# a.b.c => a/b/c +package_folder_path = PACKAGE_NAMESPACE.replace(".", "/") + +# Version extraction inspired from 'requests' +with open(os.path.join(package_folder_path, "_version.py"), "r") as fd: + version = re.search(r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) + +if not version: + raise RuntimeError("Cannot find version information") + + +setup( + name=PACKAGE_NAME, + version=version, + description="Microsoft Corporation {} Client Library for Python".format(PACKAGE_PPRINT_NAME), + long_description=open("README.md", "r").read(), + long_description_content_type="text/markdown", + license="MIT License", + author="Microsoft Corporation", + author_email="azpysdkhelp@microsoft.com", + url="https://github.com/Azure/azure-sdk-for-python/tree/main/sdk", + keywords="azure, azure sdk", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "License :: OSI Approved :: MIT License", + ], + zip_safe=False, + packages=find_packages( + exclude=[ + "tests", + # Exclude packages that will be covered by PEP420 or nspkg + "azure", + "azure.keyvault", + "azure.keyvault.certificates", + ] + ), + include_package_data=True, + package_data={ + "azure.keyvault.certificates._generated": ["py.typed"], + }, + install_requires=[ + "isodate>=0.6.1", + "azure-core>=1.37.0", + "typing-extensions>=4.6.0", + ], + python_requires=">=3.10", +) diff --git a/sdk/keyvault/azure-keyvault-certificates/tests/certs.py b/sdk/keyvault/azure-keyvault-certificates/tests/certs.py index b369590befcc..c94546338d86 100644 --- a/sdk/keyvault/azure-keyvault-certificates/tests/certs.py +++ b/sdk/keyvault/azure-keyvault-certificates/tests/certs.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client.py b/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client.py index 64328d69d73f..d5101c2909f2 100644 --- a/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client.py +++ b/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -42,7 +43,6 @@ from _test_case import get_decorator, CertificatesClientPreparer from certs import CERT_CONTENT_PASSWORD_ENCODED, CERT_CONTENT_NOT_PASSWORD_ENCODED - all_api_versions = get_decorator() only_latest = get_decorator(api_versions=[DEFAULT_VERSION]) logging_enabled = get_decorator(logging_enable=True) @@ -817,9 +817,7 @@ def test_unknown_issuer_response(self, client, **kwargs): content_type=CertificateContentType.pkcs12, validity_in_months=24, ) - create_certificate_poller = client.begin_create_certificate( - certificate_name=cert_name, policy=cert_policy - ) + create_certificate_poller = client.begin_create_certificate(certificate_name=cert_name, policy=cert_policy) result = create_certificate_poller.result() # The operation should indicate that certificate creation is in progress and requires a merge to complete assert isinstance(result, CertificateOperation) diff --git a/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client_async.py b/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client_async.py index 6333bdeab1b9..7f011297d035 100644 --- a/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -39,7 +40,6 @@ from _test_case import get_decorator from certs import CERT_CONTENT_PASSWORD_ENCODED, CERT_CONTENT_NOT_PASSWORD_ENCODED - all_api_versions = get_decorator() only_latest = get_decorator(api_versions=[DEFAULT_VERSION]) logging_enabled = get_decorator(logging_enable=True) diff --git a/sdk/keyvault/azure-keyvault-certificates/tests/test_examples_certificates_async.py b/sdk/keyvault/azure-keyvault-certificates/tests/test_examples_certificates_async.py index 821e3091c614..1a16fdd92723 100644 --- a/sdk/keyvault/azure-keyvault-certificates/tests/test_examples_certificates_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/tests/test_examples_certificates_async.py @@ -12,7 +12,6 @@ from _test_case import get_decorator from devtools_testutils.aio import recorded_by_proxy_async - all_api_versions = get_decorator(is_async=True) exclude_2016_10_01 = get_decorator(is_async=True, api_versions=[v for v in ApiVersion if v != ApiVersion.V2016_10_01]) diff --git a/sdk/keyvault/azure-keyvault-certificates/tsp-location.yaml b/sdk/keyvault/azure-keyvault-certificates/tsp-location.yaml index aeabe2d8b1b5..f86768721791 100644 --- a/sdk/keyvault/azure-keyvault-certificates/tsp-location.yaml +++ b/sdk/keyvault/azure-keyvault-certificates/tsp-location.yaml @@ -1,5 +1,5 @@ directory: specification/keyvault/data-plane/Certificates -commit: 80cf521033f9499c3c86305d11c7de6451e796e5 +commit: 44729a46b902922d8f50a383573f9c6641ce8e74 repo: Azure/azure-rest-api-specs additionalDirectories: - specification/keyvault/data-plane/Certificates/common From 77a3cf52ccf31141436827dc7481045a678c930f Mon Sep 17 00:00:00 2001 From: Nicholas Noboa Date: Mon, 18 May 2026 12:08:09 -0700 Subject: [PATCH 2/4] Restore handwritten files, add PlatformManaged tests and changelog --- .../azure-keyvault-certificates/CHANGELOG.md | 7 +- .../azure-keyvault-certificates/MANIFEST.in | 2 +- .../azure/__init__.py | 4 + .../azure/keyvault/__init__.py | 4 + .../azure/keyvault/certificates/__init__.py | 57 +- .../azure/keyvault/certificates/_client.py | 1079 ++++++++++++ .../azure/keyvault/certificates/_enums.py | 77 + .../azure/keyvault/certificates/_models.py | 1560 +++++++++++++++++ .../azure/keyvault/certificates/_polling.py | 56 + .../keyvault/certificates/_sdk_moniker.py | 7 + .../keyvault/certificates/_shared/__init__.py | 78 + .../keyvault/certificates/_shared/_polling.py | 136 ++ .../certificates/_shared/_polling_async.py | 80 + .../_shared/async_challenge_auth_policy.py | 262 +++ .../certificates/_shared/async_client_base.py | 117 ++ .../_shared/challenge_auth_policy.py | 270 +++ .../certificates/_shared/client_base.py | 162 ++ .../certificates/_shared/http_challenge.py | 186 ++ .../_shared/http_challenge_cache.py | 93 + .../azure/keyvault/certificates/_version.py | 6 + .../keyvault/certificates/aio/__init__.py | 8 + .../keyvault/certificates/aio/_client.py | 1077 ++++++++++++ .../certificates/aio/_polling_async.py | 57 + .../azure/keyvault/certificates/py.typed | 1 + .../samples/backup_restore_operations.py | 1 - .../backup_restore_operations_async.py | 1 - .../samples/contacts.py | 1 - .../samples/contacts_async.py | 1 - .../samples/hello_world.py | 1 - .../samples/hello_world_async.py | 1 - .../samples/import_certificate.py | 1 - .../samples/import_certificate_async.py | 1 - .../samples/issuers.py | 1 - .../samples/issuers_async.py | 1 - .../samples/list_operations.py | 1 - .../samples/list_operations_async.py | 1 - .../samples/parse_certificate.py | 1 - .../samples/parse_certificate_async.py | 1 - .../samples/recover_purge_operations.py | 1 - .../samples/recover_purge_operations_async.py | 1 - .../sdk_packaging.toml | 2 + .../azure-keyvault-certificates/setup.py | 71 - .../tests/certs.py | 1 - .../tests/test_certificates_client.py | 6 +- .../tests/test_certificates_client_async.py | 2 +- .../tests/test_examples_certificates_async.py | 1 + .../tests/test_platform_managed.py | 100 ++ 47 files changed, 5488 insertions(+), 97 deletions(-) create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_client.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_enums.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_polling.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_sdk_moniker.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling_async.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_client_base.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/client_base.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge_cache.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_version.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/__init__.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_client.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_polling_async.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/py.typed create mode 100644 sdk/keyvault/azure-keyvault-certificates/sdk_packaging.toml delete mode 100644 sdk/keyvault/azure-keyvault-certificates/setup.py create mode 100644 sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py diff --git a/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md b/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md index b60a9a1cf7b8..7dacdd9c9545 100644 --- a/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md @@ -1,13 +1,12 @@ # Release History -## 4.12.0b1 (2026-05-18) - -skip changelog generation for data-plane package and please add changelog manually. - ## 4.11.2 (Unreleased) ### Features Added +- Added support for service API version `2026-03-01-preview` +- Added `PlatformManaged` property to `CertificatePolicy` + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/keyvault/azure-keyvault-certificates/MANIFEST.in b/sdk/keyvault/azure-keyvault-certificates/MANIFEST.in index a39bafcb9a1a..7193a4bc539c 100644 --- a/sdk/keyvault/azure-keyvault-certificates/MANIFEST.in +++ b/sdk/keyvault/azure-keyvault-certificates/MANIFEST.in @@ -1,6 +1,6 @@ include *.md include LICENSE -include azure/keyvault/certificates/_generated/py.typed +include azure/keyvault/certificates/py.typed recursive-include tests *.py recursive-include samples *.py *.md include azure/__init__.py diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/__init__.py index d55ccad1f573..679ab6995134 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/__init__.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/__init__.py @@ -1 +1,5 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ __path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/__init__.py index d55ccad1f573..679ab6995134 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/__init__.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/__init__.py @@ -1 +1,5 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ __path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py index d55ccad1f573..3e8da4143494 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py @@ -1 +1,56 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from ._client import CertificateClient +from ._enums import( + CertificatePolicyAction, + KeyCurveName, + KeyType, + CertificateContentType, + KeyUsageType, + WellKnownIssuerNames +) +from ._models import( + AdministratorContact, + CertificateContact, + CertificateIssuer, + CertificateOperation, + CertificateOperationError, + CertificatePolicy, + CertificateProperties, + DeletedCertificate, + IssuerProperties, + LifetimeAction, + KeyVaultCertificate, + KeyVaultCertificateIdentifier +) +from ._shared.client_base import ApiVersion + +__all__ = [ + "ApiVersion", + "CertificatePolicyAction", + "AdministratorContact", + "CertificateClient", + "CertificateContact", + "CertificateIssuer", + "CertificateOperation", + "CertificateOperationError", + "CertificatePolicy", + "CertificateProperties", + "DeletedCertificate", + "IssuerProperties", + "KeyCurveName", + "KeyType", + "KeyVaultCertificate", + "KeyVaultCertificateIdentifier", + "KeyUsageType", + "LifetimeAction", + "CertificateContentType", + "WellKnownIssuerNames", + "CertificateIssuer", + "IssuerProperties" +] + +from ._version import VERSION +__version__ = VERSION diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_client.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_client.py new file mode 100644 index 000000000000..6ce432f1874d --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_client.py @@ -0,0 +1,1079 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +# pylint:disable=too-many-lines,too-many-public-methods,bad-option-value,delete-operation-wrong-return-type +import base64 +from functools import partial +from typing import Any, Dict, List, Optional, Union + +from azure.core.polling import LROPoller +from azure.core.paging import ItemPaged +from azure.core.tracing.decorator import distributed_trace + +from ._shared import KeyVaultClientBase +from ._shared._polling import DeleteRecoverPollingMethod, KeyVaultOperationPoller +from ._models import ( + AdministratorContact, + KeyVaultCertificate, + CertificateProperties, + CertificatePolicy, + DeletedCertificate, + CertificateIssuer, + IssuerProperties, + CertificateContact, + CertificateOperation, +) +from ._polling import CreateCertificatePoller + + +NO_SAN_OR_SUBJECT = "You need to set either subject or one of the subject alternative names parameters in the policy" + + +class CertificateClient(KeyVaultClientBase): + """A high-level interface for managing a vault's certificates. + + :param str vault_url: URL of the vault the client will access. This is also called the vault's "DNS Name". + You should validate that this URL references a valid Key Vault resource. See https://aka.ms/azsdk/blog/vault-uri + for details. + :param credential: An object which can provide an access token for the vault, such as a credential from + :mod:`azure.identity` + :type credential: ~azure.core.credentials.TokenCredential + + :keyword api_version: Version of the service API to use. Defaults to the most recent. + :paramtype api_version: ~azure.keyvault.certificates.ApiVersion or str + :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key + Vault domain. Defaults to True. + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START create_certificate_client] + :end-before: [END create_certificate_client] + :language: python + :caption: Create a new ``CertificateClient`` + :dedent: 4 + """ + + # pylint:disable=protected-access + + @distributed_trace + def begin_create_certificate( + self, + certificate_name: str, + policy: CertificatePolicy, + *, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + preserve_order: Optional[bool] = None, + **kwargs: Any, + ) -> LROPoller[Union[KeyVaultCertificate, CertificateOperation]]: + """Creates a new certificate. + + If this is the first version, the certificate resource is created. This operation requires the + certificates/create permission. Waiting on the returned poller requires the certificates/get permission and + gives you the certificate if creation is successful, or the CertificateOperation if not -- otherwise, it raises + an :class:`~azure.core.exceptions.HttpResponseError`. + + :param str certificate_name: The name of the certificate. + :param policy: The management policy for the certificate. Either subject or one of the subject alternative + name properties are required. + :type policy: ~azure.keyvault.certificates.CertificatePolicy + + :keyword bool enabled: Whether the certificate is enabled for use. + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] + :keyword bool preserve_order: Whether to preserve the order of the certificate chain. + + :returns: An LROPoller for the create certificate operation. Waiting on the poller gives you the certificate if + creation is successful, or the CertificateOperation if not. + :rtype: ~azure.core.polling.LROPoller[~azure.keyvault.certificates.KeyVaultCertificate or + ~azure.keyvault.certificates.CertificateOperation] + + :raises ValueError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate policy is invalid; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START create_certificate] + :end-before: [END create_certificate] + :language: python + :caption: Create a certificate + :dedent: 8 + """ + if not ( + policy.san_emails + or policy.san_user_principal_names + or policy.san_dns_names + or policy.san_ip_addresses + or policy.san_uris + or policy.subject + ): + raise ValueError(NO_SAN_OR_SUBJECT) + + polling_interval = kwargs.pop("_polling_interval", None) + if polling_interval is None: + polling_interval = 5 + + if enabled is not None: + attributes = self._models.CertificateAttributes(enabled=enabled) + else: + attributes = None + + parameters = self._models.CertificateCreateParameters( + certificate_policy=policy._to_certificate_policy_bundle(), + certificate_attributes=attributes, + tags=tags, + preserve_cert_order=preserve_order, + ) + + pipeline_response, cert_bundle = self._client.create_certificate( + certificate_name=certificate_name, + parameters=parameters, + cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), + **kwargs, + ) + + create_certificate_operation = CertificateOperation._from_certificate_operation_bundle(cert_bundle) + + command = partial(self.get_certificate_operation, certificate_name=certificate_name, **kwargs) + + get_certificate_command = partial(self.get_certificate, certificate_name=certificate_name, **kwargs) + + create_certificate_polling = CreateCertificatePoller( + pipeline_response=pipeline_response, + get_certificate_command=get_certificate_command, + interval=polling_interval + ) + + def no_op(*_, **__) -> Any: # The deserialization callback is ignored based on polling implementation + pass + + return LROPoller(command, create_certificate_operation, no_op, create_certificate_polling) + + @distributed_trace + def get_certificate(self, certificate_name: str, **kwargs: Any) -> KeyVaultCertificate: + """Gets a certificate with its management policy attached. Requires certificates/get permission. + + Does not accept the version of the certificate as a parameter. To get a specific version of the + certificate, call :func:`get_certificate_version`. + + :param str certificate_name: The name of the certificate in the given vault. + + :returns: An instance of KeyVaultCertificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START get_certificate] + :end-before: [END get_certificate] + :language: python + :caption: Get a certificate + :dedent: 8 + """ + bundle = self._client.get_certificate( + certificate_name=certificate_name, certificate_version="", **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace + def get_certificate_version(self, certificate_name: str, version: str, **kwargs: Any) -> KeyVaultCertificate: + """Gets a specific version of a certificate without returning its management policy. + + Requires certificates/get permission. To get the latest version of the certificate, or to get the certificate's + policy as well, call :func:`get_certificate`. + + :param str certificate_name: The name of the certificate in the given vault. + :param str version: The version of the certificate. + + :returns: An instance of KeyVaultCertificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START get_certificate_version] + :end-before: [END get_certificate_version] + :language: python + :caption: Get a certificate with a specific version + :dedent: 8 + """ + bundle = self._client.get_certificate( + certificate_name=certificate_name, certificate_version=version, **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace + def begin_delete_certificate(self, certificate_name: str, **kwargs: Any) -> LROPoller[DeletedCertificate]: + """Delete all versions of a certificate. Requires certificates/delete permission. + + When this method returns Key Vault has begun deleting the certificate. Deletion may take several seconds in a + vault with soft-delete enabled. This method therefore returns a poller enabling you to wait for deletion to + complete. + + :param str certificate_name: The name of the certificate to delete. + + :returns: A poller for the delete certificate operation. The poller's `result` method returns the + :class:`~azure.keyvault.certificates.DeletedCertificate` without waiting for deletion to complete. If the + vault has soft-delete enabled and you want to immediately, permanently delete the certificate with + :func:`purge_deleted_certificate`, call the poller's `wait` method first. It will block until the deletion + is complete. The `wait` method requires certificates/get permission. + :rtype: ~azure.core.polling.LROPoller[~azure.keyvault.certificates.DeletedCertificate] + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START delete_certificate] + :end-before: [END delete_certificate] + :language: python + :caption: Delete a certificate + :dedent: 8 + """ + polling_interval = kwargs.pop("_polling_interval", None) + if polling_interval is None: + polling_interval = 2 + pipeline_response, deleted_cert_bundle = self._client.delete_certificate( + certificate_name=certificate_name, + cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), + **kwargs, + ) + deleted_cert = DeletedCertificate._from_deleted_certificate_bundle(deleted_cert_bundle) + + polling_method = DeleteRecoverPollingMethod( + # no recovery ID means soft-delete is disabled, in which case we initialize the poller as finished + finished=deleted_cert.recovery_id is None, + pipeline_response=pipeline_response, + command=partial(self.get_deleted_certificate, certificate_name=certificate_name, **kwargs), + final_resource=deleted_cert, + interval=polling_interval, + ) + + return KeyVaultOperationPoller(polling_method) + + @distributed_trace + def get_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> DeletedCertificate: + """Get a deleted certificate. Possible only in a vault with soft-delete enabled. + + Requires certificates/get permission. Retrieves the deleted certificate information plus its attributes, such as + retention interval, scheduled permanent deletion, and the current deletion recovery level. + + :param str certificate_name: The name of the certificate. + + :return: The deleted certificate + :rtype: ~azure.keyvault.certificates.DeletedCertificate + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START get_deleted_certificate] + :end-before: [END get_deleted_certificate] + :language: python + :caption: Get a deleted certificate + :dedent: 8 + """ + bundle = self._client.get_deleted_certificate( + certificate_name=certificate_name, **kwargs + ) + return DeletedCertificate._from_deleted_certificate_bundle(deleted_certificate_bundle=bundle) + + @distributed_trace + def purge_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> None: + """Permanently deletes a deleted certificate. Possible only in vaults with soft-delete enabled. + + Requires certificates/purge permission. Performs an irreversible deletion of the specified certificate, without + possibility for recovery. The operation is not available if the + :py:attr:`~azure.keyvault.certificates.CertificateProperties.recovery_level` does not specify 'Purgeable'. + This method is only necessary for purging a certificate before its + :py:attr:`~azure.keyvault.certificates.DeletedCertificate.scheduled_purge_date`. + + :param str certificate_name: The name of the certificate + + :return: None + :rtype: None + + :raises ~azure.core.exceptions.HttpResponseError: + """ + self._client.purge_deleted_certificate( + certificate_name=certificate_name, **kwargs + ) + + @distributed_trace + def begin_recover_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> LROPoller[KeyVaultCertificate]: + """Recover a deleted certificate to its latest version. Possible only in a vault with soft-delete enabled. + + Requires certificates/recover permission. When this method returns Key Vault has begun recovering the + certificate. Recovery may take several seconds. This method therefore returns a poller enabling you to wait for + recovery to complete. Waiting is only necessary when you want to use the recovered certificate in another + operation immediately. + + :param str certificate_name: The name of the deleted certificate to recover + + :returns: A poller for the recovery operation. The poller's `result` method returns the recovered + :class:`~azure.keyvault.certificates.KeyVaultCertificate` without waiting for recovery to complete. If you + want to use the recovered certificate immediately, call the poller's `wait` method, which blocks until the + certificate is ready to use. The `wait` method requires certificate/get permission. + :rtype: ~azure.core.polling.LROPoller[~azure.keyvault.certificates.KeyVaultCertificate] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START recover_deleted_certificate] + :end-before: [END recover_deleted_certificate] + :language: python + :caption: Recover a deleted certificate + :dedent: 8 + """ + polling_interval = kwargs.pop("_polling_interval", None) + if polling_interval is None: + polling_interval = 2 + + pipeline_response, recovered_cert_bundle = self._client.recover_deleted_certificate( + certificate_name=certificate_name, + cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), + **kwargs, + ) + recovered_certificate = KeyVaultCertificate._from_certificate_bundle(recovered_cert_bundle) + command = partial(self.get_certificate, certificate_name=certificate_name, **kwargs) + polling_method = DeleteRecoverPollingMethod( + finished=False, + pipeline_response=pipeline_response, + command=command, + final_resource=recovered_certificate, + interval=polling_interval + ) + + return KeyVaultOperationPoller(polling_method) + + @distributed_trace + def import_certificate( + self, + certificate_name: str, + certificate_bytes: bytes, + *, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + password: Optional[str] = None, + policy: Optional[CertificatePolicy] = None, + preserve_order: Optional[bool] = None, + **kwargs: Any, + ) -> KeyVaultCertificate: + """Import a certificate created externally. Requires certificates/import permission. + + Imports an existing valid certificate, containing a private key, into Azure Key Vault. The certificate to be + imported can be in either PFX or PEM format. If the certificate is in PEM format the PEM file must contain the + key as well as x509 certificates, and you must provide a ``policy`` with + :attr:`~azure.keyvault.certificates.CertificatePolicy.content_type` of + :attr:`~azure.keyvault.certificates.CertificateContentType.pem`. + + :param str certificate_name: The name of the certificate. + :param bytes certificate_bytes: Bytes of the certificate object to import. This certificate + needs to contain the private key. + + :keyword bool enabled: Whether the certificate is enabled for use. + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] + :keyword str password: If the private key in the passed in certificate is encrypted, it + is the password used for encryption. + :keyword policy: The management policy for the certificate. Required if importing a PEM-format certificate, + with :attr:`~azure.keyvault.certificates.CertificatePolicy.content_type` set to + :attr:`~azure.keyvault.certificates.CertificateContentType.pem`. + :paramtype policy: ~azure.keyvault.certificates.CertificatePolicy + :keyword bool preserve_order: Whether to preserve the order of the certificate chain. + + :returns: The imported KeyVaultCertificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.HttpResponseError: + """ + + if enabled is not None: + attributes = self._models.CertificateAttributes(enabled=enabled) + else: + attributes = None + base64_encoded_certificate = base64.b64encode(certificate_bytes).decode("utf-8") + + parameters = self._models.CertificateImportParameters( + base64_encoded_certificate=base64_encoded_certificate, + password=password, + certificate_policy=policy._to_certificate_policy_bundle() if policy else None, + certificate_attributes=attributes, + tags=tags, + preserve_cert_order=preserve_order, + ) + + bundle = self._client.import_certificate( + certificate_name=certificate_name, parameters=parameters, **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace + def get_certificate_policy(self, certificate_name: str, **kwargs: Any) -> CertificatePolicy: + """Gets the policy for a certificate. Requires certificates/get permission. + + Returns the specified certificate policy resources in the key vault. + + :param str certificate_name: The name of the certificate in a given key vault. + + :return: The certificate policy + :rtype: ~azure.keyvault.certificates.CertificatePolicy + + :raises ~azure.core.exceptions.HttpResponseError: + """ + bundle = self._client.get_certificate_policy( + certificate_name=certificate_name, **kwargs + ) + return CertificatePolicy._from_certificate_policy_bundle(certificate_policy_bundle=bundle) + + @distributed_trace + def update_certificate_policy( + self, certificate_name: str, policy: CertificatePolicy, **kwargs: Any + ) -> CertificatePolicy: + """Updates the policy for a certificate. Requires certificates/update permission. + + Set specified members in the certificate policy. Leaves others as null. + + :param str certificate_name: The name of the certificate in the given vault. + :param policy: The policy for the certificate. + :type policy: ~azure.keyvault.certificates.CertificatePolicy + + :return: The certificate policy + :rtype: ~azure.keyvault.certificates.CertificatePolicy + + :raises ~azure.core.exceptions.HttpResponseError: + """ + bundle = self._client.update_certificate_policy( + certificate_name=certificate_name, + certificate_policy=policy._to_certificate_policy_bundle(), + **kwargs + ) + return CertificatePolicy._from_certificate_policy_bundle(certificate_policy_bundle=bundle) + + @distributed_trace + def update_certificate_properties( + self, + certificate_name: str, + version: Optional[str] = None, + *, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + **kwargs: Any, + ) -> KeyVaultCertificate: + """Change a certificate's properties. Requires certificates/update permission. + + :param str certificate_name: The name of the certificate in the given key vault. + :param str version: The version of the certificate. + + :keyword bool enabled: Whether the certificate is enabled for use. + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] + + :returns: The updated KeyVaultCertificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START update_certificate] + :end-before: [END update_certificate] + :language: python + :caption: Update a certificate's attributes + :dedent: 8 + """ + + if enabled is not None: + attributes = self._models.CertificateAttributes(enabled=enabled) + else: + attributes = None + + parameters = self._models.CertificateUpdateParameters( + certificate_attributes=attributes, tags=tags + ) + + bundle = self._client.update_certificate( + certificate_name=certificate_name, + certificate_version=version or "", + parameters=parameters, + **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace + def backup_certificate(self, certificate_name: str, **kwargs: Any) -> bytes: + """Back up a certificate in a protected form useable only by Azure Key Vault. + + Requires certificates/backup permission. This is intended to allow copying a certificate from one vault to + another. Both vaults must be owned by the same Azure subscription. Also, backup / restore cannot be performed + across geopolitical boundaries. For example, a backup from a vault in a USA region cannot be restored to a vault + in an EU region. + + :param str certificate_name: The name of the certificate. + + :return: The backup blob containing the backed up certificate. + :rtype: bytes + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START backup_certificate] + :end-before: [END backup_certificate] + :language: python + :caption: Get a certificate backup + :dedent: 8 + """ + backup_result = self._client.backup_certificate( + certificate_name=certificate_name, **kwargs + ) + return backup_result.value + + @distributed_trace + def restore_certificate_backup(self, backup: bytes, **kwargs: Any) -> KeyVaultCertificate: + """Restore a certificate backup to the vault. Requires certificates/restore permission. + + This restores all versions of the certificate, with its name, attributes, and access control policies. If the + certificate's name is already in use, restoring it will fail. Also, the target vault must be owned by the same + Microsoft Azure subscription as the source vault. + + :param bytes backup: The backup blob associated with a certificate bundle. + + :return: The restored KeyVaultCertificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START restore_certificate] + :end-before: [END restore_certificate] + :language: python + :caption: Restore a certificate backup + :dedent: 8 + """ + bundle = self._client.restore_certificate( + parameters=self._models.CertificateRestoreParameters(certificate_bundle_backup=backup), + **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace + def list_deleted_certificates( + self, *, include_pending: Optional[bool] = None, **kwargs: Any + ) -> ItemPaged[DeletedCertificate]: + """Lists the currently-recoverable deleted certificates. Possible only if vault is soft-delete enabled. + + Requires certificates/get/list permission. Retrieves the certificates in the current vault which are in a + deleted state and ready for recovery or purging. This operation includes deletion-specific information. + + :keyword bool include_pending: Specifies whether to include certificates which are not completely deleted. + Only available for API versions v7.0 and up. If not provided, Key Vault treats this as False. + :paramtype include_pending: bool or None + + :return: An iterator-like instance of DeletedCertificate + :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.DeletedCertificate] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START list_deleted_certificates] + :end-before: [END list_deleted_certificates] + :language: python + :caption: List all the deleted certificates + :dedent: 8 + """ + max_page_size = kwargs.pop("max_page_size", None) + + if self.api_version == "2016-10-01": + if include_pending is not None: + raise NotImplementedError( + "The 'include_pending' parameter to `list_deleted_certificates` " + "is only available for API versions v7.0 and up" + ) + else: + kwargs.update({"include_pending": include_pending}) + + return self._client.get_deleted_certificates( + maxresults=max_page_size, + cls=lambda objs: [ + DeletedCertificate._from_deleted_certificate_item(deleted_certificate_item=x) for x in objs + ], + **kwargs + ) + + @distributed_trace + def list_properties_of_certificates( + self, *, include_pending: Optional[bool] = None, **kwargs: Any + ) -> ItemPaged[CertificateProperties]: + """List identifiers and properties of all certificates in the vault. + + Requires certificates/list permission. + + :keyword include_pending: Specifies whether to include certificates which are not completely provisioned. + Only available for API versions v7.0 and up. If not provided, Key Vault treats this as False. + :paramtype include_pending: bool or None + + :returns: An iterator-like instance of CertificateProperties + :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateProperties] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START list_properties_of_certificates] + :end-before: [END list_properties_of_certificates] + :language: python + :caption: List all certificates + :dedent: 8 + """ + max_page_size = kwargs.pop("max_page_size", None) + + if self.api_version == "2016-10-01": + if include_pending is not None: + raise NotImplementedError( + "The 'include_pending' parameter to `list_properties_of_certificates` " + "is only available for API versions v7.0 and up" + ) + else: + kwargs.update({"include_pending": include_pending}) + + return self._client.get_certificates( + maxresults=max_page_size, + cls=lambda objs: [CertificateProperties._from_certificate_item(certificate_item=x) for x in objs], + **kwargs + ) + + @distributed_trace + def list_properties_of_certificate_versions( + self, certificate_name: str, **kwargs: Any + ) -> ItemPaged[CertificateProperties]: + """List the identifiers and properties of a certificate's versions. + + Requires certificates/list permission. + + :param str certificate_name: The name of the certificate. + + :returns: An iterator-like instance of CertificateProperties + :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateProperties] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START list_properties_of_certificate_versions] + :end-before: [END list_properties_of_certificate_versions] + :language: python + :caption: List all versions of a certificate + :dedent: 8 + """ + max_page_size = kwargs.pop("max_page_size", None) + return self._client.get_certificate_versions( + certificate_name=certificate_name, + maxresults=max_page_size, + cls=lambda objs: [CertificateProperties._from_certificate_item(certificate_item=x) for x in objs], + **kwargs + ) + + @distributed_trace + def set_contacts(self, contacts: "List[CertificateContact]", **kwargs: Any) -> "List[CertificateContact]": + """Sets the certificate contacts for the key vault. Requires certificates/managecontacts permission. + + :param contacts: The contact list for the vault certificates. + :type contacts: list[~azure.keyvault.certificates.CertificateContact] + + :returns: The created list of contacts + :rtype: list[~azure.keyvault.certificates.CertificateContact] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START set_contacts] + :end-before: [END set_contacts] + :language: python + :caption: Create contacts + :dedent: 8 + """ + new_contacts = self._client.set_certificate_contacts( + contacts=self._models.Contacts(contact_list=[c._to_certificate_contacts_item() for c in contacts]), + **kwargs + ) + return [ + CertificateContact._from_certificate_contacts_item(contact_item=item) for item in new_contacts.contact_list + ] + + @distributed_trace + def get_contacts(self, **kwargs: Any) -> "List[CertificateContact]": + """Gets the certificate contacts for the key vault. Requires the certificates/managecontacts permission. + + :return: The certificate contacts for the key vault. + :rtype: list[~azure.keyvault.certificates.CertificateContact] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START get_contacts] + :end-before: [END get_contacts] + :language: python + :caption: Get contacts + :dedent: 8 + """ + contacts = self._client.get_certificate_contacts(**kwargs) + return [CertificateContact._from_certificate_contacts_item(contact_item=item) for item in contacts.contact_list] + + @distributed_trace + def delete_contacts(self, **kwargs: Any) -> "List[CertificateContact]": + """Deletes the certificate contacts for the key vault. Requires the certificates/managecontacts permission. + + :return: The deleted contacts for the key vault. + :rtype: list[~azure.keyvault.certificates.CertificateContact] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START delete_contacts] + :end-before: [END delete_contacts] + :language: python + :caption: Delete contacts + :dedent: 8 + """ + contacts = self._client.delete_certificate_contacts(**kwargs) + return [CertificateContact._from_certificate_contacts_item(contact_item=item) for item in contacts.contact_list] + + @distributed_trace + def get_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: + """Gets the creation operation of a certificate. Requires the certificates/get permission. + + :param str certificate_name: The name of the certificate. + + :returns: The created CertificateOperation + :rtype: ~azure.keyvault.certificates.CertificateOperation + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + """ + + bundle = self._client.get_certificate_operation( + certificate_name=certificate_name, **kwargs + ) + return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) + + @distributed_trace + def delete_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: + """Deletes and stops the creation operation for a specific certificate. + + Requires the certificates/update permission. + + :param str certificate_name: The name of the certificate. + + :return: The deleted CertificateOperation + :rtype: ~azure.keyvault.certificates.CertificateOperation + + :raises ~azure.core.exceptions.HttpResponseError: + """ + bundle = self._client.delete_certificate_operation( + certificate_name=certificate_name, **kwargs + ) + return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) + + @distributed_trace + def cancel_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: + """Cancels an in-progress certificate operation. Requires the certificates/update permission. + + :param str certificate_name: The name of the certificate. + + :returns: The cancelled certificate operation + :rtype: ~azure.keyvault.certificates.CertificateOperation + + :raises ~azure.core.exceptions.HttpResponseError: + """ + bundle = self._client.update_certificate_operation( + certificate_name=certificate_name, + certificate_operation=self._models.CertificateOperationUpdateParameter(cancellation_requested=True), + **kwargs + ) + return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) + + @distributed_trace + def merge_certificate( + self, + certificate_name: str, + x509_certificates: List[bytes], + *, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + **kwargs: Any, + ) -> KeyVaultCertificate: + """Merges a certificate or a certificate chain with a key pair existing on the server. + + Requires the certificates/create permission. Performs the merging of a certificate or certificate chain with a + key pair currently available in the service. Make sure when creating the certificate to merge using + :func:`begin_create_certificate` that you set its issuer to 'Unknown'. This way Key Vault knows that the + certificate will not be signed by an issuer known to it. + + :param str certificate_name: The name of the certificate + :param x509_certificates: The certificate or the certificate chain to merge. + :type x509_certificates: list[bytes] + + :keyword enabled: Whether the certificate is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + + :return: The merged certificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.HttpResponseError: + """ + + if enabled is not None: + attributes = self._models.CertificateAttributes(enabled=enabled) + else: + attributes = None + + parameters = self._models.CertificateMergeParameters( + x509_certificates=x509_certificates, certificate_attributes=attributes, tags=tags + ) + + bundle = self._client.merge_certificate( + certificate_name=certificate_name, parameters=parameters, **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace + def get_issuer(self, issuer_name: str, **kwargs: Any) -> CertificateIssuer: + """Gets the specified certificate issuer. Requires certificates/manageissuers/getissuers permission. + + :param str issuer_name: The name of the issuer. + + :return: The specified certificate issuer. + :rtype: ~azure.keyvault.certificates.CertificateIssuer + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the issuer doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START get_issuer] + :end-before: [END get_issuer] + :language: python + :caption: Get an issuer + :dedent: 8 + """ + issuer_bundle = self._client.get_certificate_issuer( + issuer_name=issuer_name, **kwargs + ) + return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) + + @distributed_trace + def create_issuer( + self, + issuer_name: str, + provider: str, + *, + enabled: Optional[bool] = None, + account_id: Optional[str] = None, + password: Optional[str] = None, + organization_id: Optional[str] = None, + admin_contacts: Optional[List[AdministratorContact]] = None, + **kwargs: Any, + ) -> CertificateIssuer: + """Sets the specified certificate issuer. Requires certificates/setissuers permission. + + :param str issuer_name: The name of the issuer. + :param str provider: The issuer provider. + + :keyword bool enabled: Whether the issuer is enabled for use. + :keyword str account_id: The user name/account name/account id. + :keyword str password: The password/secret/account key. + :keyword str organization_id: Id of the organization + :keyword admin_contacts: Contact details of the organization administrators of the + certificate issuer. + :paramtype admin_contacts: list[~azure.keyvault.certificates.AdministratorContact] + + :returns: The created CertificateIssuer + :rtype: ~azure.keyvault.certificates.CertificateIssuer + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START create_issuer] + :end-before: [END create_issuer] + :language: python + :caption: Create an issuer + :dedent: 8 + """ + + if account_id or password: + issuer_credentials = self._models.IssuerCredentials(account_id=account_id, password=password) + else: + issuer_credentials = None + if admin_contacts: + admin_details: Optional[List[Any]] = [ + self._models.AdministratorDetails( + first_name=contact.first_name, + last_name=contact.last_name, + email_address=contact.email, + phone=contact.phone, + ) + for contact in admin_contacts + ] + else: + admin_details = None + if organization_id or admin_details: + organization_details = self._models.OrganizationDetails(id=organization_id, admin_details=admin_details) + else: + organization_details = None + if enabled is not None: + issuer_attributes = self._models.IssuerAttributes(enabled=enabled) + else: + issuer_attributes = None + + parameters = self._models.CertificateIssuerSetParameters( + provider=provider, + credentials=issuer_credentials, + organization_details=organization_details, + attributes=issuer_attributes, + ) + + issuer_bundle = self._client.set_certificate_issuer( + issuer_name=issuer_name, parameter=parameters, **kwargs + ) + return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) + + @distributed_trace + def update_issuer( + self, + issuer_name: str, + *, + enabled: Optional[bool] = None, + provider: Optional[str] = None, + account_id: Optional[str] = None, + password: Optional[str] = None, + organization_id: Optional[str] = None, + admin_contacts: Optional[List[AdministratorContact]] = None, + **kwargs: Any, + ) -> CertificateIssuer: + """Updates the specified certificate issuer. Requires certificates/setissuers permission. + + :param str issuer_name: The name of the issuer. + + :keyword bool enabled: Whether the issuer is enabled for use. + :keyword str provider: The issuer provider + :keyword str account_id: The user name/account name/account id. + :keyword str password: The password/secret/account key. + :keyword str organization_id: Id of the organization + :keyword admin_contacts: Contact details of the organization administrators of the certificate issuer + :paramtype admin_contacts: list[~azure.keyvault.certificates.AdministratorContact] + + :return: The updated issuer + :rtype: ~azure.keyvault.certificates.CertificateIssuer + + :raises ~azure.core.exceptions.HttpResponseError: + """ + + if account_id or password: + issuer_credentials = self._models.IssuerCredentials(account_id=account_id, password=password) + else: + issuer_credentials = None + if admin_contacts: + admin_details: Optional[List[Any]] = [ + self._models.AdministratorDetails( + first_name=contact.first_name, + last_name=contact.last_name, + email_address=contact.email, + phone=contact.phone, + ) + for contact in admin_contacts + ] + else: + admin_details = None + if organization_id or admin_details: + organization_details = self._models.OrganizationDetails(id=organization_id, admin_details=admin_details) + else: + organization_details = None + if enabled is not None: + issuer_attributes = self._models.IssuerAttributes(enabled=enabled) + else: + issuer_attributes = None + + parameters = self._models.CertificateIssuerUpdateParameters( + provider=provider, + credentials=issuer_credentials, + organization_details=organization_details, + attributes=issuer_attributes, + ) + + issuer_bundle = self._client.update_certificate_issuer( + issuer_name=issuer_name, parameter=parameters, **kwargs + ) + return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) + + @distributed_trace + def delete_issuer(self, issuer_name: str, **kwargs: Any) -> CertificateIssuer: + """Deletes the specified certificate issuer. + + Requires certificates/manageissuers/deleteissuers permission. + + :param str issuer_name: The name of the issuer. + + :return: CertificateIssuer + :rtype: ~azure.keyvault.certificates.CertificateIssuer + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START delete_issuer] + :end-before: [END delete_issuer] + :language: python + :caption: Delete an issuer + :dedent: 8 + """ + issuer_bundle = self._client.delete_certificate_issuer( + issuer_name=issuer_name, **kwargs + ) + return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) + + @distributed_trace + def list_properties_of_issuers(self, **kwargs: Any) -> ItemPaged[IssuerProperties]: + """Lists properties of the certificate issuers for the key vault. + + Requires the certificates/manageissuers/getissuers permission. + + :return: An iterator-like instance of Issuers + :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateIssuer] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates.py + :start-after: [START list_properties_of_issuers] + :end-before: [END list_properties_of_issuers] + :language: python + :caption: List issuers of a vault + :dedent: 8 + """ + max_page_size = kwargs.pop("max_page_size", None) + return self._client.get_certificate_issuers( + maxresults=max_page_size, + cls=lambda objs: [IssuerProperties._from_issuer_item(issuer_item=x) for x in objs], + **kwargs + ) + + def __enter__(self) -> "CertificateClient": + self._client.__enter__() + return self diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_enums.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_enums.py new file mode 100644 index 000000000000..6913db4755d7 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_enums.py @@ -0,0 +1,77 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +# pylint: disable=enum-must-be-uppercase + +from enum import Enum + +from azure.core import CaseInsensitiveEnumMeta + + +class CertificatePolicyAction(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """The supported action types for the lifetime of a certificate""" + + email_contacts = "EmailContacts" + auto_renew = "AutoRenew" + + +class CertificateContentType(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Content type of the secrets as specified in Certificate Policy""" + + pkcs12 = "application/x-pkcs12" + pem = "application/x-pem-file" + + +class KeyUsageType(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """The supported types of key usages""" + + digital_signature = "digitalSignature" + non_repudiation = "nonRepudiation" + key_encipherment = "keyEncipherment" + data_encipherment = "dataEncipherment" + key_agreement = "keyAgreement" + key_cert_sign = "keyCertSign" + crl_sign = "cRLSign" + encipher_only = "encipherOnly" + decipher_only = "decipherOnly" + + +class KeyType(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Supported key types""" + + ec = "EC" #: Elliptic Curve + ec_hsm = "EC-HSM" #: Elliptic Curve with a private key which is not exportable from the HSM + rsa = "RSA" #: RSA (https://tools.ietf.org/html/rfc3447) + rsa_hsm = "RSA-HSM" #: RSA with a private key which is not exportable from the HSM + oct = "oct" #: Octet sequence (used to represent symmetric keys) + oct_hsm = "oct-HSM" #: Octet sequence with a private key which is not exportable from the HSM + + @classmethod + def _missing_(cls, value): + for member in cls: + if member.value.lower() == value.lower(): + return member + raise ValueError(f"{value} is not a valid KeyType") + + +class KeyCurveName(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Supported elliptic curves""" + + p_256 = "P-256" #: The NIST P-256 elliptic curve, AKA SECG curve SECP256R1. + p_384 = "P-384" #: The NIST P-384 elliptic curve, AKA SECG curve SECP384R1. + p_521 = "P-521" #: The NIST P-521 elliptic curve, AKA SECG curve SECP521R1. + p_256_k = "P-256K" #: The SECG SECP256K1 elliptic curve. + + +class WellKnownIssuerNames(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Collection of well-known issuer names""" + + self = "Self" #: Use this issuer for a self-signed certificate + unknown = "Unknown" + """ + If you use this issuer, you must manually get an x509 certificate from the issuer of your choice. + You must then call :func:`~azure.keyvault.certificates.CertificateClient.merge_certificate` to + merge the public x509 certificate with your key vault certificate pending object to complete creation. + """ diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py new file mode 100644 index 000000000000..6c3d3eba60f9 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py @@ -0,0 +1,1560 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +# pylint: disable=too-many-lines,too-many-public-methods +from datetime import datetime +from typing import Any, Dict, Optional, Union, List + +from ._generated import models +from ._shared import parse_key_vault_id +from ._enums import ( + CertificatePolicyAction, + KeyUsageType, + KeyCurveName, + KeyType, + CertificateContentType, + WellKnownIssuerNames, +) + + +class AdministratorContact(object): + """Details of the organization administrator of the certificate issuer. + + :param first_name: First name of the issuer. + :type first_name: str or None + :param last_name: Last name of the issuer. + :type last_name: str or None + :param email: email of the issuer. + :type email: str or None + :param phone: phone number of the issuer. + :type phone: str or None + """ + + def __init__( + self, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + email: Optional[str] = None, + phone: Optional[str] = None, + ) -> None: + self._first_name = first_name + self._last_name = last_name + self._phone = phone + self._email = email + + def __repr__(self) -> str: + result = ( + f"AdministratorContact(first_name={self.first_name}, last_name={self.last_name}, " + + f"email={self.email}, phone={self.phone})" + ) + return result[:1024] + + @classmethod + def _from_admin_detail(cls, admin_detail: models.AdministratorDetails) -> "AdministratorContact": + return cls( + email=admin_detail.email_address, + first_name=admin_detail.first_name, + last_name=admin_detail.last_name, + phone=admin_detail.phone, + ) + + @property + def email(self) -> Optional[str]: + """Email address of the issuer. + + :rtype: str or None""" + return self._email + + @property + def first_name(self) -> Optional[str]: + """First name of the issuer. + + :rtype: str or None""" + return self._first_name + + @property + def last_name(self) -> Optional[str]: + """Last name of the issuer. + + :rtype: str or None""" + return self._last_name + + @property + def phone(self) -> Optional[str]: + """Phone number of the issuer. + + :rtype: str or None""" + return self._phone + + +class CertificateOperationError(object): + """The key vault server error. + + :param str code: The error code. + :param str message: The error message. + :param inner_error: The error object itself + :type inner_error: ~azure.keyvault.certificates.CertificateOperationError or None + """ + + def __init__(self, code: str, message: str, inner_error: "Optional[CertificateOperationError]" = None) -> None: + self._code = code + self._message = message + self._inner_error = inner_error + + def __repr__(self) -> str: + return f"CertificateOperationError({self.code}, {self.message}, {self.inner_error})"[:1024] + + @classmethod + def _from_error_bundle(cls, error_bundle: models.KeyVaultErrorError) -> "CertificateOperationError": + return cls( + code=error_bundle.code, # type: ignore + message=error_bundle.message, # type: ignore + inner_error=cls._from_error_bundle(error_bundle.inner_error) if error_bundle.inner_error else None, + ) + + @property + def code(self) -> str: + """The error code. + + :returns: The error code. + :rtype: str + """ + return self._code + + @property + def message(self) -> str: + """The error message. + + :returns: The error message. + :rtype: str + """ + return self._message + + @property + def inner_error(self) -> "Optional[CertificateOperationError]": + """The error itself. + + :returns: The error itself. + :rtype: ~azure.keyvault.certificates.CertificateOperationError or None + """ + return self._inner_error + + +class CertificateProperties(object): + """Certificate properties consists of a certificates metadata.""" + + def __init__(self, **kwargs: Any) -> None: + self._attributes = kwargs.pop("attributes", None) + self._id = kwargs.pop("cert_id", None) + self._vault_id = KeyVaultCertificateIdentifier(self._id) + self._x509_thumbprint = kwargs.pop("x509_thumbprint", None) + self._tags = kwargs.pop("tags", None) + self._preserve_order = kwargs.pop("preserve_order", False) + + def __repr__(self) -> str: + return f""[:1024] + + @classmethod + def _from_certificate_bundle( + cls, + certificate_bundle: Union[ + models.CertificateBundle, + models.DeletedCertificateBundle, + ], + ) -> "CertificateProperties": + return cls( + attributes=certificate_bundle.attributes, + cert_id=certificate_bundle.id, + x509_thumbprint=certificate_bundle.x509_thumbprint, + tags=certificate_bundle.tags, + preserve_order=certificate_bundle.preserve_cert_order, + ) + + @classmethod + def _from_certificate_item( + cls, + certificate_item: Union[ + models.CertificateItem, + models.DeletedCertificateItem, + ], + ) -> "CertificateProperties": + return cls( + attributes=certificate_item.attributes, + cert_id=certificate_item.id, + x509_thumbprint=certificate_item.x509_thumbprint, + tags=certificate_item.tags, + ) + + @property + def id(self) -> str: + """The certificate identifier. + + :returns: The certificate identifier. + :rtype: str + """ + return self._id + + @property + def name(self) -> str: + """The name of the certificate. + + :returns: The name of the certificate. + :rtype: str + """ + return self._vault_id.name + + @property + def enabled(self) -> Optional[bool]: + """Whether the certificate is enabled or not. + + :returns: True if the certificate is enabled; False otherwise. + :rtype: bool or None + """ + return self._attributes.enabled if self._attributes else None + + @property + def not_before(self) -> Optional[datetime]: + """The datetime before which the certificate is not valid. + + :returns: A datetime representing the point in time when the certificate becomes valid. + :rtype: ~datetime.datetime or None + """ + return self._attributes.not_before if self._attributes else None + + @property + def expires_on(self) -> Optional[datetime]: + """The datetime when the certificate expires. + + :returns: A datetime representing the point in time when the certificate expires. + :rtype: ~datetime.datetime or None + """ + return self._attributes.expires if self._attributes else None + + @property + def created_on(self) -> Optional[datetime]: + """The datetime when the certificate is created. + + :returns: A datetime representing the certificate's creation time. + :rtype: ~datetime.datetime or None + """ + return self._attributes.created if self._attributes else None + + @property + def updated_on(self) -> Optional[datetime]: + """The datetime when the certificate was last updated. + + :returns: A datetime representing the time of the certificate's most recent update. + :rtype: ~datetime.datetime or None + """ + return self._attributes.updated if self._attributes else None + + @property + def recoverable_days(self) -> Optional[int]: + """The number of days the certificate is retained before being deleted from a soft-delete enabled Key Vault. + + :returns: The number of days remaining where the certificate can be restored. + :rtype: int or None + """ + # recoverable_days was added in 7.1-preview + if self._attributes and hasattr(self._attributes, "recoverable_days"): + return self._attributes.recoverable_days + return None + + @property + def recovery_level(self) -> Optional[models.DeletionRecoveryLevel]: + """The deletion recovery level currently in effect for the certificate. + + :returns: The deletion recovery level currently in effect for the certificate. + :rtype: models.DeletionRecoveryLevel or None + """ + return self._attributes.recovery_level if self._attributes else None + + @property + def vault_url(self) -> str: + """The URL of the vault containing the certificate. + + :returns: The URL of the vault containing the certificate. + :rtype: str + """ + return self._vault_id.vault_url + + @property + def x509_thumbprint(self) -> bytes: + """The certificate's thumbprint, in bytes. + + To get the thumbprint as a hexadecimal string, call ``.hex()`` on this property. + + :return: The certificate's thumbprint, in bytes. + :rtype: bytes + """ + return self._x509_thumbprint + + @property + def tags(self) -> Optional[Dict[str, str]]: + """Application specific metadata in the form of key-value pairs. + + :returns: A dictionary of tags attached to the certificate. + :rtype: dict[str, str] or None + """ + return self._tags + + @property + def version(self) -> Optional[str]: + """The version of the certificate. + + :returns: The version of the certificate. + :rtype: str or None + """ + return self._vault_id.version + + @property + def preserve_order(self) -> Optional[bool]: + """Whether the certificate order should be preserved. + + :returns: Specifies whether the certificate chain preserves its original order. The default value is False, + which sets the leaf certificate at index 0. + :rtype: bool or None + """ + return self._preserve_order + + +class KeyVaultCertificate(object): + """Consists of a certificate and its attributes + + :param policy: The management policy for the certificate. + :type policy: ~azure.keyvault.certificates.CertificatePolicy or None + :param properties: The certificate's properties. + :type properties: ~azure.keyvault.certificates.CertificateProperties or None + :param cer: CER contents of the X509 certificate. + :type cer: bytearray or None + """ + + def __init__( + self, + policy: "Optional[CertificatePolicy]" = None, + properties: Optional[CertificateProperties] = None, + cer: Optional[bytearray] = None, + **kwargs: Any, + ) -> None: + self._properties = properties + self._key_id = kwargs.get("key_id", None) + self._secret_id = kwargs.get("secret_id", None) + self._policy = policy + self._cer = cer + + def __repr__(self) -> str: + return f""[:1024] + + @classmethod + def _from_certificate_bundle(cls, certificate_bundle: models.CertificateBundle) -> "KeyVaultCertificate": + # pylint:disable=protected-access + + if certificate_bundle.policy: + policy: Optional[CertificatePolicy] = CertificatePolicy._from_certificate_policy_bundle( + certificate_bundle.policy + ) + else: + policy = None + + return cls( + properties=CertificateProperties._from_certificate_bundle(certificate_bundle), + key_id=certificate_bundle.kid, + secret_id=certificate_bundle.sid, + policy=policy, + cer=certificate_bundle.cer, # type: ignore + ) + + @property + def id(self) -> Optional[str]: + """The certificate identifier. + + :returns: The certificate identifier. + :rtype: str or None + """ + return self._properties.id if self._properties else None + + @property + def name(self) -> Optional[str]: + """The name of the certificate. + + :returns: The name of the certificate. + :rtype: str or None + """ + return self._properties.name if self._properties else None + + @property + def properties(self) -> Optional[CertificateProperties]: + """The certificate's properties. + + :returns: The certificate's properties. + :rtype: ~azure.keyvault.certificates.CertificateProperties or None + """ + return self._properties + + @property + def key_id(self) -> Optional[str]: + """The ID of the key associated with the certificate. + + :returns: The ID of the key associated with the certificate. + :rtype: str or None + """ + return self._key_id + + @property + def secret_id(self) -> Optional[str]: + """The ID of the secret associated with the certificate. + + :returns: The ID of the secret associated with the certificate. + :rtype: str or None + """ + return self._secret_id + + @property + def policy(self) -> "Optional[CertificatePolicy]": + """The management policy of the certificate. + + :returns: The management policy of the certificate. + :rtype: ~azure.keyvault.certificates.CertificatePolicy or None + """ + return self._policy + + @property + def cer(self) -> Optional[bytearray]: + """The CER contents of the certificate. + + :returns: The CER contents of the certificate. + :rtype: bytearray or None + """ + return self._cer + + +class KeyVaultCertificateIdentifier(object): + """Information about a KeyVaultCertificate parsed from a certificate ID. + + :param str source_id: the full original identifier of a certificate + + :raises ValueError: if the certificate ID is improperly formatted + + Example: + .. literalinclude:: ../tests/test_parse_id.py + :start-after: [START parse_key_vault_certificate_id] + :end-before: [END parse_key_vault_certificate_id] + :language: python + :caption: Parse a certificate's ID + :dedent: 8 + """ + + def __init__(self, source_id: str) -> None: + self._resource_id = parse_key_vault_id(source_id) + + @property + def source_id(self) -> str: + return self._resource_id.source_id + + @property + def vault_url(self) -> str: + return self._resource_id.vault_url + + @property + def name(self) -> str: + return self._resource_id.name + + @property + def version(self) -> Optional[str]: + return self._resource_id.version + + +class CertificateOperation(object): + # pylint:disable=too-many-instance-attributes + """A certificate operation is returned in case of long running requests. + + :param cert_operation_id: The certificate id. + :type cert_operation_id: str or None + :param issuer_name: Name of the operation's issuer object or reserved names. + :type issuer_name: str or ~azure.keyvault.certificates.WellKnownIssuerNames or None + :param certificate_type: Type of certificate requested from the issuer provider. + :type certificate_type: str or None + :param certificate_transparency: Indicates if the certificate this operation is running for is published to + certificate transparency logs. Defaults to False. + :type certificate_transparency: bool or None + :param csr: The certificate signing request (CSR) that is being used in the certificate operation. + :type csr: bytes or None + :param cancellation_requested: Indicates if cancellation was requested on the certificate operation. Defaults + to False. + :type cancellation_requested: bool or None + :param status: Status of the certificate operation. + :type status: str or None + :param status_details: The status details of the certificate operation + :type status_details: str or None + :param error: Error encountered, if any, during the certificate operation. + :type error: ~azure.keyvault.certificates.CertificateOperationError or None + :param target: Location which contains the result of the certificate operation. + :type target: str or None + :param request_id: Identifier for the certificate operation. + :type request_id: str or None + :param bool preserve_order: Specifies whether the certificate chain preserves its original order. The default + value is False, which sets the leaf certificate at index 0. + """ + + def __init__( + self, + cert_operation_id: Optional[str] = None, + issuer_name: Optional[Union[str, WellKnownIssuerNames]] = None, + certificate_type: Optional[str] = None, + certificate_transparency: Optional[bool] = False, + csr: Optional[bytes] = None, + cancellation_requested: Optional[bool] = False, + status: Optional[str] = None, + status_details: Optional[str] = None, + error: Optional[CertificateOperationError] = None, + target: Optional[str] = None, + request_id: Optional[str] = None, + preserve_order: Optional[bool] = False, + ) -> None: + self._id = cert_operation_id + self._vault_id = parse_key_vault_id(cert_operation_id) if cert_operation_id else None + self._issuer_name = issuer_name + self._certificate_type = certificate_type + self._certificate_transparency = certificate_transparency + self._csr = csr + self._cancellation_requested = cancellation_requested + self._status = status + self._status_details = status_details + self._error = error + self._target = target + self._request_id = request_id + self._preserve_order = preserve_order + + def __repr__(self) -> str: + return f""[:1024] + + @classmethod + def _from_certificate_operation_bundle( + cls, certificate_operation_bundle: models.CertificateOperation + ) -> "CertificateOperation": + + issuer_parameters = certificate_operation_bundle.issuer_parameters + return cls( + cert_operation_id=certificate_operation_bundle.id, + issuer_name=issuer_parameters.name if issuer_parameters else None, + certificate_type=( + certificate_operation_bundle.issuer_parameters.certificate_type + if certificate_operation_bundle.issuer_parameters + else None + ), + # 2016-10-01 IssuerParameters doesn't have certificate_transparency + certificate_transparency=getattr(issuer_parameters, "certificate_transparency", None), + csr=certificate_operation_bundle.csr, + cancellation_requested=certificate_operation_bundle.cancellation_requested, + status=certificate_operation_bundle.status, + status_details=certificate_operation_bundle.status_details, + error=( + CertificateOperationError._from_error_bundle( # pylint: disable=protected-access + certificate_operation_bundle.error + ) + if certificate_operation_bundle.error + else None + ), + target=certificate_operation_bundle.target, + request_id=certificate_operation_bundle.request_id, + preserve_order=certificate_operation_bundle.preserve_cert_order, + ) + + @property + def id(self) -> Optional[str]: + """The certificate ID. + + :returns: The certificate ID. + :rtype: str or None + """ + return self._id + + @property + def name(self) -> Optional[str]: + """The certificate name. + + :returns: The certificate name. + :rtype: str or None + """ + return self._vault_id.name if self._vault_id else None + + @property + def vault_url(self) -> Optional[str]: + """URL of the vault performing the certificate operation. + + :returns: URL of the vault performing the certificate operation. + :rtype: str or None + """ + return self._vault_id.vault_url if self._vault_id else None + + @property + def issuer_name(self) -> Union[str, WellKnownIssuerNames, None]: + """The name of the certificate issuer. + + :returns: The name of the certificate issuer. + :rtype: str or ~azure.keyvault.certificates.WellKnownIssuerNames or None + """ + return self._issuer_name + + @property + def certificate_type(self) -> Optional[str]: + """Type of certificate to be requested from the issuer provider. + + :returns: Type of certificate to be requested from the issuer provider. + :rtype: str or None + """ + return self._certificate_type + + @property + def certificate_transparency(self) -> Optional[bool]: + """Whether certificates generated under this policy should be published to certificate transparency logs. + + :returns: True if the certificates should be published to transparency logs; False otherwise. + :rtype: bool or None + """ + return self._certificate_transparency + + @property + def csr(self) -> Optional[bytes]: + """The certificate signing request that is being used in this certificate operation. + + :returns: The certificate signing request that is being used in this certificate operation. + :rtype: bytes or None + """ + return self._csr + + @property + def cancellation_requested(self) -> Optional[bool]: + """Whether cancellation was requested on the certificate operation. + + :returns: True if cancellation was requested; False otherwise. + :rtype: bool or None + """ + return self._cancellation_requested + + @property + def status(self) -> Optional[str]: + """The operation status. + + :returns: The operation status. + :rtype: str or None + """ + return self._status + + @property + def status_details(self) -> Optional[str]: + """Details of the operation status. + + :returns: Details of the operation status. + :rtype: str or None + """ + return self._status_details + + @property + def error(self) -> Optional[CertificateOperationError]: + """Any error associated with the certificate operation. + + :returns: Any error associated with the operation, as a + :class:`~azure.keyvault.certificates.CertificateOperationError`. + :rtype: ~azure.keyvault.certificates.CertificateOperationError or None""" + return self._error + + @property + def target(self) -> Optional[str]: + """Location which contains the result of the certificate operation. + + :returns: Location which contains the result of the certificate operation. + :rtype: str or None + """ + return self._target + + @property + def request_id(self) -> Optional[str]: + """Identifier for the certificate operation. + + :returns: Identifier for the certificate operation. + :rtype: str or None + """ + return self._request_id + + @property + def preserve_order(self) -> Optional[bool]: + """Whether the certificate order should be preserved. + + :returns: Specifies whether the certificate chain preserves its original order. The default value is False, + which sets the leaf certificate at index 0. + :rtype: bool or None + """ + return self._preserve_order + + +class CertificatePolicy(object): + """Management policy for a certificate. + + :param issuer_name: Optional. Name of the referenced issuer object or reserved names; for example, + :attr:`~azure.keyvault.certificates.WellKnownIssuerNames.self` or + :attr:`~azure.keyvault.certificates.WellKnownIssuerNames.unknown` + :type issuer_name: str or None + + :keyword subject: The subject name of the certificate. Should be a valid X509 distinguished name. Either subject or + one of the subject alternative name parameters are required for creating a certificate. This will be ignored + when importing a certificate; the subject will be parsed from the imported certificate. + :paramtype subject: str or None + :keyword san_emails: Subject alternative emails of the X509 object. Either subject or one of the subject alternative + name parameters are required for creating a certificate. + :paramtype san_emails: list[str] or None + :keyword san_dns_names: Subject alternative DNS names of the X509 object. Either subject or one of the subject + alternative name parameters are required for creating a certificate. + :paramtype san_dns_names: list[str] or None + :keyword san_user_principal_names: Subject alternative user principal names of the X509 object. Either subject or + one of the subject alternative name parameters are required for creating a certificate. + :paramtype san_user_principal_names: list[str] or None + :keyword san_ip_addresses: Subject alternative IP addresses of the X509 object. Supports IPv4 and IPv6. Either + subject or one of the subject alternative name parameters are required for creating a certificate. + :paramtype san_ip_addresses: list[str] or None + :keyword san_uris: Subject alternative URIs of the X509 object. Either subject or one of the subject alternative + name parameters are required for creating a certificate. + :paramtype san_uris: list[str] or None + :keyword exportable: Indicates if the private key can be exported. For valid values, see KeyType. + :paramtype exportable: bool or None + :keyword key_type: The type of key pair to be used for the certificate. + :paramtype key_type: str or ~azure.keyvault.certificates.KeyType or None + :keyword key_size: The key size in bits. For example: 2048, 3072, or 4096 for RSA. + :paramtype key_size: int or None + :keyword reuse_key: Indicates if the same key pair will be used on certificate renewal. + :paramtype reuse_key: bool or None + :keyword key_curve_name: Elliptic curve name. For valid values, see KeyCurveName. + :paramtype key_curve_name: str or ~azure.keyvault.certificates.KeyCurveName or None + :keyword enhanced_key_usage: The extended ways the key of the certificate can be used. + :paramtype enhanced_key_usage: list[str] or None + :keyword key_usage: List of key usages. + :paramtype key_usage: list[str or ~azure.keyvault.certificates.KeyUsageType] or None + :keyword content_type: The media type (MIME type) of the secret backing the certificate. If not specified, + :attr:`CertificateContentType.pkcs12` is assumed. + :paramtype content_type: str or ~azure.keyvault.certificates.CertificateContentType or None + :keyword validity_in_months: The duration that the certificate is valid in months. + :paramtype validity_in_months: int or None + :keyword lifetime_actions: Actions that will be performed by Key Vault over the lifetime of a certificate. + :paramtype lifetime_actions: list[~azure.keyvault.certificates.LifetimeAction] or None + :keyword certificate_type: Type of certificate to be requested from the issuer provider. + :paramtype certificate_type: str or None + :keyword certificate_transparency: Indicates if the certificates generated under this policy should be + published to certificate transparency logs. + :paramtype certificate_transparency: bool or None + """ + + # pylint:disable=too-many-instance-attributes + def __init__( + self, + issuer_name: Optional[str] = None, + **kwargs: Any, + ) -> None: + self._issuer_name = issuer_name + self._subject = kwargs.pop("subject", None) + self._attributes = kwargs.pop("attributes", None) + self._exportable = kwargs.pop("exportable", None) + self._key_type = kwargs.pop("key_type", None) + self._key_size = kwargs.pop("key_size", None) + self._reuse_key = kwargs.pop("reuse_key", None) + self._key_curve_name = kwargs.pop("key_curve_name", None) + self._enhanced_key_usage = kwargs.pop("enhanced_key_usage", None) + self._key_usage = kwargs.pop("key_usage", None) + self._content_type = kwargs.pop("content_type", None) + self._validity_in_months = kwargs.pop("validity_in_months", None) + self._lifetime_actions = kwargs.pop("lifetime_actions", None) + self._certificate_type = kwargs.pop("certificate_type", None) + self._certificate_transparency = kwargs.pop("certificate_transparency", None) + self._san_emails = kwargs.pop("san_emails", None) or None + self._san_dns_names = kwargs.pop("san_dns_names", None) or None + self._san_user_principal_names = kwargs.pop("san_user_principal_names", None) or None + self._san_ip_addresses = kwargs.pop("san_ip_addresses", None) or None + self._san_uris = kwargs.pop("san_uris", None) or None + + @classmethod + def get_default(cls) -> "CertificatePolicy": + return cls(issuer_name=WellKnownIssuerNames.self, subject="CN=DefaultPolicy") + + def __repr__(self) -> str: + return f""[:1024] + + def _to_certificate_policy_bundle(self) -> models.CertificatePolicy: + if self.issuer_name or self.certificate_type or self.certificate_transparency: + issuer_parameters: Optional[models.IssuerParameters] = models.IssuerParameters( + name=self.issuer_name, + certificate_type=self.certificate_type, + certificate_transparency=self.certificate_transparency, # 2016-10-01 model will ignore this + ) + else: + issuer_parameters = None + + if self.enabled is not None or self.created_on is not None or self.updated_on is not None: + attributes = models.CertificateAttributes( + { + "enabled": self.enabled, + "created": self.created_on, + "updated": self.updated_on, + } + ) + else: + attributes = None + + if self.lifetime_actions: + lifetime_actions = [] + for lifetime_action in self.lifetime_actions: + lifetime_actions.append( + models.LifetimeAction( + trigger=models.Trigger( + lifetime_percentage=lifetime_action.lifetime_percentage, + days_before_expiry=lifetime_action.days_before_expiry, + ), + action=models.Action(action_type=lifetime_action.action), + ) + ) + else: + lifetime_actions = None # type: ignore + + # pylint:disable=too-many-boolean-expressions + if ( + self.subject + or self.enhanced_key_usage + or self.key_usage + or self.san_emails + or self.san_user_principal_names + or self.san_dns_names + or self.san_ip_addresses + or self.san_uris + or self.validity_in_months + ): + if self.key_usage: + key_usage: Optional[List[Union[str, KeyUsageType]]] = [ + k.value if not isinstance(k, str) else k for k in self.key_usage + ] + else: + key_usage = None + + x509_properties: Optional[models.X509CertificateProperties] = models.X509CertificateProperties( + subject=self.subject, + ekus=self.enhanced_key_usage, + subject_alternative_names=models.SubjectAlternativeNames( + emails=self.san_emails, + upns=self.san_user_principal_names, + dns_names=self.san_dns_names, + ip_addresses=self.san_ip_addresses, + uris=self.san_uris, + ), + key_usage=key_usage, + validity_in_months=self.validity_in_months, + ) + else: + x509_properties = None + + if self.exportable or self.key_type or self.key_size or self.reuse_key or self.key_curve_name: + key_properties: Optional[models.KeyProperties] = models.KeyProperties( + exportable=self.exportable, + key_type=self.key_type, + key_size=self.key_size, + reuse_key=self.reuse_key, + curve=self.key_curve_name, + ) + else: + key_properties = None + + if self.content_type: + secret_properties: Optional[models.SecretProperties] = models.SecretProperties( + content_type=self.content_type + ) + else: + secret_properties = None + + policy_bundle = models.CertificatePolicy( + key_properties=key_properties, + secret_properties=secret_properties, + x509_certificate_properties=x509_properties, + lifetime_actions=lifetime_actions, + issuer_parameters=issuer_parameters, + attributes=attributes, + ) + return policy_bundle + + @classmethod + def _from_certificate_policy_bundle( + cls, certificate_policy_bundle: Optional[models.CertificatePolicy] + ) -> "CertificatePolicy": + if certificate_policy_bundle is None: + return cls() + + if certificate_policy_bundle.lifetime_actions: + lifetime_actions: Optional[List[LifetimeAction]] = [ + LifetimeAction( + action=CertificatePolicyAction(item.action.action_type) if item.action else None, + lifetime_percentage=item.trigger.lifetime_percentage if item.trigger else None, + days_before_expiry=item.trigger.days_before_expiry if item.trigger else None, + ) + for item in certificate_policy_bundle.lifetime_actions + ] + else: + lifetime_actions = None + x509_certificate_properties = certificate_policy_bundle.x509_certificate_properties + if x509_certificate_properties and x509_certificate_properties.key_usage: + key_usage: Optional[List[KeyUsageType]] = [KeyUsageType(k) for k in x509_certificate_properties.key_usage] + else: + key_usage = None + key_properties = certificate_policy_bundle.key_properties + curve_name = getattr(key_properties, "curve", None) # missing from 2016-10-01 KeyProperties + if curve_name: + curve_name = KeyCurveName(curve_name) + + issuer_parameters = certificate_policy_bundle.issuer_parameters + return cls( + issuer_name=issuer_parameters.name if issuer_parameters else None, + subject=(x509_certificate_properties.subject if x509_certificate_properties else None), + certificate_type=issuer_parameters.certificate_type if issuer_parameters else None, + # 2016-10-01 IssuerParameters doesn't have certificate_transparency + certificate_transparency=getattr(issuer_parameters, "certificate_transparency", None), + lifetime_actions=lifetime_actions, + exportable=key_properties.exportable if key_properties else None, + key_type=KeyType(key_properties.key_type) if key_properties and key_properties.key_type else None, + key_size=key_properties.key_size if key_properties else None, + reuse_key=key_properties.reuse_key if key_properties else None, + key_curve_name=curve_name, + enhanced_key_usage=x509_certificate_properties.ekus if x509_certificate_properties else None, + key_usage=key_usage, + content_type=( + CertificateContentType(certificate_policy_bundle.secret_properties.content_type) + if certificate_policy_bundle.secret_properties + and certificate_policy_bundle.secret_properties.content_type + else None + ), + attributes=certificate_policy_bundle.attributes, + san_emails=( + x509_certificate_properties.subject_alternative_names.emails + if x509_certificate_properties and x509_certificate_properties.subject_alternative_names + else None + ), + san_user_principal_names=( + x509_certificate_properties.subject_alternative_names.upns + if x509_certificate_properties and x509_certificate_properties.subject_alternative_names + else None + ), + san_dns_names=( + x509_certificate_properties.subject_alternative_names.dns_names + if x509_certificate_properties and x509_certificate_properties.subject_alternative_names + else None + ), + san_ip_addresses=( + x509_certificate_properties.subject_alternative_names.ip_addresses + if x509_certificate_properties and x509_certificate_properties.subject_alternative_names + else None + ), + san_uris=( + x509_certificate_properties.subject_alternative_names.uris + if x509_certificate_properties and x509_certificate_properties.subject_alternative_names + else None + ), + validity_in_months=( + x509_certificate_properties.validity_in_months if x509_certificate_properties else None + ), + ) + + @property + def exportable(self) -> Optional[bool]: + """Whether the private key can be exported. + + :returns: True if the private key can be exported; False otherwise. + :rtype: bool or None + """ + return self._exportable + + @property + def key_type(self) -> Optional[KeyType]: + """The type of key pair to be used for the certificate. + + :returns: The type of key pair to be used for the certificate. + :rtype: ~azure.keyvault.certificates.KeyType or None + """ + return self._key_type + + @property + def key_size(self) -> Optional[int]: + """The key size in bits. + + :returns: The key size in bits. + :rtype: int or None + """ + return self._key_size + + @property + def reuse_key(self) -> Optional[bool]: + """Whether the same key pair will be used on certificate renewal. + + :returns: True if the same key pair will be used on certificate renewal; False otherwise. + :rtype: bool or None + """ + return self._reuse_key + + @property + def key_curve_name(self) -> Optional[KeyCurveName]: + """Elliptic curve name. + + :returns: Elliptic curve name. + :rtype: ~azure.keyvault.certificates.KeyCurveName or None + """ + return self._key_curve_name + + @property + def enhanced_key_usage(self) -> Optional[List[str]]: + """The enhanced key usage. + + :returns: The enhanced key usage. + :rtype: list[str] or None + """ + return self._enhanced_key_usage + + @property + def key_usage(self) -> Optional[List[KeyUsageType]]: + """List of key usages. + + :returns: List of key usages. + :rtype: list[~azure.keyvault.certificates.KeyUsageType] or None + """ + return self._key_usage + + @property + def content_type(self) -> Optional[CertificateContentType]: + """The media type (MIME type). + + :returns: The media type (MIME type). + :rtype: ~azure.keyvault.certificates.CertificateContentType or None + """ + return self._content_type + + @property + def subject(self) -> Optional[str]: + """The subject name of the certificate. + + :returns: The subject name of the certificate. + :rtype: str or None + """ + return self._subject + + @property + def san_emails(self) -> Optional[List[str]]: + """The subject alternative email addresses. + + :returns: The subject alternative email addresses, as a list. + :rtype: list[str] or None + """ + return self._san_emails + + @property + def san_dns_names(self) -> Optional[List[str]]: + """The subject alternative domain names. + + :returns: The subject alternative domain names, as a list. + :rtype: list[str] or None + """ + return self._san_dns_names + + @property + def san_user_principal_names(self) -> Optional[List[str]]: + """The subject alternative user principal names. + + :returns: The subject alternative user principal names, as a list. + :rtype: list[str] or None + """ + return self._san_user_principal_names + + @property + def san_ip_addresses(self) -> Optional[List[str]]: + """The subject alternative IP addresses. Supports IPv4 and IPv6. + + :returns: The subject alternative IP addresses, as a list. + :rtype: list[str] or None + """ + return self._san_ip_addresses + + @property + def san_uris(self) -> Optional[List[str]]: + """The subject alternative URIs. + + :returns: The subject alternative URIs, as a list. + :rtype: list[str] or None + """ + return self._san_uris + + @property + def validity_in_months(self) -> Optional[int]: + """The duration that the certificate is valid for in months. + + :returns: The duration that the certificate is valid for in months. + :rtype: int or None + """ + return self._validity_in_months + + @property + def lifetime_actions(self) -> "Optional[List[LifetimeAction]]": + """Actions and their triggers that will be performed by Key Vault over the lifetime of the certificate. + + :returns: Actions and their triggers that will be performed by Key Vault over the lifetime of the certificate. + :rtype: list[~azure.keyvault.certificates.LifetimeAction] or None + """ + return self._lifetime_actions + + @property + def issuer_name(self) -> Optional[str]: + """Name of the referenced issuer object or reserved names for the issuer of the certificate. + + :returns: Name of the referenced issuer object or reserved names for the issuer of the certificate. + :rtype: str or None + """ + return self._issuer_name + + @property + def certificate_type(self) -> Optional[str]: + """Type of certificate requested from the issuer provider. + + :returns: Type of certificate requested from the issuer provider. + :rtype: str or None + """ + return self._certificate_type + + @property + def certificate_transparency(self) -> Optional[bool]: + """Whether the certificates generated under this policy should be published to certificate transparency logs. + + :returns: True if the certificates should be published to transparency logs; False otherwise. + :rtype: bool or None + """ + return self._certificate_transparency + + @property + def enabled(self) -> Optional[bool]: + """Whether the certificate is enabled or not. + + :returns: True if the certificate is enabled; False otherwise. + :rtype: bool or None + """ + return self._attributes.enabled if self._attributes else None + + @property + def created_on(self) -> Optional[datetime]: + """The datetime when the certificate is created. + + :returns: The datetime when the certificate is created. + :rtype: ~datetime.datetime or None + """ + return self._attributes.created if self._attributes else None + + @property + def updated_on(self) -> Optional[datetime]: + """The datetime when the certificate was last updated. + + :returns: The datetime when the certificate was last updated. + :rtype: ~datetime.datetime or None + """ + return self._attributes.updated if self._attributes else None + + +class CertificateContact(object): + """The contact information for the vault certificates. + + :param email: Email address of a contact for the certificate. + :type email: str or None + :param name: Name of a contact for the certificate. + :type name: str or None + :param phone: phone number of a contact for the certificate. + :type phone: str or None + """ + + def __init__(self, email: Optional[str] = None, name: Optional[str] = None, phone: Optional[str] = None) -> None: + self._email = email + self._name = name + self._phone = phone + + def __repr__(self) -> str: + return f"CertificateContact(email={self.email}, name={self.name}, phone={self.phone})"[:1024] + + def _to_certificate_contacts_item(self) -> models.Contact: + return models.Contact(email_address=self.email, name=self.name, phone=self.phone) + + @classmethod + def _from_certificate_contacts_item(cls, contact_item: models.Contact) -> "CertificateContact": + return cls(email=contact_item.email_address, name=contact_item.name, phone=contact_item.phone) + + @property + def email(self) -> Optional[str]: + """Email address of a contact for the certificate. + + :rtype: str or None""" + return self._email + + @property + def name(self) -> Optional[str]: + """Name of a contact for the certificate. + + :rtype: str or None""" + return self._name + + @property + def phone(self) -> Optional[str]: + """Phone number of a contact for the certificate. + + :rtype: str or None""" + return self._phone + + +class IssuerProperties(object): + """The properties of an issuer containing the issuer metadata. + + :param provider: The issuer provider. + :type provider: str or None + """ + + def __init__(self, provider: Optional[str] = None, **kwargs: Any) -> None: + self._id = kwargs.pop("issuer_id", None) + self._vault_id = parse_key_vault_id(self._id) + self._provider = provider + + def __repr__(self) -> str: + return f"IssuerProperties(issuer_id={self.id}, provider={self.provider})"[:1024] + + @classmethod + def _from_issuer_item( + cls, issuer_item: Union[models.CertificateIssuerItem, models.IssuerBundle] + ) -> "IssuerProperties": + return cls(issuer_id=issuer_item.id, provider=issuer_item.provider) + + @property + def id(self) -> Optional[str]: + """The issuer ID. + + :returns: The issuer ID. + :rtype: str or None + """ + return self._id + + @property + def name(self) -> Optional[str]: + """The issuer name. + + :returns: The issuer name. + :rtype: str or None + """ + # Issuer name is listed under version under vault_id + return self._vault_id.version + + @property + def provider(self) -> Optional[str]: + """The issuer provider. + + :returns: The issuer provider. + :rtype: str or None + """ + return self._provider + + +class CertificateIssuer(object): + """The issuer for a Key Vault certificate. + + :param provider: The issuer provider + :type provider: str or None + :param attributes: The issuer attributes. + :type attributes: ~azure.keyvault.certificates._generated.models.IssuerAttributes or None + :param account_id: The username / account name / account id. + :type account_id: str or None + :param password: The password / secret / account key. + :type password: str or None + :param organization_id: The ID of the organization. + :type organization_id: str or None + :param admin_contacts: Details of the organization administrator. + :type admin_contacts: list[~azure.keyvault.certificates.AdministratorContact] or None + """ + + def __init__( + self, + provider: Optional[str], + attributes: Optional[models.IssuerAttributes] = None, + account_id: Optional[str] = None, + # [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Typedef, not string.")] + password: Optional[str] = None, + organization_id: Optional[str] = None, + admin_contacts: Optional[List[AdministratorContact]] = None, + **kwargs: Any, + ) -> None: + self._provider = provider + self._attributes = attributes + self._account_id = account_id + self._password = password + self._organization_id = organization_id + self._admin_contacts = admin_contacts + self._id = kwargs.pop("issuer_id", None) + self._vault_id = parse_key_vault_id(self._id) + + def __repr__(self) -> str: + return f""[:1024] + + @classmethod + def _from_issuer_bundle(cls, issuer_bundle: models.IssuerBundle) -> "CertificateIssuer": + admin_contacts = [] + admin_details = issuer_bundle.organization_details.admin_details if issuer_bundle.organization_details else None + if admin_details: + # pylint:disable=protected-access + for admin_detail in admin_details: + admin_contacts.append(AdministratorContact._from_admin_detail(admin_detail)) + return cls( + provider=IssuerProperties._from_issuer_item(issuer_bundle).provider, # pylint: disable=protected-access + attributes=issuer_bundle.attributes, + account_id=issuer_bundle.credentials.account_id if issuer_bundle.credentials else None, + password=issuer_bundle.credentials.password if issuer_bundle.credentials else None, + organization_id=issuer_bundle.organization_details.id if issuer_bundle.organization_details else None, + admin_contacts=admin_contacts, + issuer_id=issuer_bundle.id, + ) + + @property + def id(self) -> Optional[str]: + """The issuer ID. + + :returns: The issuer ID. + :rtype: str or None + """ + return self._id + + @property + def name(self) -> Optional[str]: + """The issuer name. + + :returns: The issuer name. + :rtype: str or None + """ + # Issuer name is listed under version under vault_id. + # This is because the id we pass to parse_key_vault_id has an extra segment, so where most cases the version of + # the general pattern is certificates/name/version, but here we have certificates/issuers/name/version. + # Issuers are not versioned. + return self._vault_id.version + + @property + def provider(self) -> Optional[str]: + """The issuer provider. + + :returns: The issuer provider. + :rtype: str or None + """ + return self._provider + + @property + def enabled(self) -> Optional[bool]: + """Whether the certificate is enabled or not. + + :returns: True if the certificate is enabled; False otherwise. + :rtype: bool or None + """ + return self._attributes.enabled if self._attributes else None + + @property + def created_on(self) -> Optional[datetime]: + """The datetime when the certificate is created. + + :returns: The datetime when the certificate is created. + :rtype: ~datetime.datetime or None + """ + return self._attributes.created if self._attributes else None + + @property + def updated_on(self) -> Optional[datetime]: + """The datetime when the certificate was last updated. + + :returns: The datetime when the certificate was last updated. + :rtype: ~datetime.datetime or None + """ + return self._attributes.updated if self._attributes else None + + @property + def account_id(self) -> Optional[str]: + """The username / account name / account id. + + :returns: The username / account name / account id. + :rtype: str or None + """ + return self._account_id + + @property + def password(self) -> Optional[str]: + """The password / secret / account key. + + :returns: The password / secret / account key. + :rtype: str or None + """ + return self._password + + @property + def organization_id(self) -> Optional[str]: + """The issuer organization ID. + + :returns: The issuer organization ID. + :rtype: str or None + """ + return self._organization_id + + @property + def admin_contacts(self) -> Optional[List[AdministratorContact]]: + """Contact details of the organization administrator(s) of this issuer. + + :returns: Contact details of the organization administrator(s) of this issuer. + :rtype: list[~azure.keyvault.certificates.AdministratorContact] or None + """ + return self._admin_contacts + + +class LifetimeAction(object): + """Action and its trigger that will be performed by certificate Vault over the lifetime of a certificate. + + :param action: The type of the action. For valid values, see CertificatePolicyAction + :type action: str or ~azure.keyvault.certificates.CertificatePolicyAction or None + :param lifetime_percentage: Percentage of lifetime at which to trigger. Value should be between 1 and 99. + :type lifetime_percentage: int or None + :param days_before_expiry: Days before expiry to attempt renewal. Value should be between 1 and + `validity_in_months` multiplied by 27. I.e., if validity_in_months is 36, then value should be between 1 and 972 + (36 * 27). + :type days_before_expiry: int or None + """ + + def __init__( + self, + action: Union[str, CertificatePolicyAction, None], + lifetime_percentage: Optional[int] = None, + days_before_expiry: Optional[int] = None, + ) -> None: + self._lifetime_percentage = lifetime_percentage + self._days_before_expiry = days_before_expiry + self._action = action + + def __repr__(self) -> str: + result = ( + f"LifetimeAction(action={self.action}, lifetime_percentage={self.lifetime_percentage}, " + + f"days_before_expiry={self.days_before_expiry})" + ) + return result[:1024] + + @property + def lifetime_percentage(self) -> Optional[int]: + """Percentage of lifetime at which to trigger. + + :returns: Percentage of lifetime at which to trigger. + :rtype: int or None + """ + return self._lifetime_percentage + + @property + def days_before_expiry(self) -> Optional[int]: + """Days before expiry to attempt renewal. + + :returns: Days before expiry to attempt renewal. + :rtype: int or None + """ + return self._days_before_expiry + + @property + def action(self) -> Union[str, CertificatePolicyAction, None]: + """The type of action that will be executed; see :class:`~azure.keyvault.certificates.CertificatePolicyAction`. + + :returns: The type of action that will be executed; see + :class:`~azure.keyvault.certificates.CertificatePolicyAction`. + :rtype: str or ~azure.keyvault.certificates.CertificatePolicyAction or None + """ + return self._action + + +class DeletedCertificate(KeyVaultCertificate): + """A deleted Certificate consisting of its previous ID, attributes, tags, and information on when it will be purged. + + :param properties: Properties of the deleted certificate. + :type properties: ~azure.keyvault.certificates.CertificateProperties + :param policy: The management policy of the deleted certificate. + :type policy: ~azure.keyvault.certificates.CertificatePolicy or None + :param cer: CER contents of the X509 certificate. + :type cer: bytearray or None + + :keyword deleted_on: The time when the certificate was deleted, in UTC. + :paramtype deleted_on: ~datetime.datetime or None + :keyword recovery_id: The url of the recovery object, used to identify and recover the deleted certificate. + :paramtype recovery_id: str or None + :keyword scheduled_purge_date: The time when the certificate is scheduled to be purged, in UTC. + :paramtype scheduled_purge_date: ~datetime.datetime or None + """ + + def __init__( + self, + properties: Optional[CertificateProperties] = None, + policy: Optional[CertificatePolicy] = None, + cer: Optional[bytearray] = None, + **kwargs: Any, + ) -> None: + super(DeletedCertificate, self).__init__(properties=properties, policy=policy, cer=cer, **kwargs) + self._deleted_on = kwargs.get("deleted_on", None) + self._recovery_id = kwargs.get("recovery_id", None) + self._scheduled_purge_date = kwargs.get("scheduled_purge_date", None) + + def __repr__(self) -> str: + return f""[:1024] + + @classmethod + def _from_deleted_certificate_item( + cls, deleted_certificate_item: models.DeletedCertificateItem + ) -> "DeletedCertificate": + return cls( + properties=CertificateProperties._from_certificate_item( # pylint: disable=protected-access + deleted_certificate_item + ), + key_id=None, + secret_id=None, + policy=None, + cer=None, + deleted_on=deleted_certificate_item.deleted_date, + recovery_id=deleted_certificate_item.recovery_id, + scheduled_purge_date=deleted_certificate_item.scheduled_purge_date, + ) + + @classmethod + def _from_deleted_certificate_bundle( + cls, deleted_certificate_bundle: models.DeletedCertificateBundle + ) -> "DeletedCertificate": + # pylint:disable=protected-access + return cls( + properties=CertificateProperties._from_certificate_bundle(deleted_certificate_bundle), + key_id=deleted_certificate_bundle.kid, + secret_id=deleted_certificate_bundle.sid, + policy=CertificatePolicy._from_certificate_policy_bundle(deleted_certificate_bundle.policy), + cer=deleted_certificate_bundle.cer, # type: ignore + deleted_on=deleted_certificate_bundle.deleted_date, + recovery_id=deleted_certificate_bundle.recovery_id, + scheduled_purge_date=deleted_certificate_bundle.scheduled_purge_date, + ) + + @property + def deleted_on(self) -> Optional[datetime]: + """The datetime when the certificate was deleted. + + :returns: The datetime when the certificate was deleted. + :rtype: ~datetime.datetime or None + """ + return self._deleted_on + + @property + def recovery_id(self) -> Optional[str]: + """The URL of the recovery object, used to identify and recover the deleted certificate. + + :returns: The URL of the recovery object, used to identify and recover the deleted certificate. + :rtype: str or None + """ + return self._recovery_id + + @property + def scheduled_purge_date(self) -> Optional[datetime]: + """The datetime when the certificate is scheduled to be purged. + + :returns: The datetime when the certificate is scheduled to be purged. + :rtype: ~datetime.datetime or None + """ + return self._scheduled_purge_date diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_polling.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_polling.py new file mode 100644 index 000000000000..7d1f955f3313 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_polling.py @@ -0,0 +1,56 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import Any, Callable, cast, Optional, Union + +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import HttpTransport +from azure.core.polling import PollingMethod +from azure.keyvault.certificates._models import KeyVaultCertificate, CertificateOperation + + +class CreateCertificatePoller(PollingMethod): + def __init__( + self, pipeline_response: PipelineResponse, get_certificate_command: Callable, interval: int = 5 + ) -> None: + self._pipeline_response = pipeline_response + self._command: Optional[Callable] = None + self._resource: Optional[Union[CertificateOperation, KeyVaultCertificate]] = None + self._pending_certificate_op: Optional[CertificateOperation] = None + self._get_certificate_command = get_certificate_command + self._polling_interval = interval + + def _update_status(self) -> None: + self._pending_certificate_op = self._command() if self._command else None + + def initialize(self, client: Any, initial_response: Any, _: Any) -> None: + self._command = client + self._pending_certificate_op = initial_response + + def run(self) -> None: + while not self.finished(): + self._update_status() + if not self.finished(): + # We should always ask the client's transport to sleep, instead of sleeping directly + transport: HttpTransport = cast(HttpTransport, self._pipeline_response.context.transport) + transport.sleep(self._polling_interval) + operation = self._pending_certificate_op + if operation and operation.status and operation.status.lower() == "completed": + self._resource = self._get_certificate_command() + else: + self._resource = self._pending_certificate_op + + def finished(self) -> bool: + operation = self._pending_certificate_op + if operation and operation.issuer_name and operation.issuer_name.lower() == "unknown": + # Because we've finished, self._resource won't be set by the run method; set it here so we don't return None + self._resource = self._pending_certificate_op + return True + return self._pending_certificate_op.status.lower() != "inprogress" # type: ignore + + def resource(self) -> Union[KeyVaultCertificate, CertificateOperation]: + return self._resource # type: ignore + + def status(self) -> str: + return self._pending_certificate_op.status.lower() # type: ignore diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_sdk_moniker.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_sdk_moniker.py new file mode 100644 index 000000000000..f7ac28c79e91 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_sdk_moniker.py @@ -0,0 +1,7 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from ._version import VERSION + +SDK_MONIKER = f"keyvault-certificates/{VERSION}" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py new file mode 100644 index 000000000000..cb088e31ad23 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py @@ -0,0 +1,78 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import Optional +from urllib import parse + +from .challenge_auth_policy import ChallengeAuthPolicy +from .client_base import KeyVaultClientBase +from .http_challenge import HttpChallenge +from . import http_challenge_cache + +HttpChallengeCache = http_challenge_cache # to avoid aliasing pylint error (C4745) + + +__all__ = [ + "ChallengeAuthPolicy", + "HttpChallenge", + "HttpChallengeCache", + "KeyVaultClientBase", +] + + +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: str, + vault_url: str, + name: str, + version: "Optional[str]" = None, + ) -> None: + self.source_id = source_id + self.vault_url = vault_url + self.name = name + self.version = version + + +def parse_key_vault_id(source_id: str) -> KeyVaultResourceId: + try: + parsed_uri = parse.urlparse(source_id) + except Exception as exc: + raise ValueError(f"'{source_id}' is not a valid ID") from exc + if not (parsed_uri.scheme and parsed_uri.hostname): + raise ValueError(f"'{source_id}' is not a valid ID") + + path = list(filter(None, parsed_uri.path.split("/"))) + + if len(path) < 2 or len(path) > 3: + raise ValueError(f"'{source_id}' is not a valid ID") + + vault_url = f"{parsed_uri.scheme}://{parsed_uri.hostname}" + if parsed_uri.port: + vault_url += f":{parsed_uri.port}" + + return KeyVaultResourceId( + source_id=source_id, + vault_url=vault_url, + name=path[1], + version=path[2] if len(path) == 3 else None, + ) + + +try: + # pylint:disable=unused-import + from .async_challenge_auth_policy import AsyncChallengeAuthPolicy + from .async_client_base import AsyncKeyVaultClientBase + + __all__.extend(["AsyncChallengeAuthPolicy", "AsyncKeyVaultClientBase"]) +except (SyntaxError, ImportError): + pass diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling.py new file mode 100644 index 000000000000..ff0c398bba6d --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling.py @@ -0,0 +1,136 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import threading +import uuid +from typing import Any, Callable, cast, Optional + +from azure.core.exceptions import ResourceNotFoundError, HttpResponseError +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import HttpTransport +from azure.core.polling import PollingMethod, LROPoller, NoPolling + +from azure.core.tracing.decorator import distributed_trace +from azure.core.tracing.common import with_current_context + + +class KeyVaultOperationPoller(LROPoller): + """Poller for long running operations where calling result() doesn't wait for operation to complete. + + :param polling_method: The poller's polling method. + :type polling_method: ~azure.core.polling.PollingMethod + """ + + def __init__(self, polling_method: PollingMethod) -> None: + super(KeyVaultOperationPoller, self).__init__(None, None, lambda *_: None, NoPolling()) + self._polling_method = polling_method + + # pylint: disable=arguments-differ + def result(self) -> "Any": # type: ignore + """Returns a representation of the final resource without waiting for the operation to complete. + + :returns: The deserialized resource of the long running operation + :rtype: Any + + :raises ~azure.core.exceptions.HttpResponseError: Server problem with the query. + """ + return self._polling_method.resource() + + @distributed_trace + def wait(self, timeout: Optional[float] = None) -> None: + """Wait on the long running operation for a number of seconds. + + You can check if this call has ended with timeout with the "done()" method. + + :param float timeout: Period of time to wait for the long running operation to complete (in seconds). + + :raises ~azure.core.exceptions.HttpResponseError: Server problem with the query. + """ + + if not self._polling_method.finished(): + self._done = threading.Event() + self._thread = threading.Thread( + target=with_current_context(self._start), name=f"KeyVaultOperationPoller({uuid.uuid4()})" + ) + self._thread.daemon = True + self._thread.start() + + if self._thread is None: + return + self._thread.join(timeout=timeout) + try: + # Let's handle possible None in forgiveness here + raise self._exception # type: ignore + except TypeError: # Was None + pass + + +class DeleteRecoverPollingMethod(PollingMethod): + """Poller for deleting resources, and recovering deleted resources, in vaults with soft-delete enabled. + + This works by polling for the existence of the deleted or recovered resource. When a resource is deleted, Key Vault + immediately removes it from its collection. However, the resource will not immediately appear in the deleted + collection. Key Vault will therefore respond 404 to GET requests for the deleted resource; when it responds 2xx, + the resource exists in the deleted collection i.e. its deletion is complete. + + Similarly, while recovering a deleted resource, Key Vault will respond 404 to GET requests for the non-deleted + resource; when it responds 2xx, the resource exists in the non-deleted collection, i.e. its recovery is complete. + + :param pipeline_response: The operation's original pipeline response. + :type pipeline_response: PipelineResponse + :param command: A callable to invoke when polling. + :type command: Callable + :param final_resource: The final resource returned by the polling operation. + :type final_resource: Any + :param bool finished: Whether or not the polling operation is completed. + :param int interval: The polling interval, in seconds. + """ + + def __init__( + self, + pipeline_response: PipelineResponse, + command: Callable, + final_resource: Any, + finished: bool, + interval: int = 2, + ) -> None: + self._pipeline_response = pipeline_response + self._command = command + self._resource = final_resource + self._polling_interval = interval + self._finished = finished + + def _update_status(self) -> None: + try: + self._command() + self._finished = True + except ResourceNotFoundError: + pass + except HttpResponseError as e: + # If we are polling on get_deleted_* and we don't have get permissions, we will get + # ResourceNotFoundError until the resource is recovered, at which point we'll get a 403. + if e.status_code == 403: + self._finished = True + else: + raise + + def initialize(self, client: Any, initial_response: Any, deserialization_callback: Callable) -> None: + pass + + def run(self) -> None: + while not self.finished(): + self._update_status() + if not self.finished(): + # We should always ask the client's transport to sleep, instead of sleeping directly + transport: HttpTransport = cast(HttpTransport, self._pipeline_response.context.transport) + transport.sleep(self._polling_interval) + + def finished(self) -> bool: + return self._finished + + def resource(self) -> Any: + return self._resource + + def status(self) -> str: + return "finished" if self._finished else "polling" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling_async.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling_async.py new file mode 100644 index 000000000000..16168229af08 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/_polling_async.py @@ -0,0 +1,80 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import Any, Callable, cast + +from azure.core.exceptions import ResourceNotFoundError, HttpResponseError +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import AsyncHttpTransport +from azure.core.polling import AsyncPollingMethod + + +class AsyncDeleteRecoverPollingMethod(AsyncPollingMethod): + """Poller for deleting resources, and recovering deleted resources, in vaults with soft-delete enabled. + + This works by polling for the existence of the deleted or recovered resource. When a resource is deleted, Key Vault + immediately removes it from its collection. However, the resource will not immediately appear in the deleted + collection. Key Vault will therefore respond 404 to GET requests for the deleted resource; when it responds 2xx, + the resource exists in the deleted collection i.e. its deletion is complete. + + Similarly, while recovering a deleted resource, Key Vault will respond 404 to GET requests for the non-deleted + resource; when it responds 2xx, the resource exists in the non-deleted collection, i.e. its recovery is complete. + + :param pipeline_response: The operation's original pipeline response. + :type pipeline_response: PipelineResponse + :param command: An awaitable to invoke when polling. + :type command: Callable + :param final_resource: The final resource returned by the polling operation. + :type final_resource: Any + :param bool finished: Whether or not the polling operation is completed. + :param int interval: The polling interval, in seconds. + """ + + def __init__( + self, + pipeline_response: PipelineResponse, + command: Callable, + final_resource: Any, + finished: bool, + interval: int = 2, + ) -> None: + self._pipeline_response = pipeline_response + self._command = command + self._resource = final_resource + self._polling_interval = interval + self._finished = finished + + def initialize(self, client, initial_response, deserialization_callback): + pass + + async def _update_status(self) -> None: + try: + await self._command() + self._finished = True + except ResourceNotFoundError: + pass + except HttpResponseError as e: + # If we are polling on get_deleted_* and we don't have get permissions, we will get + # ResourceNotFoundError until the resource is recovered, at which point we'll get a 403. + if e.status_code == 403: + self._finished = True + else: + raise + + async def run(self) -> None: + while not self.finished(): + await self._update_status() + if not self.finished(): + # We should always ask the client's transport to sleep, instead of sleeping directly + transport: AsyncHttpTransport = cast(AsyncHttpTransport, self._pipeline_response.context.transport) + await transport.sleep(self._polling_interval) + + def finished(self) -> bool: + return self._finished + + def resource(self) -> Any: + return self._resource + + def status(self) -> str: + return "finished" if self._finished else "polling" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py new file mode 100644 index 000000000000..0f84607e3ccd --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py @@ -0,0 +1,262 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Policy implementing Key Vault's challenge authentication protocol. + +Normally the protocol is only used for the client's first service request, upon which: +1. The challenge authentication policy sends a copy of the request, without authorization or content. +2. Key Vault responds 401 with a header (the 'challenge') detailing how the client should authenticate such a request. +3. The policy authenticates according to the challenge and sends the original request with authorization. + +The policy caches the challenge and thus knows how to authenticate future requests. However, authentication +requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the +protocol again. +""" + +from copy import deepcopy +import sys +import time +from typing import Any, Callable, cast, Optional, overload, TypeVar, Union +from urllib.parse import urlparse + +from typing_extensions import ParamSpec + +from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions +from azure.core.credentials_async import AsyncSupportsTokenInfo, AsyncTokenCredential, AsyncTokenProvider +from azure.core.pipeline import PipelineRequest, PipelineResponse +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy +from azure.core.rest import AsyncHttpResponse, HttpRequest + +from .http_challenge import HttpChallenge +from . import http_challenge_cache as ChallengeCache +from .challenge_auth_policy import _enforce_tls, _has_claims, _update_challenge + +if sys.version_info < (3, 9): + from typing import Awaitable +else: + from collections.abc import Awaitable + + +P = ParamSpec("P") +T = TypeVar("T") + + +@overload +async def await_result(func: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwargs) -> T: ... + + +@overload +async def await_result(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: ... + + +async def await_result(func: Callable[P, Union[T, Awaitable[T]]], *args: P.args, **kwargs: P.kwargs) -> T: + """If func returns an awaitable, await it. + + :param func: The function to run. + :type func: callable + :param args: The positional arguments to pass to the function. + :type args: list + :rtype: any + :return: The result of the function + """ + result = func(*args, **kwargs) + if isinstance(result, Awaitable): + return await result + return result + + + +class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): + """Policy for handling HTTP authentication challenges. + + :param credential: An object which can provide an access token for the vault, such as a credential from + :mod:`azure.identity.aio` + :type credential: ~azure.core.credentials_async.AsyncTokenProvider + """ + + def __init__(self, credential: AsyncTokenProvider, *scopes: str, **kwargs: Any) -> None: + # Pass `enable_cae` so `enable_cae=True` is always passed through self.authorize_request + super().__init__(credential, *scopes, enable_cae=True, **kwargs) + self._credential: AsyncTokenProvider = credential + self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None + self._verify_challenge_resource = kwargs.pop("verify_challenge_resource", True) + self._request_copy: Optional[HttpRequest] = None + + async def send( + self, request: PipelineRequest[HttpRequest] + ) -> PipelineResponse[HttpRequest, AsyncHttpResponse]: + """Authorize request with a bearer token and send it to the next policy. + + We implement this method to account for the valid scenario where a Key Vault authentication challenge is + immediately followed by a CAE claims challenge. The base class's implementation would return the second 401 to + the caller, but we should handle that second challenge as well (and only return any third 401 response). + + :param request: The pipeline request object + :type request: ~azure.core.pipeline.PipelineRequest + :return: The pipeline response object + :rtype: ~azure.core.pipeline.PipelineResponse + """ + await await_result(self.on_request, request) + response: PipelineResponse[HttpRequest, AsyncHttpResponse] + try: + response = await self.next.send(request) + except Exception: # pylint:disable=broad-except + await await_result(self.on_exception, request) + raise + await await_result(self.on_response, request, response) + + if response.http_response.status_code == 401: + return await self.handle_challenge_flow(request, response) + return response + + async def handle_challenge_flow( + self, + request: PipelineRequest[HttpRequest], + response: PipelineResponse[HttpRequest, AsyncHttpResponse], + consecutive_challenge: bool = False, + ) -> PipelineResponse[HttpRequest, AsyncHttpResponse]: + """Handle the challenge flow of Key Vault and CAE authentication. + + :param request: The pipeline request object + :type request: ~azure.core.pipeline.PipelineRequest + :param response: The pipeline response object + :type response: ~azure.core.pipeline.PipelineResponse + :param bool consecutive_challenge: Whether the challenge is arriving immediately after another challenge. + Consecutive challenges can only be valid if a Key Vault challenge is followed by a CAE claims challenge. + True if the preceding challenge was a Key Vault challenge; False otherwise. + + :return: The pipeline response object + :rtype: ~azure.core.pipeline.PipelineResponse + """ + self._token = None # any cached token is invalid + if "WWW-Authenticate" in response.http_response.headers: + # If the previous challenge was a KV challenge and this one is too, return the 401 + claims_challenge = _has_claims(response.http_response.headers["WWW-Authenticate"]) + if consecutive_challenge and not claims_challenge: + return response + + request_authorized = await self.on_challenge(request, response) + if request_authorized: + # if we receive a challenge response, we retrieve a new token + # which matches the new target. In this case, we don't want to remove + # token from the request so clear the 'insecure_domain_change' tag + request.context.options.pop("insecure_domain_change", False) + try: + response = await self.next.send(request) + except Exception: # pylint:disable=broad-except + await await_result(self.on_exception, request) + raise + + # If consecutive_challenge == True, this could be a third consecutive 401 + if response.http_response.status_code == 401 and not consecutive_challenge: + # If the previous challenge wasn't from CAE, we can try this function one more time + if not claims_challenge: + return await self.handle_challenge_flow(request, response, consecutive_challenge=True) + await await_result(self.on_response, request, response) + return response + + + async def on_request(self, request: PipelineRequest) -> None: + _enforce_tls(request) + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token(): + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + await self._request_kv_token(scope, challenge) + + bearer_token = cast(Union[AccessToken, AccessTokenInfo], self._token).token + request.http_request.headers["Authorization"] = f"Bearer {bearer_token}" + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, use the original request including the body, authorize the + # request, and tell super to send it again. + if request.http_request.content: + self._request_copy = request.http_request + bodiless_request = HttpRequest( + method=request.http_request.method, + url=request.http_request.url, + headers=deepcopy(request.http_request.headers), + ) + bodiless_request.headers["Content-Length"] = "0" + request.http_request = bodiless_request + + async def on_challenge(self, request: PipelineRequest, response: PipelineResponse) -> bool: + try: + # CAE challenges may not include a scope or tenant; cache from the previous challenge to use if necessary + old_scope: Optional[str] = None + old_tenant: Optional[str] = None + cached_challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if cached_challenge: + old_scope = cached_challenge.get_scope() or cached_challenge.get_resource() + "/.default" + old_tenant = cached_challenge.tenant_id + + challenge = _update_challenge(request, response) + # CAE challenges may not include a scope or tenant; use the previous challenge's values if necessary + if challenge.claims and old_scope: + challenge._parameters["scope"] = old_scope # pylint:disable=protected-access + challenge.tenant_id = old_tenant + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + except ValueError: + return False + + if self._verify_challenge_resource: + resource_domain = urlparse(scope).netloc + if not resource_domain: + raise ValueError(f"The challenge contains invalid scope '{scope}'.") + + request_domain = urlparse(request.http_request.url).netloc + if not request_domain.lower().endswith(f".{resource_domain.lower()}"): + raise ValueError( + f"The challenge resource '{resource_domain}' does not match the requested domain. Pass " + "`verify_challenge_resource=False` to your client's constructor to disable this verification. " + "See https://aka.ms/azsdk/blog/vault-uri for more information." + ) + + # If we had created a request copy in on_request, use it now to send along the original body content + if self._request_copy: + request.http_request = self._request_copy + + # The tenant parsed from AD FS challenges is "adfs"; we don't actually need a tenant for AD FS authentication + # For AD FS we skip cross-tenant authentication per https://github.com/Azure/azure-sdk-for-python/issues/28648 + if challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs"): + await self.authorize_request(request, scope, claims=challenge.claims) + else: + await self.authorize_request( + request, scope, claims=challenge.claims, tenant_id=challenge.tenant_id + ) + + return True + + def _need_new_token(self) -> bool: + now = time.time() + refresh_on = getattr(self._token, "refresh_on", None) + return not self._token or (refresh_on and refresh_on <= now) or self._token.expires_on - now < 300 + + async def _request_kv_token(self, scope: str, challenge: HttpChallenge) -> None: + """Implementation of BearerTokenCredentialPolicy's _request_token method, but specific to Key Vault. + + :param str scope: The scope for which to request a token. + :param challenge: The challenge for the request being made. + :type challenge: HttpChallenge + """ + # Exclude tenant for AD FS authentication + exclude_tenant = challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs") + # The AsyncSupportsTokenInfo protocol needs TokenRequestOptions for token requests instead of kwargs + if hasattr(self._credential, "get_token_info"): + options: TokenRequestOptions = {"enable_cae": True} + if challenge.tenant_id and not exclude_tenant: + options["tenant_id"] = challenge.tenant_id + self._token = await cast(AsyncSupportsTokenInfo, self._credential).get_token_info(scope, options=options) + else: + if exclude_tenant: + self._token = await self._credential.get_token(scope, enable_cae=True) + else: + self._token = await cast(AsyncTokenCredential, self._credential).get_token( + scope, tenant_id=challenge.tenant_id, enable_cae=True + ) diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_client_base.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_client_base.py new file mode 100644 index 000000000000..3e1a2bec8fc4 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_client_base.py @@ -0,0 +1,117 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import sys +from typing import Any + +from azure.core.credentials_async import AsyncTokenCredential +from azure.core.pipeline.policies import HttpLoggingPolicy +from azure.core.rest import AsyncHttpResponse, HttpRequest +from azure.core.tracing.decorator_async import distributed_trace_async + +from . import AsyncChallengeAuthPolicy +from .client_base import ApiVersion, DEFAULT_VERSION, _format_api_version, _SERIALIZER +from .._sdk_moniker import SDK_MONIKER +from .._generated.aio import KeyVaultClient as _KeyVaultClient +from .._generated import models as _models + +if sys.version_info < (3, 9): + from typing import Awaitable +else: + from collections.abc import Awaitable + + +class AsyncKeyVaultClientBase(object): + # pylint:disable=protected-access + def __init__(self, vault_url: str, credential: AsyncTokenCredential, **kwargs: Any) -> None: + if not credential: + raise ValueError( + "credential should be an object supporting the AsyncTokenCredential 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") + + try: + self.api_version = kwargs.pop("api_version", DEFAULT_VERSION) + # If API version was provided as an enum value, need to make a plain string for 3.11 compatibility + if hasattr(self.api_version, "value"): + self.api_version = self.api_version.value + self._vault_url = vault_url.strip(" /") + + client = kwargs.get("generated_client") + if client: + # caller provided a configured client -> only models left to initialize + self._client = client + models = kwargs.get("generated_models") + self._models = models or _models + return + + http_logging_policy = HttpLoggingPolicy(**kwargs) + http_logging_policy.allowed_header_names.update( + {"x-ms-keyvault-network-info", "x-ms-keyvault-region", "x-ms-keyvault-service-version"} + ) + + verify_challenge = kwargs.pop("verify_challenge_resource", True) + self._client = _KeyVaultClient( + credential=credential, + vault_base_url=self._vault_url, + api_version=self.api_version, + authentication_policy=AsyncChallengeAuthPolicy(credential, verify_challenge_resource=verify_challenge), + sdk_moniker=SDK_MONIKER, + http_logging_policy=http_logging_policy, + **kwargs, + ) + self._models = _models + except ValueError as exc: + # Ignore pyright error that comes from not identifying ApiVersion as an iterable enum + raise NotImplementedError( + f"This package doesn't support API version '{self.api_version}'. " + + "Supported versions: " + + f"{', '.join(v.value for v in ApiVersion)}" # pyright: ignore[reportGeneralTypeIssues] + ) from exc + + @property + def vault_url(self) -> str: + return self._vault_url + + async def __aenter__(self) -> "AsyncKeyVaultClientBase": + await self._client.__aenter__() + return self + + async def __aexit__(self, *args: Any) -> None: + await self._client.__aexit__(*args) + + async def close(self) -> None: + """Close sockets opened by the client. + + Calling this method is unnecessary when using the client as a context manager. + """ + await self._client.close() + + @distributed_trace_async + def send_request( + self, request: HttpRequest, *, stream: bool = False, **kwargs: Any + ) -> Awaitable[AsyncHttpResponse]: + """Runs a network request using the client's existing pipeline. + + The request URL can be relative to the vault URL. The service API version used for the request is the same as + the client's unless otherwise specified. This method does not raise if the response is an error; to raise an + exception, call `raise_for_status()` on the returned response object. For more information about how to send + custom requests with this method, see https://aka.ms/azsdk/dpcodegen/python/send_request. + + :param request: The network request you want to make. + :type request: ~azure.core.rest.HttpRequest + + :keyword bool stream: Whether the response payload will be streamed. Defaults to False. + + :return: The response of your network call. Does not do error handling on your response. + :rtype: ~azure.core.rest.AsyncHttpResponse + """ + request_copy = _format_api_version(request, self.api_version) + path_format_arguments = { + "vaultBaseUrl": _SERIALIZER.url("vault_base_url", self._vault_url, "str", skip_quote=True), + } + request_copy.url = self._client._client.format_url(request_copy.url, **path_format_arguments) + return self._client._client.send_request(request_copy, stream=stream, **kwargs) diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py new file mode 100644 index 000000000000..eb4073d0e699 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py @@ -0,0 +1,270 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Policy implementing Key Vault's challenge authentication protocol. + +Normally the protocol is only used for the client's first service request, upon which: +1. The challenge authentication policy sends a copy of the request, without authorization or content. +2. Key Vault responds 401 with a header (the 'challenge') detailing how the client should authenticate such a request. +3. The policy authenticates according to the challenge and sends the original request with authorization. + +The policy caches the challenge and thus knows how to authenticate future requests. However, authentication +requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the +protocol again. +""" + +from copy import deepcopy +import time +from typing import Any, cast, Optional, Union +from urllib.parse import urlparse + +from azure.core.credentials import ( + AccessToken, + AccessTokenInfo, + TokenCredential, + TokenProvider, + TokenRequestOptions, + SupportsTokenInfo, +) +from azure.core.exceptions import ServiceRequestError +from azure.core.pipeline import PipelineRequest, PipelineResponse +from azure.core.pipeline.policies import BearerTokenCredentialPolicy +from azure.core.rest import HttpRequest, HttpResponse + +from .http_challenge import HttpChallenge +from . import http_challenge_cache as ChallengeCache + + +def _enforce_tls(request: PipelineRequest) -> None: + if not request.http_request.url.lower().startswith("https"): + raise ServiceRequestError( + "Bearer token authentication is not permitted for non-TLS protected (non-https) URLs." + ) + + +def _has_claims(challenge: str) -> bool: + """Check if a challenge header contains claims. + + :param challenge: The challenge header to check. + :type challenge: str + + :returns: True if the challenge contains claims; False otherwise. + :rtype: bool + """ + # Split the challenge into its scheme and parameters, then check if any parameter contains claims + split_challenge = challenge.strip().split(" ", 1) + return any("claims=" in item for item in split_challenge[1].split(",")) + + +def _update_challenge(request: PipelineRequest, challenger: PipelineResponse) -> HttpChallenge: + """Parse challenge from a challenge response, cache it, and return it. + + :param request: The pipeline request that prompted the challenge response. + :type request: ~azure.core.pipeline.PipelineRequest + :param challenger: The pipeline response containing the authentication challenge. + :type challenger: ~azure.core.pipeline.PipelineResponse + + :returns: An HttpChallenge object representing the authentication challenge. + :rtype: HttpChallenge + """ + + challenge = HttpChallenge( + request.http_request.url, + challenger.http_response.headers.get("WWW-Authenticate"), + response_headers=challenger.http_response.headers, + ) + ChallengeCache.set_challenge_for_url(request.http_request.url, challenge) + return challenge + + +class ChallengeAuthPolicy(BearerTokenCredentialPolicy): + """Policy for handling HTTP authentication challenges. + + :param credential: An object which can provide an access token for the vault, such as a credential from + :mod:`azure.identity` + :type credential: ~azure.core.credentials.TokenProvider + :param str scopes: Lets you specify the type of access needed. + """ + + def __init__(self, credential: TokenProvider, *scopes: str, **kwargs: Any) -> None: + # Pass `enable_cae` so `enable_cae=True` is always passed through self.authorize_request + super(ChallengeAuthPolicy, self).__init__(credential, *scopes, enable_cae=True, **kwargs) + self._credential: TokenProvider = credential + self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None + self._verify_challenge_resource = kwargs.pop("verify_challenge_resource", True) + self._request_copy: Optional[HttpRequest] = None + + def send(self, request: PipelineRequest[HttpRequest]) -> PipelineResponse[HttpRequest, HttpResponse]: + """Authorize request with a bearer token and send it to the next policy. + + We implement this method to account for the valid scenario where a Key Vault authentication challenge is + immediately followed by a CAE claims challenge. The base class's implementation would return the second 401 to + the caller, but we should handle that second challenge as well (and only return any third 401 response). + + :param request: The pipeline request object + :type request: ~azure.core.pipeline.PipelineRequest + + :return: The pipeline response object + :rtype: ~azure.core.pipeline.PipelineResponse + """ + self.on_request(request) + try: + response = self.next.send(request) + except Exception: # pylint:disable=broad-except + self.on_exception(request) + raise + + self.on_response(request, response) + if response.http_response.status_code == 401: + return self.handle_challenge_flow(request, response) + return response + + def handle_challenge_flow( + self, + request: PipelineRequest[HttpRequest], + response: PipelineResponse[HttpRequest, HttpResponse], + consecutive_challenge: bool = False, + ) -> PipelineResponse[HttpRequest, HttpResponse]: + """Handle the challenge flow of Key Vault and CAE authentication. + + :param request: The pipeline request object + :type request: ~azure.core.pipeline.PipelineRequest + :param response: The pipeline response object + :type response: ~azure.core.pipeline.PipelineResponse + :param bool consecutive_challenge: Whether the challenge is arriving immediately after another challenge. + Consecutive challenges can only be valid if a Key Vault challenge is followed by a CAE claims challenge. + True if the preceding challenge was a Key Vault challenge; False otherwise. + + :return: The pipeline response object + :rtype: ~azure.core.pipeline.PipelineResponse + """ + self._token = None # any cached token is invalid + if "WWW-Authenticate" in response.http_response.headers: + # If the previous challenge was a KV challenge and this one is too, return the 401 + claims_challenge = _has_claims(response.http_response.headers["WWW-Authenticate"]) + if consecutive_challenge and not claims_challenge: + return response + + request_authorized = self.on_challenge(request, response) + if request_authorized: + # if we receive a challenge response, we retrieve a new token + # which matches the new target. In this case, we don't want to remove + # token from the request so clear the 'insecure_domain_change' tag + request.context.options.pop("insecure_domain_change", False) + try: + response = self.next.send(request) + except Exception: # pylint:disable=broad-except + self.on_exception(request) + raise + + # If consecutive_challenge == True, this could be a third consecutive 401 + if response.http_response.status_code == 401 and not consecutive_challenge: + # If the previous challenge wasn't from CAE, we can try this function one more time + if not claims_challenge: + return self.handle_challenge_flow(request, response, consecutive_challenge=True) + self.on_response(request, response) + return response + + def on_request(self, request: PipelineRequest) -> None: + _enforce_tls(request) + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._request_kv_token(scope, challenge) + + bearer_token = cast(Union["AccessToken", "AccessTokenInfo"], self._token).token + request.http_request.headers["Authorization"] = f"Bearer {bearer_token}" + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, use the original request including the body, authorize the + # request, and tell super to send it again. + if request.http_request.content: + self._request_copy = request.http_request + bodiless_request = HttpRequest( + method=request.http_request.method, + url=request.http_request.url, + headers=deepcopy(request.http_request.headers), + ) + bodiless_request.headers["Content-Length"] = "0" + request.http_request = bodiless_request + + def on_challenge(self, request: PipelineRequest, response: PipelineResponse) -> bool: + try: + # CAE challenges may not include a scope or tenant; cache from the previous challenge to use if necessary + old_scope: Optional[str] = None + old_tenant: Optional[str] = None + cached_challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if cached_challenge: + old_scope = cached_challenge.get_scope() or cached_challenge.get_resource() + "/.default" + old_tenant = cached_challenge.tenant_id + + challenge = _update_challenge(request, response) + # CAE challenges may not include a scope or tenant; use the previous challenge's values if necessary + if challenge.claims and old_scope: + challenge._parameters["scope"] = old_scope # pylint:disable=protected-access + challenge.tenant_id = old_tenant + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + except ValueError: + return False + + if self._verify_challenge_resource: + resource_domain = urlparse(scope).netloc + if not resource_domain: + raise ValueError(f"The challenge contains invalid scope '{scope}'.") + + request_domain = urlparse(request.http_request.url).netloc + if not request_domain.lower().endswith(f".{resource_domain.lower()}"): + raise ValueError( + f"The challenge resource '{resource_domain}' does not match the requested domain. Pass " + "`verify_challenge_resource=False` to your client's constructor to disable this verification. " + "See https://aka.ms/azsdk/blog/vault-uri for more information." + ) + + # If we had created a request copy in on_request, use it now to send along the original body content + if self._request_copy: + request.http_request = self._request_copy + + # The tenant parsed from AD FS challenges is "adfs"; we don't actually need a tenant for AD FS authentication + # For AD FS we skip cross-tenant authentication per https://github.com/Azure/azure-sdk-for-python/issues/28648 + if challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs"): + self.authorize_request(request, scope, claims=challenge.claims) + else: + self.authorize_request(request, scope, claims=challenge.claims, tenant_id=challenge.tenant_id) + + return True + + @property + def _need_new_token(self) -> bool: + now = time.time() + refresh_on = getattr(self._token, "refresh_on", None) + return not self._token or (refresh_on and refresh_on <= now) or self._token.expires_on - now < 300 + + def _request_kv_token(self, scope: str, challenge: HttpChallenge) -> None: + """Implementation of BearerTokenCredentialPolicy's _request_token method, but specific to Key Vault. + + :param str scope: The scope for which to request a token. + :param challenge: The challenge for the request being made. + :type challenge: HttpChallenge + """ + # Exclude tenant for AD FS authentication + exclude_tenant = challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs") + # The SupportsTokenInfo protocol needs TokenRequestOptions for token requests instead of kwargs + if hasattr(self._credential, "get_token_info"): + options: TokenRequestOptions = {"enable_cae": True} + if challenge.tenant_id and not exclude_tenant: + options["tenant_id"] = challenge.tenant_id + self._token = cast(SupportsTokenInfo, self._credential).get_token_info(scope, options=options) + else: + if exclude_tenant: + self._token = self._credential.get_token(scope, enable_cae=True) + else: + self._token = cast(TokenCredential, self._credential).get_token( + scope, tenant_id=challenge.tenant_id, enable_cae=True + ) diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/client_base.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/client_base.py new file mode 100644 index 000000000000..9e721ef6cfe2 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/client_base.py @@ -0,0 +1,162 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from copy import deepcopy +from enum import Enum +from typing import Any +from urllib.parse import urlparse + +from azure.core import CaseInsensitiveEnumMeta +from azure.core.credentials import TokenCredential +from azure.core.pipeline.policies import HttpLoggingPolicy +from azure.core.rest import HttpRequest, HttpResponse +from azure.core.tracing.decorator import distributed_trace + +from . import ChallengeAuthPolicy +from .._generated import KeyVaultClient as _KeyVaultClient +from .._generated import models as _models +from .._generated._utils.serialization import Serializer +from .._sdk_moniker import SDK_MONIKER + + +class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Key Vault API versions supported by this package""" + + #: this is the default version + V2025_07_01 = "2025-07-01" + V7_6 = "7.6" + V7_5 = "7.5" + V7_4 = "7.4" + V7_3 = "7.3" + V7_2 = "7.2" + V7_1 = "7.1" + V7_0 = "7.0" + V2016_10_01 = "2016-10-01" + + +DEFAULT_VERSION = ApiVersion.V2025_07_01 + +_SERIALIZER = Serializer() +_SERIALIZER.client_side_validation = False + + +def _format_api_version(request: HttpRequest, api_version: str) -> HttpRequest: + """Returns a request copy that includes an api-version query parameter if one wasn't originally present. + + :param request: The HTTP request being sent. + :type request: ~azure.core.rest.HttpRequest + :param str api_version: The service API version that the request should include. + + :returns: A copy of the request that includes an api-version query parameter. + :rtype: azure.core.rest.HttpRequest + """ + request_copy = deepcopy(request) + params = {"api-version": api_version} # By default, we want to use the client's API version + query = urlparse(request_copy.url).query + + if query: + request_copy.url = request_copy.url.partition("?")[0] + existing_params = {p[0]: p[-1] for p in [p.partition("=") for p in query.split("&")]} + params.update(existing_params) # If an api-version was provided, this will overwrite our default + + # Reconstruct the query parameters onto the URL + query_params = [] + for k, v in params.items(): + query_params.append("{}={}".format(k, v)) + query = "?" + "&".join(query_params) + request_copy.url = request_copy.url + query + return request_copy + + +class KeyVaultClientBase(object): + # pylint:disable=protected-access + def __init__(self, vault_url: str, credential: TokenCredential, **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") + + try: + self.api_version = kwargs.pop("api_version", DEFAULT_VERSION) + # If API version was provided as an enum value, need to make a plain string for 3.11 compatibility + if hasattr(self.api_version, "value"): + self.api_version = self.api_version.value + self._vault_url = vault_url.strip(" /") + + client = kwargs.get("generated_client") + if client: + # caller provided a configured client -> only models left to initialize + self._client = client + models = kwargs.get("generated_models") + self._models = models or _models + return + + http_logging_policy = HttpLoggingPolicy(**kwargs) + http_logging_policy.allowed_header_names.update( + {"x-ms-keyvault-network-info", "x-ms-keyvault-region", "x-ms-keyvault-service-version"} + ) + + verify_challenge = kwargs.pop("verify_challenge_resource", True) + self._client = _KeyVaultClient( + credential=credential, + vault_base_url=self._vault_url, + api_version=self.api_version, + authentication_policy=ChallengeAuthPolicy(credential, verify_challenge_resource=verify_challenge), + sdk_moniker=SDK_MONIKER, + http_logging_policy=http_logging_policy, + **kwargs, + ) + self._models = _models + except ValueError as exc: + # Ignore pyright error that comes from not identifying ApiVersion as an iterable enum + raise NotImplementedError( + f"This package doesn't support API version '{self.api_version}'. " + + "Supported versions: " + + f"{', '.join(v.value for v in ApiVersion)}" # pyright: ignore[reportGeneralTypeIssues] + ) from exc + + @property + def vault_url(self) -> str: + return self._vault_url + + def __enter__(self) -> "KeyVaultClientBase": + self._client.__enter__() + return self + + def __exit__(self, *args: Any) -> None: + self._client.__exit__(*args) + + def close(self) -> None: + """Close sockets opened by the client. + + Calling this method is unnecessary when using the client as a context manager. + """ + self._client.close() + + @distributed_trace + def send_request(self, request: HttpRequest, *, stream: bool = False, **kwargs: Any) -> HttpResponse: + """Runs a network request using the client's existing pipeline. + + The request URL can be relative to the vault URL. The service API version used for the request is the same as + the client's unless otherwise specified. This method does not raise if the response is an error; to raise an + exception, call `raise_for_status()` on the returned response object. For more information about how to send + custom requests with this method, see https://aka.ms/azsdk/dpcodegen/python/send_request. + + :param request: The network request you want to make. + :type request: ~azure.core.rest.HttpRequest + + :keyword bool stream: Whether the response payload will be streamed. Defaults to False. + + :return: The response of your network call. Does not do error handling on your response. + :rtype: ~azure.core.rest.HttpResponse + """ + request_copy = _format_api_version(request, self.api_version) + path_format_arguments = { + "vaultBaseUrl": _SERIALIZER.url("vault_base_url", self._vault_url, "str", skip_quote=True), + } + request_copy.url = self._client._client.format_url(request_copy.url, **path_format_arguments) + return self._client._client.send_request(request_copy, stream=stream, **kwargs) diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py new file mode 100644 index 000000000000..8b14b999de78 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py @@ -0,0 +1,186 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import base64 +from typing import Dict, MutableMapping, Optional +from urllib import parse + + +class HttpChallenge(object): + """An object representing the content of a Key Vault authentication challenge. + + :param str request_uri: The URI of the HTTP request that prompted this challenge. + :param str challenge: The WWW-Authenticate header of the challenge response. + :param response_headers: Optional. The headers attached to the challenge response. + :type response_headers: MutableMapping[str, str] or None + """ + + def __init__( + self, request_uri: str, challenge: str, response_headers: "Optional[MutableMapping[str, str]]" = None + ) -> None: + """Parses an HTTP WWW-Authentication Bearer challenge from a server. + + Example challenge with claims: + Bearer authorization="https://login.windows-ppe.net/", error="invalid_token", + error_description="User session has been revoked", + claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwMzc0MjgwMCJ9fX0=" + """ + self.source_authority = self._validate_request_uri(request_uri) + self.source_uri = request_uri + self._parameters: "Dict[str, str]" = {} + + # get the scheme of the challenge and remove from the challenge string + trimmed_challenge = self._validate_challenge(challenge) + split_challenge = trimmed_challenge.split(" ", 1) + self.scheme = split_challenge[0] + trimmed_challenge = split_challenge[1] + + self.claims = None + # split trimmed challenge into comma-separated name=value pairs. Values are expected + # to be surrounded by quotes which are stripped here. + for item in trimmed_challenge.split(","): + # Special case for claims, which can contain = symbols as padding. Assume at most one claim per challenge + if "claims=" in item: + encoded_claims = item[item.index("=") + 1 :].strip(" \"'") + padding_needed = -len(encoded_claims) % 4 + try: + decoded_claims = base64.urlsafe_b64decode(encoded_claims + "=" * padding_needed).decode() + self.claims = decoded_claims + except Exception: # pylint:disable=broad-except + continue + # process name=value pairs + else: + comps = item.split("=") + if len(comps) == 2: + key = comps[0].strip(' "') + value = comps[1].strip(' "') + if key: + self._parameters[key] = value + + # minimum set of parameters + if not self._parameters: + raise ValueError("Invalid challenge parameters") + + # must specify authorization or authorization_uri + if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: + raise ValueError("Invalid challenge parameters") + + authorization_uri = self.get_authorization_server() + # the authorization server URI should look something like https://login.windows.net/tenant-id + raw_uri_path = str(parse.urlparse(authorization_uri).path) + uri_path = raw_uri_path.lstrip("/") + self.tenant_id = uri_path.split("/", maxsplit=1)[0] or None + + # if the response headers were supplied + if response_headers: + # get the message signing key and message key encryption key from the headers + self.server_signature_key = response_headers.get("x-ms-message-signing-key", None) + self.server_encryption_key = response_headers.get("x-ms-message-encryption-key", None) + + def is_bearer_challenge(self) -> bool: + """Tests whether the HttpChallenge is a Bearer challenge. + + :returns: True if the challenge is a Bearer challenge; False otherwise. + :rtype: bool + """ + if not self.scheme: + return False + + return self.scheme.lower() == "bearer" + + def is_pop_challenge(self) -> bool: + """Tests whether the HttpChallenge is a proof of possession challenge. + + :returns: True if the challenge is a proof of possession challenge; False otherwise. + :rtype: bool + """ + if not self.scheme: + return False + + return self.scheme.lower() == "pop" + + def get_value(self, key: str) -> "Optional[str]": + return self._parameters.get(key) + + def get_authorization_server(self) -> str: + """Returns the URI for the authorization server if present, otherwise an empty string. + + :returns: The URI for the authorization server if present, otherwise an empty string. + :rtype: str + """ + value = "" + for key in ["authorization_uri", "authorization"]: + value = self.get_value(key) or "" + if value: + break + return value + + def get_resource(self) -> str: + """Returns the resource if present, otherwise an empty string. + + :returns: The challenge resource if present, otherwise an empty string. + :rtype: str + """ + return self.get_value("resource") or "" + + def get_scope(self) -> str: + """Returns the scope if present, otherwise an empty string. + + :returns: The challenge scope if present, otherwise an empty string. + :rtype: str + """ + return self.get_value("scope") or "" + + def supports_pop(self) -> bool: + """Returns True if the challenge supports proof of possession token auth; False otherwise. + + :returns: True if the challenge supports proof of possession token auth; False otherwise. + :rtype: bool + """ + return self._parameters.get("supportspop", "").lower() == "true" + + def supports_message_protection(self) -> bool: + """Returns True if the challenge vault supports message protection; False otherwise. + + :returns: True if the challenge vault supports message protection; False otherwise. + :rtype: bool + """ + return self.supports_pop() and self.server_encryption_key and self.server_signature_key # type: ignore + + def _validate_challenge( + self, challenge: str + ) -> str: # pylint:disable=bad-option-value,useless-option-value,no-self-use + """Verifies that the challenge is a valid auth challenge and returns the key=value pairs. + + :param str challenge: The WWW-Authenticate header of the challenge response. + + :returns: The challenge key/value pairs, with whitespace removed, as a string. + :rtype: str + """ + if not challenge: + raise ValueError("Challenge cannot be empty") + + return challenge.strip() + + def _validate_request_uri( + self, uri: str + ) -> str: # pylint:disable=bad-option-value,useless-option-value,no-self-use + """Extracts the host authority from the given URI. + + :param str uri: The URI of the HTTP request that prompted the challenge. + + :returns: The challenge host authority. + :rtype: str + """ + if not uri: + raise ValueError("request_uri cannot be empty") + + parsed = parse.urlparse(uri) + if not parsed.netloc: + raise ValueError("request_uri must be an absolute URI") + + if parsed.scheme.lower() not in ["http", "https"]: + raise ValueError("request_uri must be HTTP or HTTPS") + + return parsed.netloc diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge_cache.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge_cache.py new file mode 100644 index 000000000000..99f32091e24b --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge_cache.py @@ -0,0 +1,93 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import threading +from typing import Dict, Optional +from urllib import parse + +from .http_challenge import HttpChallenge + + +_cache: "Dict[str, HttpChallenge]" = {} +_lock = threading.Lock() + + +def get_challenge_for_url(url: str) -> "Optional[HttpChallenge]": + """Gets the challenge for the cached URL. + + :param str url: the URL the challenge is cached for. + + :returns: The challenge for the cached request URL, or None if the request URL isn't cached. + :rtype: HttpChallenge or None + """ + + if not url: + raise ValueError("URL cannot be None") + + key = _get_cache_key(url) + + with _lock: + return _cache.get(key.lower()) + + +def _get_cache_key(url: str) -> str: + """Use the URL's netloc as cache key except when the URL specifies the default port for its scheme. In that case + use the netloc without the port. That is to say, https://foo.bar and https://foo.bar:443 are considered equivalent. + + This equivalency prevents an unnecessary challenge when using Key Vault's paging API. The Key Vault client doesn't + specify ports, but Key Vault's next page links do, so a redundant challenge would otherwise be executed when the + client requests the next page. + + :param str url: The HTTP request URL. + + :returns: The URL's `netloc`, minus any port attached to the URL. + :rtype: str + """ + + parsed = parse.urlparse(url) + if parsed.scheme == "https" and parsed.port == 443: + return parsed.netloc[:-4] + return parsed.netloc + + +def remove_challenge_for_url(url: str) -> None: + """Removes the cached challenge for the specified URL. + + :param str url: the URL for which to remove the cached challenge + """ + if not url: + raise ValueError("URL cannot be empty") + + key = _get_cache_key(url) + with _lock: + del _cache[key.lower()] + + +def set_challenge_for_url(url: str, challenge: "HttpChallenge") -> None: + """Caches the challenge for the specified URL. + + :param str url: the URL for which to cache the challenge + :param challenge: the challenge to cache + :type challenge: HttpChallenge + """ + if not url: + raise ValueError("URL cannot be empty") + + if not challenge: + raise ValueError("Challenge cannot be empty") + + src_url = parse.urlparse(url) + if src_url.netloc.lower() != challenge.source_authority.lower(): + raise ValueError("Source URL and Challenge URL do not match") + + key = _get_cache_key(url) + with _lock: + _cache[key.lower()] = challenge + + +def clear() -> None: + """Clears the cache.""" + + with _lock: + _cache.clear() diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_version.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_version.py new file mode 100644 index 000000000000..f526d23db20e --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_version.py @@ -0,0 +1,6 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +VERSION = "4.11.2" diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/__init__.py new file mode 100644 index 000000000000..ba1c15a8d7ae --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/__init__.py @@ -0,0 +1,8 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +from ._client import CertificateClient + +__all__ = ["CertificateClient"] diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_client.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_client.py new file mode 100644 index 000000000000..96efea0105c8 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_client.py @@ -0,0 +1,1077 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +# pylint:disable=too-many-lines,too-many-public-methods +import base64 +from typing import Any, Dict, List, Optional, Union +from functools import partial + +from azure.core.polling import AsyncLROPoller +from azure.core.tracing.decorator import distributed_trace +from azure.core.tracing.decorator_async import distributed_trace_async +from azure.core.async_paging import AsyncItemPaged + +from .. import ( + AdministratorContact, + KeyVaultCertificate, + CertificateOperation, + CertificatePolicy, + DeletedCertificate, + CertificateProperties, + CertificateContact, + CertificateIssuer, + IssuerProperties, +) +from ._polling_async import CreateCertificatePollerAsync +from .._client import NO_SAN_OR_SUBJECT +from .._shared import AsyncKeyVaultClientBase +from .._shared._polling_async import AsyncDeleteRecoverPollingMethod + + +class CertificateClient(AsyncKeyVaultClientBase): + """A high-level asynchronous interface for managing a vault's certificates. + + :param str vault_url: URL of the vault the client will access. This is also called the vault's "DNS Name". + You should validate that this URL references a valid Key Vault resource. See https://aka.ms/azsdk/blog/vault-uri + for details. + :param credential: An object which can provide an access token for the vault, such as a credential from + :mod:`azure.identity.aio` + :type credential: ~azure.core.credentials_async.AsyncTokenCredential + + :keyword api_version: Version of the service API to use. Defaults to the most recent. + :paramtype api_version: ~azure.keyvault.certificates.ApiVersion or str + :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key + Vault domain. Defaults to True. + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START create_certificate_client] + :end-before: [END create_certificate_client] + :language: python + :dedent: 4 + :caption: Creates a new instance of the Certificate client + """ + + # pylint:disable=protected-access + @distributed_trace_async + async def create_certificate( + self, + certificate_name: str, + policy: CertificatePolicy, + *, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + preserve_order: Optional[bool] = None, + **kwargs: Any, + ) -> Union[KeyVaultCertificate, CertificateOperation]: + """Creates a new certificate. + + If this is the first version, the certificate resource is created. This operation requires the + certificates/create permission. The poller requires the certificates/get permission, otherwise raises an + :class:`~azure.core.exceptions.HttpResponseError`. + + :param str certificate_name: The name of the certificate. + :param policy: The management policy for the certificate. Either subject or one of the subject alternative + name properties are required. + :type policy: ~azure.keyvault.certificates.CertificatePolicy + + :keyword bool enabled: Whether the certificate is enabled for use. + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] + :keyword bool preserve_order: Whether to preserve the order of the certificate chain. + + :returns: A coroutine for the creation of the certificate. Awaiting the coroutine returns the created + KeyVaultCertificate if creation is successful, or the CertificateOperation if not. + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate or ~azure.keyvault.certificates.CertificateOperation + + :raises ValueError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate policy is invalid; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START create_certificate] + :end-before: [END create_certificate] + :language: python + :caption: Create a certificate + :dedent: 8 + """ + if not ( + policy.san_emails + or policy.san_user_principal_names + or policy.san_dns_names + or policy.san_ip_addresses + or policy.san_uris + or policy.subject + ): + raise ValueError(NO_SAN_OR_SUBJECT) + + polling_interval = kwargs.pop("_polling_interval", None) + if polling_interval is None: + polling_interval = 5 + + if enabled is not None: + attributes = self._models.CertificateAttributes(enabled=enabled) + else: + attributes = None + + parameters = self._models.CertificateCreateParameters( + certificate_policy=policy._to_certificate_policy_bundle(), + certificate_attributes=attributes, + tags=tags, + preserve_cert_order=preserve_order, + ) + + pipeline_response, cert_bundle = await self._client.create_certificate( + certificate_name=certificate_name, + parameters=parameters, + cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), + **kwargs + ) + + create_certificate_operation = CertificateOperation._from_certificate_operation_bundle(cert_bundle) + + command = partial(self.get_certificate_operation, certificate_name=certificate_name, **kwargs) + + get_certificate_command = partial(self.get_certificate, certificate_name=certificate_name, **kwargs) + + create_certificate_polling = CreateCertificatePollerAsync( + pipeline_response=pipeline_response, + get_certificate_command=get_certificate_command, + interval=polling_interval, + ) + def no_op(*_, **__) -> Any: # The deserialization callback is ignored based on polling implementation + pass + return await AsyncLROPoller(command, create_certificate_operation, no_op, create_certificate_polling) + + @distributed_trace_async + async def get_certificate(self, certificate_name: str, **kwargs: Any) -> KeyVaultCertificate: + """Gets a certificate with its management policy attached. Requires certificates/get permission. + + Does not accept the version of the certificate as a parameter. To get a specific version of the + certificate, call :func:`get_certificate_version`. + + :param str certificate_name: The name of the certificate in the given vault. + + :returns: An instance of KeyVaultCertificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START get_certificate] + :end-before: [END get_certificate] + :language: python + :caption: Get a certificate + :dedent: 8 + """ + bundle = await self._client.get_certificate( + certificate_name=certificate_name, + certificate_version="", + **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace_async + async def get_certificate_version( + self, certificate_name: str, version: str, **kwargs: Any + ) -> KeyVaultCertificate: + """Gets a specific version of a certificate without returning its management policy. + + Requires certificates/get permission. To get the latest version of the certificate, or to get the certificate's + policy as well, call :func:`get_certificate`. + + :param str certificate_name: The name of the certificate in the given vault. + :param str version: The version of the certificate. + + :returns: An instance of KeyVaultCertificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START get_certificate_version] + :end-before: [END get_certificate_version] + :language: python + :caption: Get a certificate with a specific version + :dedent: 8 + """ + bundle = await self._client.get_certificate( + certificate_name=certificate_name, + certificate_version=version, + **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace_async + async def delete_certificate(self, certificate_name: str, **kwargs: Any) -> DeletedCertificate: + """Delete all versions of a certificate. Requires certificates/delete permission. + + If the vault has soft-delete enabled, deletion may take several seconds to complete. + + :param str certificate_name: The name of the certificate. + + :returns: The deleted certificate + :rtype: ~azure.keyvault.certificates.DeletedCertificate + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START delete_certificate] + :end-before: [END delete_certificate] + :language: python + :caption: Delete a certificate + :dedent: 8 + """ + polling_interval = kwargs.pop("_polling_interval", None) + if polling_interval is None: + polling_interval = 2 + pipeline_response, deleted_cert_bundle = await self._client.delete_certificate( + certificate_name=certificate_name, + cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), + **kwargs, + ) + deleted_certificate = DeletedCertificate._from_deleted_certificate_bundle(deleted_cert_bundle) + + polling_method = AsyncDeleteRecoverPollingMethod( + # no recovery ID means soft-delete is disabled, in which case we initialize the poller as finished + finished=deleted_certificate.recovery_id is None, + pipeline_response=pipeline_response, + command=partial(self.get_deleted_certificate, certificate_name=certificate_name, **kwargs), + final_resource=deleted_certificate, + interval=polling_interval, + ) + await polling_method.run() + + return polling_method.resource() + + @distributed_trace_async + async def get_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> DeletedCertificate: + """Get a deleted certificate. Possible only in a vault with soft-delete enabled. + + Requires certificates/get permission. Retrieves the deleted certificate information plus its attributes, such as + retention interval, scheduled permanent deletion, and the current deletion recovery level. + + :param str certificate_name: The name of the certificate. + + :return: The deleted certificate + :rtype: ~azure.keyvault.certificates.DeletedCertificate + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START get_deleted_certificate] + :end-before: [END get_deleted_certificate] + :language: python + :caption: Get a deleted certificate + :dedent: 8 + """ + bundle = await self._client.get_deleted_certificate( + certificate_name=certificate_name, **kwargs + ) + return DeletedCertificate._from_deleted_certificate_bundle(deleted_certificate_bundle=bundle) + + @distributed_trace_async + async def purge_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> None: + """Permanently deletes a deleted certificate. Possible only in vaults with soft-delete enabled. + + Requires certificates/purge permission. Performs an irreversible deletion of the specified certificate, without + possibility for recovery. The operation is not available if the + :py:attr:`~azure.keyvault.certificates.CertificateProperties.recovery_level` does not specify 'Purgeable'. + This method is only necessary for purging a certificate before its + :py:attr:`~azure.keyvault.certificates.DeletedCertificate.scheduled_purge_date`. + + :param str certificate_name: The name of the certificate + + :return: None + :rtype: None + + :raises ~azure.core.exceptions.HttpResponseError: + """ + await self._client.purge_deleted_certificate( + certificate_name=certificate_name, **kwargs + ) + + @distributed_trace_async + async def recover_deleted_certificate(self, certificate_name: str, **kwargs: Any) -> KeyVaultCertificate: + """Recover a deleted certificate to its latest version. Possible only in a vault with soft-delete enabled. + + Requires certificates/recover permission. If the vault does not have soft-delete enabled, + :func:`delete_certificate` is permanent, and this method will raise an error. Attempting to recover a + non-deleted certificate will also raise an error. + + :param str certificate_name: The name of the deleted certificate + + :returns: The recovered certificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START recover_deleted_certificate] + :end-before: [END recover_deleted_certificate] + :language: python + :caption: Recover a deleted certificate + :dedent: 8 + """ + polling_interval = kwargs.pop("_polling_interval", None) + if polling_interval is None: + polling_interval = 2 + pipeline_response, recovered_cert_bundle = await self._client.recover_deleted_certificate( + certificate_name=certificate_name, + cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), + **kwargs, + ) + recovered_certificate = KeyVaultCertificate._from_certificate_bundle(recovered_cert_bundle) + + command = partial(self.get_certificate, certificate_name=certificate_name, **kwargs) + polling_method = AsyncDeleteRecoverPollingMethod( + pipeline_response=pipeline_response, + command=command, + final_resource=recovered_certificate, + finished=False, + interval=polling_interval + ) + await polling_method.run() + + return polling_method.resource() + + @distributed_trace_async + async def import_certificate( + self, + certificate_name: str, + certificate_bytes: bytes, + *, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + password: Optional[str] = None, + policy: Optional[CertificatePolicy] = None, + preserve_order: Optional[bool] = None, + **kwargs: Any, + ) -> KeyVaultCertificate: + """Import a certificate created externally. Requires certificates/import permission. + + Imports an existing valid certificate, containing a private key, into Azure Key Vault. The certificate to be + imported can be in either PFX or PEM format. If the certificate is in PEM format the PEM file must contain the + key as well as x509 certificates, and you must provide a ``policy`` with + :attr:`~azure.keyvault.certificates.CertificatePolicy.content_type` of + :attr:`~azure.keyvault.certificates.CertificateContentType.pem`. + + :param str certificate_name: The name of the certificate. + :param bytes certificate_bytes: Bytes of the certificate object to import. + This certificate needs to contain the private key. + + :keyword bool enabled: Whether the certificate is enabled for use. + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] + :keyword str password: If the private key in the passed in certificate is encrypted, it + is the password used for encryption. + :keyword policy: The management policy for the certificate. Required if importing a PEM-format certificate, + with :attr:`~azure.keyvault.certificates.CertificatePolicy.content_type` set to + :attr:`~azure.keyvault.certificates.CertificateContentType.pem`. + :paramtype policy: ~azure.keyvault.certificates.CertificatePolicy + :keyword bool preserve_order: Whether to preserve the order of the certificate chain. + + :returns: The imported KeyVaultCertificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.HttpResponseError: + """ + + if enabled is not None: + attributes = self._models.CertificateAttributes(enabled=enabled) + else: + attributes = None + base64_encoded_certificate = base64.b64encode(certificate_bytes).decode("utf-8") + + parameters = self._models.CertificateImportParameters( + base64_encoded_certificate=base64_encoded_certificate, + password=password, + certificate_policy=policy._to_certificate_policy_bundle() if policy else None, + certificate_attributes=attributes, + tags=tags, + preserve_cert_order=preserve_order, + ) + + bundle = await self._client.import_certificate( + certificate_name=certificate_name, + parameters=parameters, + **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace_async + async def get_certificate_policy(self, certificate_name: str, **kwargs: Any) -> CertificatePolicy: + """Gets the policy for a certificate. Requires certificates/get permission. + + Returns the specified certificate policy resources in the key vault. + + :param str certificate_name: The name of the certificate in a given key vault. + + :return: The certificate policy + :rtype: ~azure.keyvault.certificates.CertificatePolicy + + :raises ~azure.core.exceptions.HttpResponseError: + """ + bundle = await self._client.get_certificate_policy( + certificate_name=certificate_name, **kwargs + ) + return CertificatePolicy._from_certificate_policy_bundle(certificate_policy_bundle=bundle) + + @distributed_trace_async + async def update_certificate_policy( + self, certificate_name: str, policy: CertificatePolicy, **kwargs: Any + ) -> CertificatePolicy: + """Updates the policy for a certificate. Requires certificates/update permission. + + Set specified members in the certificate policy. Leaves others as null. + + :param str certificate_name: The name of the certificate in the given vault. + :param policy: The policy for the certificate. + :type policy: ~azure.keyvault.certificates.CertificatePolicy + + :return: The certificate policy + :rtype: ~azure.keyvault.certificates.CertificatePolicy + + :raises ~azure.core.exceptions.HttpResponseError: + """ + bundle = await self._client.update_certificate_policy( + certificate_name=certificate_name, + certificate_policy=policy._to_certificate_policy_bundle(), + **kwargs + ) + return CertificatePolicy._from_certificate_policy_bundle(certificate_policy_bundle=bundle) + + @distributed_trace_async + async def update_certificate_properties( + self, + certificate_name: str, + version: Optional[str] = None, + *, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + **kwargs: Any, + ) -> KeyVaultCertificate: + """Change a certificate's properties. Requires certificates/update permission. + + :param str certificate_name: The name of the certificate in the given key vault. + :param str version: The version of the certificate. + + :keyword bool enabled: Whether the certificate is enabled for use. + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] + + :returns: The updated KeyVaultCertificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START update_certificate] + :end-before: [END update_certificate] + :language: python + :caption: Update a certificate's attributes + :dedent: 8 + """ + + if enabled is not None: + attributes = self._models.CertificateAttributes(enabled=enabled) + else: + attributes = None + + parameters = self._models.CertificateUpdateParameters( + certificate_attributes=attributes, tags=tags + ) + + bundle = await self._client.update_certificate( + certificate_name=certificate_name, + certificate_version=version or "", + parameters=parameters, + **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace_async + async def backup_certificate(self, certificate_name: str, **kwargs: Any) -> bytes: + """Back up a certificate in a protected form useable only by Azure Key Vault. + + Requires certificates/backup permission. This is intended to allow copying a certificate from one vault to + another. Both vaults must be owned by the same Azure subscription. Also, backup / restore cannot be performed + across geopolitical boundaries. For example, a backup from a vault in a USA region cannot be restored to a vault + in an EU region. + + :param str certificate_name: The name of the certificate. + + :return: The backup blob containing the backed up certificate. + :rtype: bytes + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START backup_certificate] + :end-before: [END backup_certificate] + :language: python + :caption: Get a certificate backup + :dedent: 8 + """ + backup_result = await self._client.backup_certificate( + certificate_name=certificate_name, **kwargs + ) + return backup_result.value + + @distributed_trace_async + async def restore_certificate_backup(self, backup: bytes, **kwargs: Any) -> KeyVaultCertificate: + """Restore a certificate backup to the vault. Requires certificates/restore permission. + + This restores all versions of the certificate, with its name, attributes, and access control policies. If the + certificate's name is already in use, restoring it will fail. Also, the target vault must be owned by the same + Microsoft Azure subscription as the source vault. + + :param bytes backup: The backup blob associated with a certificate bundle. + + :return: The restored KeyVaultCertificate + :rtype: ~azure.keyvault.certificates.KeyVaultCertificate + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START restore_certificate] + :end-before: [END restore_certificate] + :language: python + :caption: Restore a certificate backup + :dedent: 8 + """ + bundle = await self._client.restore_certificate( + parameters=self._models.CertificateRestoreParameters(certificate_bundle_backup=backup), + **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace + def list_deleted_certificates( + self, *, include_pending: Optional[bool] = None, **kwargs: Any + ) -> AsyncItemPaged[DeletedCertificate]: + """Lists the currently-recoverable deleted certificates. Possible only if vault is soft-delete enabled. + + Requires certificates/get/list permission. Retrieves the certificates in the current vault which are in a + deleted state and ready for recovery or purging. This operation includes deletion-specific information. + + :keyword bool include_pending: Specifies whether to include certificates which are not completely deleted. + Only available for API versions v7.0 and up. If not provided, Key Vault treats this as False. + :paramtype include_pending: bool or None + + :return: An iterator-like instance of DeletedCertificate + :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.DeletedCertificate] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START list_deleted_certificates] + :end-before: [END list_deleted_certificates] + :language: python + :caption: List all the deleted certificates + :dedent: 8 + """ + max_page_size = kwargs.pop("max_page_size", None) + + if self.api_version == "2016-10-01": + if include_pending is not None: + raise NotImplementedError( + "The 'include_pending' parameter to `list_deleted_certificates` " + "is only available for API versions v7.0 and up" + ) + else: + kwargs.update({"include_pending": include_pending}) + + return self._client.get_deleted_certificates( + maxresults=max_page_size, + cls=lambda objs: [DeletedCertificate._from_deleted_certificate_item(x) for x in objs], + **kwargs + ) + + @distributed_trace + def list_properties_of_certificates( + self, *, include_pending: Optional[bool] = None, **kwargs: Any + ) -> AsyncItemPaged[CertificateProperties]: + """List identifiers and properties of all certificates in the vault. + + Requires certificates/list permission. + + :keyword bool include_pending: Specifies whether to include certificates which are not completely provisioned. + Only available for API versions v7.0 and up. If not provided, Key Vault treats this as False. + :paramtype include_pending: bool or None + + :returns: An iterator-like instance of CertificateProperties + :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateProperties] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START list_properties_of_certificates] + :end-before: [END list_properties_of_certificates] + :language: python + :caption: List all certificates + :dedent: 8 + """ + max_page_size = kwargs.pop("max_page_size", None) + + if self.api_version == "2016-10-01": + if include_pending is not None: + raise NotImplementedError( + "The 'include_pending' parameter to `list_properties_of_certificates` " + "is only available for API versions v7.0 and up" + ) + else: + kwargs.update({"include_pending": include_pending}) + + return self._client.get_certificates( + maxresults=max_page_size, + cls=lambda objs: [CertificateProperties._from_certificate_item(x) for x in objs], + **kwargs + ) + + @distributed_trace + def list_properties_of_certificate_versions( + self, certificate_name: str, **kwargs: Any + ) -> AsyncItemPaged[CertificateProperties]: + """List the identifiers and properties of a certificate's versions. + + Requires certificates/list permission. + + :param str certificate_name: The name of the certificate. + + :returns: An iterator-like instance of CertificateProperties + :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateProperties] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START list_properties_of_certificate_versions] + :end-before: [END list_properties_of_certificate_versions] + :language: python + :caption: List all versions of a certificate + :dedent: 8 + """ + max_page_size = kwargs.pop("max_page_size", None) + return self._client.get_certificate_versions( + certificate_name=certificate_name, + maxresults=max_page_size, + cls=lambda objs: [CertificateProperties._from_certificate_item(x) for x in objs], + **kwargs + ) + + @distributed_trace_async + async def set_contacts(self, contacts: List[CertificateContact], **kwargs: Any) -> List[CertificateContact]: + """Sets the certificate contacts for the key vault. Requires certificates/managecontacts permission. + + :param contacts: The contact list for the vault certificates. + :type contacts: list[~azure.keyvault.certificates.CertificateContact] + + :returns: The created list of contacts + :rtype: list[~azure.keyvault.certificates.CertificateContact] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START set_contacts] + :end-before: [END set_contacts] + :language: python + :caption: Create contacts + :dedent: 8 + """ + new_contacts = await self._client.set_certificate_contacts( + contacts=self._models.Contacts(contact_list=[c._to_certificate_contacts_item() for c in contacts]), + **kwargs + ) + return [ + CertificateContact._from_certificate_contacts_item(contact_item=item) for item in new_contacts.contact_list + ] + + @distributed_trace_async + async def get_contacts(self, **kwargs: Any) -> List[CertificateContact]: + """Gets the certificate contacts for the key vault. Requires the certificates/managecontacts permission. + + :return: The certificate contacts for the key vault. + :rtype: list[azure.keyvault.certificates.CertificateContact] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START get_contacts] + :end-before: [END get_contacts] + :language: python + :caption: Get contacts + :dedent: 8 + """ + contacts = await self._client.get_certificate_contacts( **kwargs + ) + return [CertificateContact._from_certificate_contacts_item(contact_item=item) for item in contacts.contact_list] + + @distributed_trace_async + async def delete_contacts(self, **kwargs: Any) -> List[CertificateContact]: + """Deletes the certificate contacts for the key vault. Requires the certificates/managecontacts permission. + + :return: The deleted contacts for the key vault. + :rtype: list[~azure.keyvault.certificates.CertificateContact] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START delete_contacts] + :end-before: [END delete_contacts] + :language: python + :caption: Delete contacts + :dedent: 8 + """ + contacts = await self._client.delete_certificate_contacts( + **kwargs + ) + return [CertificateContact._from_certificate_contacts_item(contact_item=item) for item in contacts.contact_list] + + @distributed_trace_async + async def get_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: + """Gets the creation operation of a certificate. Requires the certificates/get permission. + + :param str certificate_name: The name of the certificate. + + :returns: The created CertificateOperation + :rtype: ~azure.keyvault.certificates.CertificateOperation + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the certificate doesn't exist; the latter for other errors + """ + + bundle = await self._client.get_certificate_operation( + certificate_name=certificate_name, **kwargs + ) + return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) + + @distributed_trace_async + async def delete_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: + """Deletes and stops the creation operation for a specific certificate. + + Requires the certificates/update permission. + + :param str certificate_name: The name of the certificate. + + :return: The deleted CertificateOperation + :rtype: ~azure.keyvault.certificates.CertificateOperation + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the operation doesn't exist; the latter for other errors + """ + bundle = await self._client.delete_certificate_operation( + certificate_name=certificate_name, **kwargs + ) + return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) + + @distributed_trace_async + async def cancel_certificate_operation(self, certificate_name: str, **kwargs: Any) -> CertificateOperation: + """Cancels an in-progress certificate operation. Requires the certificates/update permission. + + :param str certificate_name: The name of the certificate. + + :returns: The cancelled certificate operation + :rtype: ~azure.keyvault.certificates.CertificateOperation + + :raises ~azure.core.exceptions.HttpResponseError: + """ + bundle = await self._client.update_certificate_operation( + certificate_name=certificate_name, + certificate_operation=self._models.CertificateOperationUpdateParameter(cancellation_requested=True), + **kwargs + ) + return CertificateOperation._from_certificate_operation_bundle(certificate_operation_bundle=bundle) + + @distributed_trace_async + async def merge_certificate( + self, + certificate_name: str, + x509_certificates: List[bytes], + *, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + **kwargs: Any, + ) -> KeyVaultCertificate: + """Merges a certificate or a certificate chain with a key pair existing on the server. + + Requires the certificates/create permission. Performs the merging of a certificate or certificate chain with a + key pair currently available in the service. Make sure when creating the certificate to merge using + :func:`create_certificate` that you set its issuer to 'Unknown'. This way Key Vault knows that the + certificate will not be signed by an issuer known to it. + + :param str certificate_name: The name of the certificate + :param x509_certificates: The certificate or the certificate chain to merge. + :type x509_certificates: list[bytes] + + :keyword bool enabled: Whether the certificate is enabled for use. + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] + + :return: The merged certificate operation + :rtype: ~azure.keyvault.certificates.CertificateOperation + + :raises ~azure.core.exceptions.HttpResponseError: + """ + + if enabled is not None: + attributes = self._models.CertificateAttributes(enabled=enabled) + else: + attributes = None + + parameters = self._models.CertificateMergeParameters( + x509_certificates=x509_certificates, certificate_attributes=attributes, tags=tags + ) + + bundle = await self._client.merge_certificate( + certificate_name=certificate_name, + parameters=parameters, + **kwargs + ) + return KeyVaultCertificate._from_certificate_bundle(certificate_bundle=bundle) + + @distributed_trace_async + async def get_issuer(self, issuer_name: str, **kwargs: Any) -> CertificateIssuer: + """Gets the specified certificate issuer. Requires certificates/manageissuers/getissuers permission. + + :param str issuer_name: The name of the issuer. + + :return: The specified certificate issuer. + :rtype: ~azure.keyvault.certificates.CertificateIssuer + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the issuer doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START get_issuer] + :end-before: [END get_issuer] + :language: python + :caption: Get an issuer + :dedent: 8 + """ + issuer_bundle = await self._client.get_certificate_issuer( + issuer_name=issuer_name, **kwargs + ) + return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) + + @distributed_trace_async + async def create_issuer( + self, + issuer_name: str, + provider: str, + *, + enabled: Optional[bool] = None, + account_id: Optional[str] = None, + password: Optional[str] = None, + organization_id: Optional[str] = None, + admin_contacts: Optional[List[AdministratorContact]] = None, + **kwargs: Any, + ) -> CertificateIssuer: + """Sets the specified certificate issuer. Requires certificates/setissuers permission. + + :param str issuer_name: The name of the issuer. + :param str provider: The issuer provider. + + :keyword bool enabled: Whether the issuer is enabled for use. + :keyword str account_id: The user name/account name/account id. + :keyword str password: The password/secret/account key. + :keyword str organization_id: Id of the organization + :keyword admin_contacts: Contact details of the organization administrators of the + certificate issuer. + :paramtype admin_contacts: list[~azure.keyvault.certificates.AdministratorContact] + + :returns: The created CertificateIssuer + :rtype: ~azure.keyvault.certificates.CertificateIssuer + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START create_issuer] + :end-before: [END create_issuer] + :language: python + :caption: Create an issuer + :dedent: 8 + """ + + if account_id or password: + issuer_credentials = self._models.IssuerCredentials(account_id=account_id, password=password) + else: + issuer_credentials = None + if admin_contacts: + admin_details: Optional[List[Any]] = [ + self._models.AdministratorDetails( + first_name=contact.first_name, + last_name=contact.last_name, + email_address=contact.email, + phone=contact.phone, + ) + for contact in admin_contacts + ] + else: + admin_details = None + if organization_id or admin_details: + organization_details = self._models.OrganizationDetails(id=organization_id, admin_details=admin_details) + else: + organization_details = None + if enabled is not None: + issuer_attributes = self._models.IssuerAttributes(enabled=enabled) + else: + issuer_attributes = None + + parameters = self._models.CertificateIssuerSetParameters( + provider=provider, + credentials=issuer_credentials, + organization_details=organization_details, + attributes=issuer_attributes, + ) + + issuer_bundle = await self._client.set_certificate_issuer( + issuer_name=issuer_name, parameter=parameters, **kwargs + ) + return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) + + @distributed_trace_async + async def update_issuer( + self, + issuer_name: str, + *, + enabled: Optional[bool] = None, + provider: Optional[str] = None, + account_id: Optional[str] = None, + password: Optional[str] = None, + organization_id: Optional[str] = None, + admin_contacts: Optional[List[AdministratorContact]] = None, + **kwargs: Any, + ) -> CertificateIssuer: + """Updates the specified certificate issuer. Requires certificates/setissuers permission. + + :param str issuer_name: The name of the issuer. + + :keyword bool enabled: Whether the issuer is enabled for use. + :keyword str provider: The issuer provider + :keyword str account_id: The user name/account name/account id. + :keyword str password: The password/secret/account key. + :keyword str organization_id: Id of the organization + :keyword admin_contacts: Contact details of the organization administrators of + the certificate issuer + :paramtype admin_contacts: list[~azure.keyvault.certificates.AdministratorContact] + + :return: The updated issuer + :rtype: ~azure.keyvault.certificates.CertificateIssuer + + :raises ~azure.core.exceptions.HttpResponseError: + """ + + if account_id or password: + issuer_credentials = self._models.IssuerCredentials(account_id=account_id, password=password) + else: + issuer_credentials = None + if admin_contacts: + admin_details: Optional[List[Any]] = list( + self._models.AdministratorDetails( + first_name=contact.first_name, + last_name=contact.last_name, + email_address=contact.email, + phone=contact.phone, + ) + for contact in admin_contacts + ) + else: + admin_details = None + if organization_id or admin_details: + organization_details = self._models.OrganizationDetails(id=organization_id, admin_details=admin_details) + else: + organization_details = None + if enabled is not None: + issuer_attributes = self._models.IssuerAttributes(enabled=enabled) + else: + issuer_attributes = None + + parameters = self._models.CertificateIssuerUpdateParameters( + provider=provider, + credentials=issuer_credentials, + organization_details=organization_details, + attributes=issuer_attributes, + ) + + issuer_bundle = await self._client.update_certificate_issuer( + issuer_name=issuer_name, parameter=parameters, **kwargs + ) + return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) + + @distributed_trace_async + async def delete_issuer(self, issuer_name: str, **kwargs: Any) -> CertificateIssuer: + """Deletes the specified certificate issuer. + + Requires certificates/manageissuers/deleteissuers permission. + + :param str issuer_name: The name of the issuer. + + :return: CertificateIssuer + :rtype: ~azure.keyvault.certificates.CertificateIssuer + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START delete_issuer] + :end-before: [END delete_issuer] + :language: python + :caption: Delete an issuer + :dedent: 8 + """ + issuer_bundle = await self._client.delete_certificate_issuer( + issuer_name=issuer_name, **kwargs + ) + return CertificateIssuer._from_issuer_bundle(issuer_bundle=issuer_bundle) + + @distributed_trace + def list_properties_of_issuers(self, **kwargs: Any) -> AsyncItemPaged[IssuerProperties]: + """Lists properties of the certificate issuers for the key vault. + + Requires the certificates/manageissuers/getissuers permission. + + :return: An iterator-like instance of Issuers + :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.certificates.CertificateIssuer] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_examples_certificates_async.py + :start-after: [START list_properties_of_issuers] + :end-before: [END list_properties_of_issuers] + :language: python + :caption: List issuers of a vault + :dedent: 8 + """ + max_page_size = kwargs.pop("max_page_size", None) + return self._client.get_certificate_issuers( + maxresults=max_page_size, + cls=lambda objs: [IssuerProperties._from_issuer_item(x) for x in objs], + **kwargs + ) + + async def __aenter__(self) -> "CertificateClient": + await self._client.__aenter__() + return self diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_polling_async.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_polling_async.py new file mode 100644 index 000000000000..dbfe2f4bd0e3 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_polling_async.py @@ -0,0 +1,57 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import Any, Callable, cast, Optional, Union + +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import AsyncHttpTransport +from azure.core.polling import AsyncPollingMethod + +from .._models import KeyVaultCertificate, CertificateOperation + + +class CreateCertificatePollerAsync(AsyncPollingMethod): + def __init__( + self, pipeline_response: PipelineResponse, get_certificate_command: Callable, interval: int = 5 + ) -> None: + self._pipeline_response = pipeline_response + self._command: Optional[Callable] = None + self._resource: Optional[Union[CertificateOperation, KeyVaultCertificate]] = None + self._pending_certificate_op: Optional[CertificateOperation] = None + self._get_certificate_command = get_certificate_command + self._polling_interval = interval + + async def _update_status(self) -> None: + self._pending_certificate_op = await self._command() if self._command else None + + def initialize(self, client: Any, initial_response: Any, _: Callable) -> None: + self._command = client + self._pending_certificate_op = initial_response + + async def run(self) -> None: + while not self.finished(): + await self._update_status() + if not self.finished(): + # We should always ask the client's transport to sleep, instead of sleeping directly + transport: AsyncHttpTransport = cast(AsyncHttpTransport, self._pipeline_response.context.transport) + await transport.sleep(self._polling_interval) + operation = self._pending_certificate_op + if operation and operation.status and operation.status.lower() == "completed": + self._resource = await self._get_certificate_command() + else: + self._resource = self._pending_certificate_op + + def finished(self) -> bool: + operation = self._pending_certificate_op + if operation and operation.issuer_name and operation.issuer_name.lower() == "unknown": + # Because we've finished, self._resource won't be set by the run method; set it here so we don't return None + self._resource = self._pending_certificate_op + return True + return self._pending_certificate_op.status.lower() != "inprogress" # type: ignore + + def resource(self) -> Union[KeyVaultCertificate, CertificateOperation]: + return self._resource # type: ignore + + def status(self) -> str: + return self._pending_certificate_op.status.lower() # type: ignore diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/py.typed b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/py.typed new file mode 100644 index 000000000000..e5aff4f83af8 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations.py b/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations.py index 5f9eff53f9ef..c6e83e410383 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations_async.py index e85cd9d5967c..8ed40e3b8210 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/backup_restore_operations_async.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/contacts.py b/sdk/keyvault/azure-keyvault-certificates/samples/contacts.py index 2cb45aa52862..0c518e230ecf 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/contacts.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/contacts.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/contacts_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/contacts_async.py index fc05c0f3ebe4..994b7db23004 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/contacts_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/contacts_async.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/hello_world.py b/sdk/keyvault/azure-keyvault-certificates/samples/hello_world.py index baeaabff1e42..b2df23b1a491 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/hello_world.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/hello_world.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/hello_world_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/hello_world_async.py index 826fbba557d3..d712b51e5b82 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/hello_world_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/hello_world_async.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate.py b/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate.py index a886ba8f099f..3819b769b131 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate_async.py index de8b3619d08b..150d071788d1 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/import_certificate_async.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/issuers.py b/sdk/keyvault/azure-keyvault-certificates/samples/issuers.py index e7babf5e0658..57936e4f917f 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/issuers.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/issuers.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/issuers_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/issuers_async.py index 393c1ffbcaef..9aed2e1af2fe 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/issuers_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/issuers_async.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/list_operations.py b/sdk/keyvault/azure-keyvault-certificates/samples/list_operations.py index 21ac303178e6..8345036baedc 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/list_operations.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/list_operations.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/list_operations_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/list_operations_async.py index 21d92d58ed7b..54d21eb47342 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/list_operations_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/list_operations_async.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate.py b/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate.py index f38aaf1c87d4..a062bf199127 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate_async.py index 5d3dd7b3521a..6e9065b66684 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/parse_certificate_async.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations.py b/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations.py index 6750e7695282..ae17348dbdd7 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations_async.py b/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations_async.py index edd06b98d307..9c8ceb14ccb4 100644 --- a/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/samples/recover_purge_operations_async.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/sdk_packaging.toml b/sdk/keyvault/azure-keyvault-certificates/sdk_packaging.toml new file mode 100644 index 000000000000..e7687fdae93b --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/sdk_packaging.toml @@ -0,0 +1,2 @@ +[packaging] +auto_update = false \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault-certificates/setup.py b/sdk/keyvault/azure-keyvault-certificates/setup.py deleted file mode 100644 index 7343c53938ad..000000000000 --- a/sdk/keyvault/azure-keyvault-certificates/setup.py +++ /dev/null @@ -1,71 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- - - -import os -import re -from setuptools import setup, find_packages - -PACKAGE_NAME = "azure-keyvault-certificates" -PACKAGE_PPRINT_NAME = "Key Vault Certificates" -PACKAGE_NAMESPACE = "azure.keyvault.certificates._generated" - -# a.b.c => a/b/c -package_folder_path = PACKAGE_NAMESPACE.replace(".", "/") - -# Version extraction inspired from 'requests' -with open(os.path.join(package_folder_path, "_version.py"), "r") as fd: - version = re.search(r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) - -if not version: - raise RuntimeError("Cannot find version information") - - -setup( - name=PACKAGE_NAME, - version=version, - description="Microsoft Corporation {} Client Library for Python".format(PACKAGE_PPRINT_NAME), - long_description=open("README.md", "r").read(), - long_description_content_type="text/markdown", - license="MIT License", - author="Microsoft Corporation", - author_email="azpysdkhelp@microsoft.com", - url="https://github.com/Azure/azure-sdk-for-python/tree/main/sdk", - keywords="azure, azure sdk", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "License :: OSI Approved :: MIT License", - ], - zip_safe=False, - packages=find_packages( - exclude=[ - "tests", - # Exclude packages that will be covered by PEP420 or nspkg - "azure", - "azure.keyvault", - "azure.keyvault.certificates", - ] - ), - include_package_data=True, - package_data={ - "azure.keyvault.certificates._generated": ["py.typed"], - }, - install_requires=[ - "isodate>=0.6.1", - "azure-core>=1.37.0", - "typing-extensions>=4.6.0", - ], - python_requires=">=3.10", -) diff --git a/sdk/keyvault/azure-keyvault-certificates/tests/certs.py b/sdk/keyvault/azure-keyvault-certificates/tests/certs.py index c94546338d86..b369590befcc 100644 --- a/sdk/keyvault/azure-keyvault-certificates/tests/certs.py +++ b/sdk/keyvault/azure-keyvault-certificates/tests/certs.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client.py b/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client.py index d5101c2909f2..64328d69d73f 100644 --- a/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client.py +++ b/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -43,6 +42,7 @@ from _test_case import get_decorator, CertificatesClientPreparer from certs import CERT_CONTENT_PASSWORD_ENCODED, CERT_CONTENT_NOT_PASSWORD_ENCODED + all_api_versions = get_decorator() only_latest = get_decorator(api_versions=[DEFAULT_VERSION]) logging_enabled = get_decorator(logging_enable=True) @@ -817,7 +817,9 @@ def test_unknown_issuer_response(self, client, **kwargs): content_type=CertificateContentType.pkcs12, validity_in_months=24, ) - create_certificate_poller = client.begin_create_certificate(certificate_name=cert_name, policy=cert_policy) + create_certificate_poller = client.begin_create_certificate( + certificate_name=cert_name, policy=cert_policy + ) result = create_certificate_poller.result() # The operation should indicate that certificate creation is in progress and requires a merge to complete assert isinstance(result, CertificateOperation) diff --git a/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client_async.py b/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client_async.py index 7f011297d035..6333bdeab1b9 100644 --- a/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client_async.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -40,6 +39,7 @@ from _test_case import get_decorator from certs import CERT_CONTENT_PASSWORD_ENCODED, CERT_CONTENT_NOT_PASSWORD_ENCODED + all_api_versions = get_decorator() only_latest = get_decorator(api_versions=[DEFAULT_VERSION]) logging_enabled = get_decorator(logging_enable=True) diff --git a/sdk/keyvault/azure-keyvault-certificates/tests/test_examples_certificates_async.py b/sdk/keyvault/azure-keyvault-certificates/tests/test_examples_certificates_async.py index 1a16fdd92723..821e3091c614 100644 --- a/sdk/keyvault/azure-keyvault-certificates/tests/test_examples_certificates_async.py +++ b/sdk/keyvault/azure-keyvault-certificates/tests/test_examples_certificates_async.py @@ -12,6 +12,7 @@ from _test_case import get_decorator from devtools_testutils.aio import recorded_by_proxy_async + all_api_versions = get_decorator(is_async=True) exclude_2016_10_01 = get_decorator(is_async=True, api_versions=[v for v in ApiVersion if v != ApiVersion.V2016_10_01]) diff --git a/sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py b/sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py new file mode 100644 index 000000000000..48953817b8f1 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py @@ -0,0 +1,100 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +"""Unit tests for PlatformManaged property on CertificatePolicy (2026-03-01-preview).""" + +import pytest +from azure.keyvault.certificates._generated import models + + +class TestPlatformManagedModel: + """Tests for the generated PlatformManaged model.""" + + def test_platform_managed_with_certificate_usage(self): + pm = models.PlatformManaged(certificate_usage="tls-server") + assert pm.certificate_usage == "tls-server" + assert pm.metadata is None + + def test_platform_managed_with_metadata(self): + metadata = {"key1": "value1", "key2": 42} + pm = models.PlatformManaged(certificate_usage="tls-client", metadata=metadata) + assert pm.certificate_usage == "tls-client" + assert pm.metadata == {"key1": "value1", "key2": 42} + + def test_platform_managed_empty_metadata(self): + pm = models.PlatformManaged(certificate_usage="tls-server", metadata={}) + assert pm.certificate_usage == "tls-server" + assert pm.metadata == {} + + def test_platform_managed_serialization_key(self): + pm = models.PlatformManaged(certificate_usage="tls-server") + serialized = dict(pm) + assert "certificateUsage" in serialized + assert serialized["certificateUsage"] == "tls-server" + + +class TestCertificatePolicyPlatformManaged: + """Tests for platform_managed on the generated CertificatePolicy model.""" + + def test_policy_with_platform_managed(self): + pm = models.PlatformManaged(certificate_usage="tls-server") + policy = models.CertificatePolicy( + issuer_parameters=models.IssuerParameters(name="Self"), + platform_managed=pm, + ) + assert policy.platform_managed is not None + assert policy.platform_managed.certificate_usage == "tls-server" + + def test_policy_platform_managed_none_by_default(self): + policy = models.CertificatePolicy( + issuer_parameters=models.IssuerParameters(name="Self"), + ) + assert policy.platform_managed is None + + def test_policy_with_platform_managed_and_metadata(self): + pm = models.PlatformManaged( + certificate_usage="tls-client", + metadata={"issuer": "internal-ca", "rotation_days": 90}, + ) + policy = models.CertificatePolicy( + issuer_parameters=models.IssuerParameters(name="Self"), + x509_certificate_properties=models.X509CertificateProperties(subject="CN=Test"), + platform_managed=pm, + ) + assert policy.platform_managed.certificate_usage == "tls-client" + assert policy.platform_managed.metadata["issuer"] == "internal-ca" + assert policy.platform_managed.metadata["rotation_days"] == 90 + + def test_policy_serialization_includes_platform_managed(self): + pm = models.PlatformManaged(certificate_usage="tls-server", metadata={"env": "prod"}) + policy = models.CertificatePolicy( + issuer_parameters=models.IssuerParameters(name="Self"), + platform_managed=pm, + ) + serialized = dict(policy) + assert "platformManaged" in serialized + assert serialized["platformManaged"]["certificateUsage"] == "tls-server" + assert serialized["platformManaged"]["metadata"] == {"env": "prod"} + + def test_policy_deserialization_with_platform_managed(self): + raw = { + "issuer": {"name": "Self"}, + "platformManaged": { + "certificateUsage": "tls-server", + "metadata": {"key": "value"}, + }, + } + policy = models.CertificatePolicy(raw) + assert policy.platform_managed is not None + assert policy.platform_managed.certificate_usage == "tls-server" + assert policy.platform_managed.metadata == {"key": "value"} + + def test_policy_deserialization_without_platform_managed(self): + raw = { + "issuer": {"name": "Self"}, + "x509_props": {"subject": "CN=Test"}, + } + policy = models.CertificatePolicy(raw) + assert policy.platform_managed is None From 0d7300aa81f10eeeeab79ab994ca788c061dba2f Mon Sep 17 00:00:00 2001 From: Nicholas Noboa Date: Tue, 19 May 2026 14:08:14 -0700 Subject: [PATCH 3/4] Clean up tests --- .../azure-keyvault-certificates/tests/test_platform_managed.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py b/sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py index 48953817b8f1..130cab7d886f 100644 --- a/sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py +++ b/sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py @@ -60,7 +60,6 @@ def test_policy_with_platform_managed_and_metadata(self): ) policy = models.CertificatePolicy( issuer_parameters=models.IssuerParameters(name="Self"), - x509_certificate_properties=models.X509CertificateProperties(subject="CN=Test"), platform_managed=pm, ) assert policy.platform_managed.certificate_usage == "tls-client" @@ -94,7 +93,6 @@ def test_policy_deserialization_with_platform_managed(self): def test_policy_deserialization_without_platform_managed(self): raw = { "issuer": {"name": "Self"}, - "x509_props": {"subject": "CN=Test"}, } policy = models.CertificatePolicy(raw) assert policy.platform_managed is None From 9386f658a55bdb6db7f05d013d0d89cd661365b1 Mon Sep 17 00:00:00 2001 From: Rohit Singhal Date: Thu, 4 Jun 2026 18:51:02 +0100 Subject: [PATCH 4/4] Expose PlatformManaged from public azure.keyvault.certificates namespace Addresses SDK team feedback: the APIView sample for the platformManaged feature was importing PlatformManaged from the private _generated.models namespace. Add a thin public wrapper and re-export it from the package __init__.py so customers (and samples) can use: from azure.keyvault.certificates import PlatformManaged, CertificatePolicy policy = CertificatePolicy( issuer_name=WellKnownIssuerNames.self, platform_managed=PlatformManaged('tls-server', metadata={...}), ) The wrapper is wired through CertificatePolicy._to_certificate_policy_bundle and CertificatePolicy._from_certificate_policy_bundle so the value survives round-tripping. 22 unit tests cover the new surface. --- .../azure-keyvault-certificates/CHANGELOG.md | 2 +- .../azure/keyvault/certificates/__init__.py | 6 +- .../azure/keyvault/certificates/_models.py | 66 +++++++++++++++ .../tests/test_platform_managed.py | 82 +++++++++++++++++++ 4 files changed, 153 insertions(+), 3 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md b/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md index 7dacdd9c9545..988c44c37716 100644 --- a/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md @@ -5,7 +5,7 @@ ### Features Added - Added support for service API version `2026-03-01-preview` -- Added `PlatformManaged` property to `CertificatePolicy` +- Added `PlatformManaged` model and `platform_managed` keyword argument / property on `CertificatePolicy`, exposed from the public `azure.keyvault.certificates` namespace ### Breaking Changes diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py index 3e8da4143494..c6082213307b 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/__init__.py @@ -23,7 +23,8 @@ IssuerProperties, LifetimeAction, KeyVaultCertificate, - KeyVaultCertificateIdentifier + KeyVaultCertificateIdentifier, + PlatformManaged ) from ._shared.client_base import ApiVersion @@ -49,7 +50,8 @@ "CertificateContentType", "WellKnownIssuerNames", "CertificateIssuer", - "IssuerProperties" + "IssuerProperties", + "PlatformManaged" ] from ._version import VERSION diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py index 6c3d3eba60f9..86710949f2e2 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_models.py @@ -689,6 +689,53 @@ def preserve_order(self) -> Optional[bool]: return self._preserve_order +class PlatformManaged(object): + """Configuration that enables the platform to manage the certificate on behalf of the user. + + This feature is currently intended for internal use only. + + :param certificate_usage: The intended usage of the certificate. + :type certificate_usage: str + :keyword metadata: JSON-formatted platform managed metadata. The schema is intentionally + undefined as this feature is currently intended for internal use only. + :paramtype metadata: dict[str, Any] or None + """ + + def __init__(self, certificate_usage: str, *, metadata: Optional[Dict[str, Any]] = None) -> None: + self._certificate_usage = certificate_usage + self._metadata = metadata + + def __repr__(self) -> str: + return f""[:1024] + + @property + def certificate_usage(self) -> str: + """The intended usage of the certificate. + + :returns: The intended usage of the certificate. + :rtype: str + """ + return self._certificate_usage + + @property + def metadata(self) -> Optional[Dict[str, Any]]: + """JSON-formatted platform managed metadata. + + :returns: Platform managed metadata, if any. + :rtype: dict[str, Any] or None + """ + return self._metadata + + def _to_generated(self) -> models.PlatformManaged: + return models.PlatformManaged(certificate_usage=self._certificate_usage, metadata=self._metadata) + + @classmethod + def _from_generated(cls, generated: Optional[models.PlatformManaged]) -> Optional["PlatformManaged"]: + if generated is None: + return None + return cls(certificate_usage=generated.certificate_usage, metadata=generated.metadata) + + class CertificatePolicy(object): """Management policy for a certificate. @@ -742,6 +789,9 @@ class CertificatePolicy(object): :keyword certificate_transparency: Indicates if the certificates generated under this policy should be published to certificate transparency logs. :paramtype certificate_transparency: bool or None + :keyword platform_managed: Configuration that enables the platform to manage the certificate on behalf + of the user. This feature is currently intended for internal use only. + :paramtype platform_managed: ~azure.keyvault.certificates.PlatformManaged or None """ # pylint:disable=too-many-instance-attributes @@ -770,6 +820,7 @@ def __init__( self._san_user_principal_names = kwargs.pop("san_user_principal_names", None) or None self._san_ip_addresses = kwargs.pop("san_ip_addresses", None) or None self._san_uris = kwargs.pop("san_uris", None) or None + self._platform_managed = kwargs.pop("platform_managed", None) @classmethod def get_default(cls) -> "CertificatePolicy": @@ -874,6 +925,7 @@ def _to_certificate_policy_bundle(self) -> models.CertificatePolicy: lifetime_actions=lifetime_actions, issuer_parameters=issuer_parameters, attributes=attributes, + platform_managed=self._platform_managed._to_generated() if self._platform_managed else None, ) return policy_bundle @@ -955,6 +1007,9 @@ def _from_certificate_policy_bundle( validity_in_months=( x509_certificate_properties.validity_in_months if x509_certificate_properties else None ), + platform_managed=PlatformManaged._from_generated( + getattr(certificate_policy_bundle, "platform_managed", None) + ), ) @property @@ -1083,6 +1138,17 @@ def san_uris(self) -> Optional[List[str]]: """ return self._san_uris + @property + def platform_managed(self) -> Optional[PlatformManaged]: + """Configuration that enables the platform to manage the certificate on behalf of the user. + + This feature is currently intended for internal use only. + + :returns: The platform managed configuration, if set. + :rtype: ~azure.keyvault.certificates.PlatformManaged or None + """ + return self._platform_managed + @property def validity_in_months(self) -> Optional[int]: """The duration that the certificate is valid for in months. diff --git a/sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py b/sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py index 130cab7d886f..a3be8abe7de0 100644 --- a/sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py +++ b/sdk/keyvault/azure-keyvault-certificates/tests/test_platform_managed.py @@ -6,6 +6,7 @@ """Unit tests for PlatformManaged property on CertificatePolicy (2026-03-01-preview).""" import pytest +from azure.keyvault.certificates import CertificatePolicy, PlatformManaged, WellKnownIssuerNames from azure.keyvault.certificates._generated import models @@ -96,3 +97,84 @@ def test_policy_deserialization_without_platform_managed(self): } policy = models.CertificatePolicy(raw) assert policy.platform_managed is None + + +class TestPublicPlatformManaged: + """Tests for the public PlatformManaged wrapper and CertificatePolicy.platform_managed.""" + + def test_wrapper_required_field_only(self): + pm = PlatformManaged("tls-server") + assert pm.certificate_usage == "tls-server" + assert pm.metadata is None + + def test_wrapper_with_metadata(self): + pm = PlatformManaged("tls-client", metadata={"env": "prod"}) + assert pm.certificate_usage == "tls-client" + assert pm.metadata == {"env": "prod"} + + def test_wrapper_repr(self): + pm = PlatformManaged("tls-server") + assert "tls-server" in repr(pm) + + def test_metadata_kwarg_only(self): + # metadata must be keyword-only + with pytest.raises(TypeError): + PlatformManaged("tls-server", {"env": "prod"}) # type: ignore + + def test_policy_default_platform_managed_is_none(self): + policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self) + assert policy.platform_managed is None + + def test_policy_with_platform_managed(self): + pm = PlatformManaged("tls-server", metadata={"k": "v"}) + policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self, platform_managed=pm) + assert policy.platform_managed is pm + assert policy.platform_managed.certificate_usage == "tls-server" + assert policy.platform_managed.metadata == {"k": "v"} + + def test_to_bundle_serializes_platform_managed(self): + pm = PlatformManaged("tls-server", metadata={"env": "prod"}) + policy = CertificatePolicy(issuer_name="Self", platform_managed=pm) + bundle = policy._to_certificate_policy_bundle() + assert bundle.platform_managed is not None + assert bundle.platform_managed.certificate_usage == "tls-server" + assert bundle.platform_managed.metadata == {"env": "prod"} + + def test_to_bundle_without_platform_managed(self): + policy = CertificatePolicy(issuer_name="Self") + bundle = policy._to_certificate_policy_bundle() + assert bundle.platform_managed is None + + def test_from_bundle_reads_platform_managed(self): + gen_pm = models.PlatformManaged(certificate_usage="tls-client", metadata={"a": 1}) + bundle = models.CertificatePolicy( + issuer_parameters=models.IssuerParameters(name="Self"), + platform_managed=gen_pm, + ) + policy = CertificatePolicy._from_certificate_policy_bundle(bundle) + assert policy.platform_managed is not None + assert policy.platform_managed.certificate_usage == "tls-client" + assert policy.platform_managed.metadata == {"a": 1} + + def test_from_bundle_without_platform_managed(self): + bundle = models.CertificatePolicy( + issuer_parameters=models.IssuerParameters(name="Self"), + ) + policy = CertificatePolicy._from_certificate_policy_bundle(bundle) + assert policy.platform_managed is None + + def test_round_trip_preserves_platform_managed(self): + pm = PlatformManaged("tls-server", metadata={"env": "prod", "owner": "team-x"}) + original = CertificatePolicy(issuer_name="Self", platform_managed=pm) + bundle = original._to_certificate_policy_bundle() + restored = CertificatePolicy._from_certificate_policy_bundle(bundle) + assert restored.platform_managed is not None + assert restored.platform_managed.certificate_usage == "tls-server" + assert restored.platform_managed.metadata == {"env": "prod", "owner": "team-x"} + + def test_platform_managed_exported_from_public_namespace(self): + # Guards against future regression where the wrapper is only available under _models. + import azure.keyvault.certificates as kv_certs + + assert kv_certs.PlatformManaged is PlatformManaged + assert "PlatformManaged" in kv_certs.__all__