Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
db3c5f8
Fix begin_create_certificate validator to accept san_ip_addresses and…
singhalrohit4u Apr 17, 2026
f5217b6
Update 4.11.1 release date to 2026-04-20
singhalrohit4u Apr 18, 2026
0b20476
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 18, 2026
20d57ee
Bump azure-keyvault-certificates version to 4.11.1
singhalrohit4u Apr 19, 2026
25a65b6
Merge branch 'fix/keyvault-certificates-san-ip-uri-validator' of http…
singhalrohit4u Apr 19, 2026
8ab52ed
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 19, 2026
98e0dd4
Replace broad except Exception with specific exceptions in test_polic…
singhalrohit4u Apr 21, 2026
f670722
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 21, 2026
c6b1f2e
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 23, 2026
a58ec98
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 23, 2026
a952228
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 23, 2026
12fd96d
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 24, 2026
d880fc8
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 24, 2026
45c41bf
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 24, 2026
bce9f82
fix to handle loop for python3.14
kashifkhan Apr 24, 2026
c0fc08e
bump pyopenssl for pypy py313
kashifkhan Apr 24, 2026
856dddf
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 24, 2026
153502d
revert pyopenssl bump
kashifkhan Apr 24, 2026
f15b610
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
laiapat Apr 27, 2026
f5b0a02
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 28, 2026
2a5da27
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 28, 2026
d69bc8b
Update release date for version 4.11.1
rohitsinghal4u Apr 28, 2026
3acafb4
Merge branch 'main' into fix/keyvault-certificates-san-ip-uri-validator
rohitsinghal4u Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Release History

## 4.11.1 (2026-04-29)

### Bugs Fixed

- Fixed `CertificateClient.begin_create_certificate` (and its async counterpart) incorrectly raising
`ValueError` when a `CertificatePolicy` was created with only `san_ip_addresses` or `san_uris` and no
`subject`, `san_dns_names`, `san_emails`, or `san_user_principal_names`. IP addresses and URIs are
valid subject alternative name types and are now recognized by the client's policy validator.

## 4.11.0 (2026-03-27)

### Features Added
Expand Down
2 changes: 1 addition & 1 deletion sdk/keyvault/azure-keyvault-certificates/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "python",
"TagPrefix": "python/keyvault/azure-keyvault-certificates",
"Tag": "python/keyvault/azure-keyvault-certificates_6bfbc7f623"
"Tag": "python/keyvault/azure-keyvault-certificates_a5c5c34814"
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,14 @@ def begin_create_certificate(
:caption: Create a certificate
:dedent: 8
"""
if not (policy.san_emails or policy.san_user_principal_names or policy.san_dns_names or policy.subject):
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
# Licensed under the MIT License.
# ------------------------------------

VERSION = "4.11.0"
VERSION = "4.11.1"
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,14 @@ async def create_certificate(
:caption: Create a certificate
:dedent: 8
"""
if not (policy.san_emails or policy.san_user_principal_names or policy.san_dns_names or policy.subject):
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,20 @@ def __init__(self, **kwargs) -> None:
self.is_logging_enabled = kwargs.pop("logging_enable", True)

if is_live():
os.environ["AZURE_TENANT_ID"] = os.getenv("KEYVAULT_TENANT_ID", "") # empty in pipelines
os.environ["AZURE_CLIENT_ID"] = os.getenv("KEYVAULT_CLIENT_ID", "") # empty in pipelines
os.environ["AZURE_CLIENT_SECRET"] = os.getenv("KEYVAULT_CLIENT_SECRET", "") # empty for user-based auth
# Only set AZURE_* vars if the KEYVAULT_* counterpart is non-empty.
# Setting them to empty strings causes EnvironmentCredential to attempt (and fail)
# ClientSecretCredential construction. Removing them lets DefaultAzureCredential
# fall through to AzureCliCredential for developer/interactive auth.
for keyvault_var, azure_var in (
("KEYVAULT_TENANT_ID", "AZURE_TENANT_ID"),
("KEYVAULT_CLIENT_ID", "AZURE_CLIENT_ID"),
("KEYVAULT_CLIENT_SECRET", "AZURE_CLIENT_SECRET"),
):
value = os.getenv(keyvault_var, "")
if value:
os.environ[azure_var] = value
else:
os.environ.pop(azure_var, None)

def __call__(self, fn):
async def _preparer(test_class, api_version, **kwargs):
Expand Down
17 changes: 14 additions & 3 deletions sdk/keyvault/azure-keyvault-certificates/tests/_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,20 @@ def __init__(self, **kwargs) -> None:

if is_live():
self.azure_keyvault_url = os.environ["AZURE_KEYVAULT_URL"]
os.environ["AZURE_TENANT_ID"] = os.getenv("KEYVAULT_TENANT_ID", "") # empty in pipelines
os.environ["AZURE_CLIENT_ID"] = os.getenv("KEYVAULT_CLIENT_ID", "") # empty in pipelines
os.environ["AZURE_CLIENT_SECRET"] = os.getenv("KEYVAULT_CLIENT_SECRET", "") # empty for user-based auth
# Only set AZURE_* vars if the KEYVAULT_* counterpart is non-empty.
# Setting them to empty strings causes EnvironmentCredential to attempt (and fail)
# ClientSecretCredential construction. Removing them lets DefaultAzureCredential
# fall through to AzureCliCredential for developer/interactive auth.
for keyvault_var, azure_var in (
("KEYVAULT_TENANT_ID", "AZURE_TENANT_ID"),
("KEYVAULT_CLIENT_ID", "AZURE_CLIENT_ID"),
("KEYVAULT_CLIENT_SECRET", "AZURE_CLIENT_SECRET"),
):
value = os.getenv(keyvault_var, "")
if value:
os.environ[azure_var] = value
else:
os.environ.pop(azure_var, None)

def __call__(self, fn):
def _preparer(test_class, api_version, **kwargs):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ def immediate_return(_):

@pytest.fixture(scope="session")
def event_loop(request):
loop = asyncio.get_event_loop()
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
yield loop
loop.close()
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import time
from unittest.mock import Mock, patch

from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError
from azure.core.exceptions import HttpResponseError, ResourceExistsError, ResourceNotFoundError, ServiceRequestError
from azure.core.pipeline.policies import SansIOHTTPPolicy
from devtools_testutils import recorded_by_proxy
from azure.keyvault.certificates import (
Expand Down Expand Up @@ -763,6 +763,41 @@ def run(*_, **__):
with pytest.raises(ResourceExistsError):
client.begin_create_certificate("...", CertificatePolicy.get_default())

@pytest.mark.parametrize("api_version", only_latest)
@CertificatesClientPreparer()
@recorded_by_proxy
def test_create_certificate_with_san_ip_and_uris(self, client, **kwargs):
"""Verify certificates with only san_ip_addresses or san_uris (no subject/dns) can be created."""
# Certificate with only IP addresses in SANs
ip_cert_name = self.get_resource_name("sanIpCert")
ip_policy = CertificatePolicy(
issuer_name=WellKnownIssuerNames.self,
san_ip_addresses=["10.0.0.1", "192.168.1.1"],
content_type=CertificateContentType.pkcs12,
)
ip_cert = client.begin_create_certificate(certificate_name=ip_cert_name, policy=ip_policy).result()
assert ip_cert.name == ip_cert_name
returned_ip_policy = client.get_certificate_policy(ip_cert_name)
assert set(returned_ip_policy.san_ip_addresses) == {"10.0.0.1", "192.168.1.1"}
assert not returned_ip_policy.san_dns_names
assert not returned_ip_policy.san_uris
client.begin_delete_certificate(ip_cert_name).wait()

# Certificate with only URIs in SANs
uri_cert_name = self.get_resource_name("sanUriCert")
uri_policy = CertificatePolicy(
issuer_name=WellKnownIssuerNames.self,
san_uris=["https://service.example.com/api"],
content_type=CertificateContentType.pkcs12,
)
uri_cert = client.begin_create_certificate(certificate_name=uri_cert_name, policy=uri_policy).result()
assert uri_cert.name == uri_cert_name
returned_uri_policy = client.get_certificate_policy(uri_cert_name)
assert returned_uri_policy.san_uris
assert not returned_uri_policy.san_dns_names
assert not returned_uri_policy.san_ip_addresses
client.begin_delete_certificate(uri_cert_name).wait()

@pytest.mark.parametrize("api_version", only_latest)
@CertificatesClientPreparer()
@recorded_by_proxy
Expand Down Expand Up @@ -804,6 +839,24 @@ def test_policy_expected_errors_for_create_cert():
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self)
client.begin_create_certificate("...", policy=policy)

# san_ip_addresses alone should be accepted (no ValueError)
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self, san_ip_addresses=["10.0.0.1"])
try:
client.begin_create_certificate("...", policy=policy)
except ValueError:
pytest.fail("begin_create_certificate should not raise ValueError for san_ip_addresses-only policy")
except (HttpResponseError, ServiceRequestError):
pass # Expected: network/auth error since we are using a fake client

# san_uris alone should be accepted (no ValueError)
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self, san_uris=["https://example.com"])
try:
client.begin_create_certificate("...", policy=policy)
except ValueError:
pytest.fail("begin_create_certificate should not raise ValueError for san_uris-only policy")
except (HttpResponseError, ServiceRequestError):
pass # Expected: network/auth error since we are using a fake client
Comment thread
rohitsinghal4u marked this conversation as resolved.


def test_service_headers_allowed_in_logs():
service_headers = {"x-ms-keyvault-network-info", "x-ms-keyvault-region", "x-ms-keyvault-service-version"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import json
from unittest.mock import Mock, patch

from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError
from azure.core.exceptions import HttpResponseError, ResourceExistsError, ResourceNotFoundError, ServiceRequestError
from azure.core.pipeline.policies import SansIOHTTPPolicy
from devtools_testutils import set_bodiless_matcher, set_custom_default_matcher
from devtools_testutils.aio import recorded_by_proxy_async
Expand Down Expand Up @@ -787,6 +787,43 @@ async def run(*_, **__):
await client.create_certificate("...", CertificatePolicy.get_default())
await client.close()

@pytest.mark.asyncio
@pytest.mark.parametrize("api_version", only_latest)
@AsyncCertificatesClientPreparer()
@recorded_by_proxy_async
async def test_create_certificate_with_san_ip_and_uris(self, client, **kwargs):
"""Verify certificates with only san_ip_addresses or san_uris (no subject/dns) can be created."""
# Certificate with only IP addresses in SANs
ip_cert_name = self.get_resource_name("sanIpCert")
ip_policy = CertificatePolicy(
issuer_name=WellKnownIssuerNames.self,
san_ip_addresses=["10.0.0.1", "192.168.1.1"],
content_type=CertificateContentType.pkcs12,
)
ip_cert = await client.create_certificate(certificate_name=ip_cert_name, policy=ip_policy)
assert ip_cert.name == ip_cert_name
returned_ip_policy = await client.get_certificate_policy(ip_cert_name)
assert set(returned_ip_policy.san_ip_addresses) == {"10.0.0.1", "192.168.1.1"}
assert not returned_ip_policy.san_dns_names
assert not returned_ip_policy.san_uris
await client.delete_certificate(ip_cert_name)

# Certificate with only URIs in SANs
uri_cert_name = self.get_resource_name("sanUriCert")
uri_policy = CertificatePolicy(
issuer_name=WellKnownIssuerNames.self,
san_uris=["https://service.example.com/api"],
content_type=CertificateContentType.pkcs12,
)
uri_cert = await client.create_certificate(certificate_name=uri_cert_name, policy=uri_policy)
assert uri_cert.name == uri_cert_name
returned_uri_policy = await client.get_certificate_policy(uri_cert_name)
assert returned_uri_policy.san_uris
assert not returned_uri_policy.san_dns_names
assert not returned_uri_policy.san_ip_addresses
await client.delete_certificate(uri_cert_name)
await client.close()

@pytest.mark.asyncio
@pytest.mark.parametrize("api_version", only_latest)
@AsyncCertificatesClientPreparer()
Expand Down Expand Up @@ -827,6 +864,24 @@ async def test_policy_expected_errors_for_create_cert():
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self)
await client.create_certificate("...", policy=policy)

# san_ip_addresses alone should be accepted (no ValueError)
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self, san_ip_addresses=["10.0.0.1"])
try:
await client.create_certificate("...", policy=policy)
except ValueError:
pytest.fail("create_certificate should not raise ValueError for san_ip_addresses-only policy")
except (HttpResponseError, ServiceRequestError):
pass # Expected: network/auth error since we are using a fake client

# san_uris alone should be accepted (no ValueError)
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self, san_uris=["https://example.com"])
try:
await client.create_certificate("...", policy=policy)
except ValueError:
pytest.fail("create_certificate should not raise ValueError for san_uris-only policy")
except (HttpResponseError, ServiceRequestError):
pass # Expected: network/auth error since we are using a fake client
Comment thread
rohitsinghal4u marked this conversation as resolved.


def test_service_headers_allowed_in_logs():
service_headers = {"x-ms-keyvault-network-info", "x-ms-keyvault-region", "x-ms-keyvault-service-version"}
Expand Down
Loading