Skip to content

Commit db3c5f8

Browse files
Fix begin_create_certificate validator to accept san_ip_addresses and san_uris
- Extended the policy validator in both the sync and async CertificateClient to also accept policies that specify only san_ip_addresses or san_uris, which are valid SAN types but were previously treated as missing subject information - Added test_create_certificate_with_san_ip_and_uris (sync + async) with live recordings to verify IP-only and URI-only SAN policies succeed end-to-end - Extended test_policy_expected_errors_for_create_cert to assert that IP-only and URI-only policies no longer raise ValueError - Fixed _test_case.py and _async_test_case.py to avoid setting AZURE_CLIENT_ID/ AZURE_CLIENT_SECRET to empty strings, which newer azure-identity treats as an attempt to use ClientSecretCredential and raises ValueError
1 parent 314c9ed commit db3c5f8

File tree

8 files changed

+162
-9
lines changed

8 files changed

+162
-9
lines changed

sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Release History
22

3+
## 4.11.1 (Unreleased)
4+
5+
### Bugs Fixed
6+
7+
- Fixed `CertificateClient.begin_create_certificate` (and its async counterpart) incorrectly raising
8+
`ValueError` when a `CertificatePolicy` was created with only `san_ip_addresses` or `san_uris` and no
9+
`subject`, `san_dns_names`, `san_emails`, or `san_user_principal_names`. IP addresses and URIs are
10+
valid subject alternative name types and are now recognized by the client's policy validator.
11+
312
## 4.11.0 (2026-03-27)
413

514
### Features Added

sdk/keyvault/azure-keyvault-certificates/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/keyvault/azure-keyvault-certificates",
5-
"Tag": "python/keyvault/azure-keyvault-certificates_6bfbc7f623"
5+
"Tag": "python/keyvault/azure-keyvault-certificates_a5c5c34814"
66
}

sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,14 @@ def begin_create_certificate(
100100
:caption: Create a certificate
101101
:dedent: 8
102102
"""
103-
if not (policy.san_emails or policy.san_user_principal_names or policy.san_dns_names or policy.subject):
103+
if not (
104+
policy.san_emails
105+
or policy.san_user_principal_names
106+
or policy.san_dns_names
107+
or policy.san_ip_addresses
108+
or policy.san_uris
109+
or policy.subject
110+
):
104111
raise ValueError(NO_SAN_OR_SUBJECT)
105112

106113
polling_interval = kwargs.pop("_polling_interval", None)

sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/aio/_client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@ async def create_certificate(
9696
:caption: Create a certificate
9797
:dedent: 8
9898
"""
99-
if not (policy.san_emails or policy.san_user_principal_names or policy.san_dns_names or policy.subject):
99+
if not (
100+
policy.san_emails
101+
or policy.san_user_principal_names
102+
or policy.san_dns_names
103+
or policy.san_ip_addresses
104+
or policy.san_uris
105+
or policy.subject
106+
):
100107
raise ValueError(NO_SAN_OR_SUBJECT)
101108

102109
polling_interval = kwargs.pop("_polling_interval", None)

sdk/keyvault/azure-keyvault-certificates/tests/_async_test_case.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,20 @@ def __init__(self, **kwargs) -> None:
2020
self.is_logging_enabled = kwargs.pop("logging_enable", True)
2121

2222
if is_live():
23-
os.environ["AZURE_TENANT_ID"] = os.getenv("KEYVAULT_TENANT_ID", "") # empty in pipelines
24-
os.environ["AZURE_CLIENT_ID"] = os.getenv("KEYVAULT_CLIENT_ID", "") # empty in pipelines
25-
os.environ["AZURE_CLIENT_SECRET"] = os.getenv("KEYVAULT_CLIENT_SECRET", "") # empty for user-based auth
23+
# Only set AZURE_* vars if the KEYVAULT_* counterpart is non-empty.
24+
# Setting them to empty strings causes EnvironmentCredential to attempt (and fail)
25+
# ClientSecretCredential construction. Removing them lets DefaultAzureCredential
26+
# fall through to AzureCliCredential for developer/interactive auth.
27+
for keyvault_var, azure_var in (
28+
("KEYVAULT_TENANT_ID", "AZURE_TENANT_ID"),
29+
("KEYVAULT_CLIENT_ID", "AZURE_CLIENT_ID"),
30+
("KEYVAULT_CLIENT_SECRET", "AZURE_CLIENT_SECRET"),
31+
):
32+
value = os.getenv(keyvault_var, "")
33+
if value:
34+
os.environ[azure_var] = value
35+
else:
36+
os.environ.pop(azure_var, None)
2637

2738
def __call__(self, fn):
2839
async def _preparer(test_class, api_version, **kwargs):

sdk/keyvault/azure-keyvault-certificates/tests/_test_case.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,20 @@ def __init__(self, **kwargs) -> None:
2525

2626
if is_live():
2727
self.azure_keyvault_url = os.environ["AZURE_KEYVAULT_URL"]
28-
os.environ["AZURE_TENANT_ID"] = os.getenv("KEYVAULT_TENANT_ID", "") # empty in pipelines
29-
os.environ["AZURE_CLIENT_ID"] = os.getenv("KEYVAULT_CLIENT_ID", "") # empty in pipelines
30-
os.environ["AZURE_CLIENT_SECRET"] = os.getenv("KEYVAULT_CLIENT_SECRET", "") # empty for user-based auth
28+
# Only set AZURE_* vars if the KEYVAULT_* counterpart is non-empty.
29+
# Setting them to empty strings causes EnvironmentCredential to attempt (and fail)
30+
# ClientSecretCredential construction. Removing them lets DefaultAzureCredential
31+
# fall through to AzureCliCredential for developer/interactive auth.
32+
for keyvault_var, azure_var in (
33+
("KEYVAULT_TENANT_ID", "AZURE_TENANT_ID"),
34+
("KEYVAULT_CLIENT_ID", "AZURE_CLIENT_ID"),
35+
("KEYVAULT_CLIENT_SECRET", "AZURE_CLIENT_SECRET"),
36+
):
37+
value = os.getenv(keyvault_var, "")
38+
if value:
39+
os.environ[azure_var] = value
40+
else:
41+
os.environ.pop(azure_var, None)
3142

3243
def __call__(self, fn):
3344
def _preparer(test_class, api_version, **kwargs):

sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,41 @@ def run(*_, **__):
763763
with pytest.raises(ResourceExistsError):
764764
client.begin_create_certificate("...", CertificatePolicy.get_default())
765765

766+
@pytest.mark.parametrize("api_version", only_latest)
767+
@CertificatesClientPreparer()
768+
@recorded_by_proxy
769+
def test_create_certificate_with_san_ip_and_uris(self, client, **kwargs):
770+
"""Verify certificates with only san_ip_addresses or san_uris (no subject/dns) can be created."""
771+
# Certificate with only IP addresses in SANs
772+
ip_cert_name = self.get_resource_name("sanIpCert")
773+
ip_policy = CertificatePolicy(
774+
issuer_name=WellKnownIssuerNames.self,
775+
san_ip_addresses=["10.0.0.1", "192.168.1.1"],
776+
content_type=CertificateContentType.pkcs12,
777+
)
778+
ip_cert = client.begin_create_certificate(certificate_name=ip_cert_name, policy=ip_policy).result()
779+
assert ip_cert.name == ip_cert_name
780+
returned_ip_policy = client.get_certificate_policy(ip_cert_name)
781+
assert set(returned_ip_policy.san_ip_addresses) == {"10.0.0.1", "192.168.1.1"}
782+
assert not returned_ip_policy.san_dns_names
783+
assert not returned_ip_policy.san_uris
784+
client.begin_delete_certificate(ip_cert_name).wait()
785+
786+
# Certificate with only URIs in SANs
787+
uri_cert_name = self.get_resource_name("sanUriCert")
788+
uri_policy = CertificatePolicy(
789+
issuer_name=WellKnownIssuerNames.self,
790+
san_uris=["https://service.example.com/api"],
791+
content_type=CertificateContentType.pkcs12,
792+
)
793+
uri_cert = client.begin_create_certificate(certificate_name=uri_cert_name, policy=uri_policy).result()
794+
assert uri_cert.name == uri_cert_name
795+
returned_uri_policy = client.get_certificate_policy(uri_cert_name)
796+
assert returned_uri_policy.san_uris
797+
assert not returned_uri_policy.san_dns_names
798+
assert not returned_uri_policy.san_ip_addresses
799+
client.begin_delete_certificate(uri_cert_name).wait()
800+
766801
@pytest.mark.parametrize("api_version", only_latest)
767802
@CertificatesClientPreparer()
768803
@recorded_by_proxy
@@ -804,6 +839,24 @@ def test_policy_expected_errors_for_create_cert():
804839
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self)
805840
client.begin_create_certificate("...", policy=policy)
806841

842+
# san_ip_addresses alone should be accepted (no ValueError)
843+
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self, san_ip_addresses=["10.0.0.1"])
844+
try:
845+
client.begin_create_certificate("...", policy=policy)
846+
except ValueError:
847+
pytest.fail("begin_create_certificate should not raise ValueError for san_ip_addresses-only policy")
848+
except Exception:
849+
pass # Expected: network/auth error since we are using a fake client
850+
851+
# san_uris alone should be accepted (no ValueError)
852+
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self, san_uris=["https://example.com"])
853+
try:
854+
client.begin_create_certificate("...", policy=policy)
855+
except ValueError:
856+
pytest.fail("begin_create_certificate should not raise ValueError for san_uris-only policy")
857+
except Exception:
858+
pass # Expected: network/auth error since we are using a fake client
859+
807860

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

sdk/keyvault/azure-keyvault-certificates/tests/test_certificates_client_async.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,43 @@ async def run(*_, **__):
787787
await client.create_certificate("...", CertificatePolicy.get_default())
788788
await client.close()
789789

790+
@pytest.mark.asyncio
791+
@pytest.mark.parametrize("api_version", only_latest)
792+
@AsyncCertificatesClientPreparer()
793+
@recorded_by_proxy_async
794+
async def test_create_certificate_with_san_ip_and_uris(self, client, **kwargs):
795+
"""Verify certificates with only san_ip_addresses or san_uris (no subject/dns) can be created."""
796+
# Certificate with only IP addresses in SANs
797+
ip_cert_name = self.get_resource_name("sanIpCert")
798+
ip_policy = CertificatePolicy(
799+
issuer_name=WellKnownIssuerNames.self,
800+
san_ip_addresses=["10.0.0.1", "192.168.1.1"],
801+
content_type=CertificateContentType.pkcs12,
802+
)
803+
ip_cert = await client.create_certificate(certificate_name=ip_cert_name, policy=ip_policy)
804+
assert ip_cert.name == ip_cert_name
805+
returned_ip_policy = await client.get_certificate_policy(ip_cert_name)
806+
assert set(returned_ip_policy.san_ip_addresses) == {"10.0.0.1", "192.168.1.1"}
807+
assert not returned_ip_policy.san_dns_names
808+
assert not returned_ip_policy.san_uris
809+
await client.delete_certificate(ip_cert_name)
810+
811+
# Certificate with only URIs in SANs
812+
uri_cert_name = self.get_resource_name("sanUriCert")
813+
uri_policy = CertificatePolicy(
814+
issuer_name=WellKnownIssuerNames.self,
815+
san_uris=["https://service.example.com/api"],
816+
content_type=CertificateContentType.pkcs12,
817+
)
818+
uri_cert = await client.create_certificate(certificate_name=uri_cert_name, policy=uri_policy)
819+
assert uri_cert.name == uri_cert_name
820+
returned_uri_policy = await client.get_certificate_policy(uri_cert_name)
821+
assert returned_uri_policy.san_uris
822+
assert not returned_uri_policy.san_dns_names
823+
assert not returned_uri_policy.san_ip_addresses
824+
await client.delete_certificate(uri_cert_name)
825+
await client.close()
826+
790827
@pytest.mark.asyncio
791828
@pytest.mark.parametrize("api_version", only_latest)
792829
@AsyncCertificatesClientPreparer()
@@ -827,6 +864,24 @@ async def test_policy_expected_errors_for_create_cert():
827864
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self)
828865
await client.create_certificate("...", policy=policy)
829866

867+
# san_ip_addresses alone should be accepted (no ValueError)
868+
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self, san_ip_addresses=["10.0.0.1"])
869+
try:
870+
await client.create_certificate("...", policy=policy)
871+
except ValueError:
872+
pytest.fail("create_certificate should not raise ValueError for san_ip_addresses-only policy")
873+
except Exception:
874+
pass # Expected: network/auth error since we are using a fake client
875+
876+
# san_uris alone should be accepted (no ValueError)
877+
policy = CertificatePolicy(issuer_name=WellKnownIssuerNames.self, san_uris=["https://example.com"])
878+
try:
879+
await client.create_certificate("...", policy=policy)
880+
except ValueError:
881+
pytest.fail("create_certificate should not raise ValueError for san_uris-only policy")
882+
except Exception:
883+
pass # Expected: network/auth error since we are using a fake client
884+
830885

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

0 commit comments

Comments
 (0)