diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index 034766972361..fbfbab4dae26 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md +++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md @@ -7,6 +7,7 @@ ### Breaking Changes ### Bugs Fixed +- Fixed an issue where a disabled certificate in Azure Key Vault caused keystore initialization to fail with an HTTP 403 error. Disabled certificates are now skipped when loading aliases and a warning is logged for each skipped certificate. [#49730](https://github.com/Azure/azure-sdk-for-java/pull/49730) ### Other Changes diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java index 6fadbad95419..411f6b1db75f 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java @@ -288,6 +288,14 @@ public List getAliases() { for (CertificateItem certificateItem : certificateListResult.getValue()) { String id = certificateItem.getId(); String alias = getCertificateNameFromCertificateItemId(id); + + // Skip certificates that are explicitly disabled in Key Vault. Attempting to load a disabled + // certificate's key/secret later would fail with an HTTP 403 and break keystore initialization. + if (!certificateItem.isEnabled()) { + LOGGER.log(WARNING, "Skipping disabled certificate with alias: {0}", alias); + continue; + } + result.add(alias); } } else { diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/model/CertificateItem.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/model/CertificateItem.java index 9878b5829999..8bd04d35a566 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/model/CertificateItem.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/model/CertificateItem.java @@ -19,6 +19,11 @@ public class CertificateItem implements JsonSerializable { */ private String id; + /** + * Stores the management attributes of the certificate. + */ + private CertificateItemAttributes attributes; + /** * Get the id. * @@ -37,10 +42,42 @@ public void setId(String id) { this.id = id; } + /** + * Get the management attributes of the certificate. + * + * @return the management attributes of the certificate, or {@code null} if not specified. + */ + public CertificateItemAttributes getAttributes() { + return attributes; + } + + /** + * Set the management attributes of the certificate. + * + * @param attributes the management attributes of the certificate. + */ + public void setAttributes(CertificateItemAttributes attributes) { + this.attributes = attributes; + } + + /** + * Indicates whether the certificate is enabled. + *

+ * A certificate is considered enabled unless its attributes explicitly mark it as disabled (i.e. + * {@code attributes.enabled == false}). When the attributes or the {@code enabled} flag are absent, the certificate + * is treated as enabled for backward compatibility. + * + * @return {@code false} only when the certificate is explicitly disabled, otherwise {@code true}. + */ + public boolean isEnabled() { + return attributes == null || !Boolean.FALSE.equals(attributes.isEnabled()); + } + @Override public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { jsonWriter.writeStartObject(); jsonWriter.writeStringField("id", this.id); + jsonWriter.writeJsonField("attributes", this.attributes); return jsonWriter.writeEndObject(); } @@ -66,6 +103,8 @@ public static CertificateItem fromJson(JsonReader jsonReader) throws IOException if ("id".equals(fieldName)) { deserializedCertificateItem.id = reader.getString(); + } else if ("attributes".equals(fieldName)) { + deserializedCertificateItem.attributes = CertificateItemAttributes.fromJson(reader); } else { reader.skipChildren(); } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/model/CertificateItemAttributes.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/model/CertificateItemAttributes.java new file mode 100644 index 000000000000..691887af4a5c --- /dev/null +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/model/CertificateItemAttributes.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.security.keyvault.jca.implementation.model; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * The management attributes of a {@link CertificateItem}, as returned by the Azure Key Vault "list certificates" REST + * API. + */ +public class CertificateItemAttributes implements JsonSerializable { + /** + * Stores whether the certificate is enabled. + */ + private Boolean enabled; + + /** + * Get whether the certificate is enabled. + * + * @return whether the certificate is enabled, or {@code null} if the attribute was not specified. + */ + public Boolean isEnabled() { + return enabled; + } + + /** + * Set whether the certificate is enabled. + * + * @param enabled whether the certificate is enabled. + */ + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + jsonWriter.writeBooleanField("enabled", this.enabled); + + return jsonWriter.writeEndObject(); + } + + /** + * Reads an instance of {@link CertificateItemAttributes} from the {@link JsonReader}. + * + * @param jsonReader The {@link JsonReader} being read. + * + * @return An instance of {@link CertificateItemAttributes} if the {@link JsonReader} was pointing to an instance of + * it, or {@code null} if it was pointing to JSON {@code null}. + * + * @throws IOException If an error occurs while reading the {@link CertificateItemAttributes}. + */ + public static CertificateItemAttributes fromJson(JsonReader jsonReader) throws IOException { + return jsonReader.readObject(reader -> { + CertificateItemAttributes deserializedCertificateItemAttributes = new CertificateItemAttributes(); + + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + + reader.nextToken(); + + if ("enabled".equals(fieldName)) { + deserializedCertificateItemAttributes.enabled = reader.getNullable(JsonReader::getBoolean); + } else { + reader.skipChildren(); + } + } + + return deserializedCertificateItemAttributes; + }); + } +} diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java index 22f80900efa6..19f7631bf4e6 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java @@ -6,6 +6,7 @@ import com.azure.security.keyvault.jca.PropertyConvertorUtils; import com.azure.security.keyvault.jca.implementation.model.AccessToken; import com.azure.security.keyvault.jca.implementation.model.CertificateItem; +import com.azure.security.keyvault.jca.implementation.model.CertificateItemAttributes; import com.azure.security.keyvault.jca.implementation.model.CertificateListResult; import com.azure.security.keyvault.jca.implementation.utils.AccessTokenUtil; import com.azure.security.keyvault.jca.implementation.utils.HttpUtil; @@ -19,6 +20,7 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyMap; @@ -109,6 +111,97 @@ public void testGetAliasWithCertificateInfoWith2Pages() { } } + @Test + public void testGetAliasFiltersOutDisabledCertificate() { + try (MockedStatic utilities = Mockito.mockStatic(HttpUtil.class)) { + utilities.when(() -> HttpUtil.validateUri(anyString(), anyString())).thenCallRealMethod(); + utilities.when(() -> HttpUtil.addTrailingSlashIfRequired(anyString())).thenCallRealMethod(); + + // Enabled certificate. + CertificateItemAttributes enabledAttributes = new CertificateItemAttributes(); + enabledAttributes.setEnabled(true); + CertificateItem enabledCertificate = new CertificateItem(); + enabledCertificate.setId("certificates/client-cert-active"); + enabledCertificate.setAttributes(enabledAttributes); + + // Disabled certificate. This one previously caused an HTTP 403 while initializing the keystore. + CertificateItemAttributes disabledAttributes = new CertificateItemAttributes(); + disabledAttributes.setEnabled(false); + CertificateItem disabledCertificate = new CertificateItem(); + disabledCertificate.setId("certificates/client-cert-unused"); + disabledCertificate.setAttributes(disabledAttributes); + + CertificateListResult certificateListResult = new CertificateListResult(); + certificateListResult.setValue(Arrays.asList(enabledCertificate, disabledCertificate)); + + String certificateListResultString = JsonConverterUtil.toJson(certificateListResult); + utilities.when(() -> HttpUtil.get(notNull(), anyMap())).thenReturn(certificateListResultString); + + KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null); + List result = keyVaultClient.getAliases(); + + assertEquals(1, result.size()); + assertTrue(result.contains("client-cert-active")); + assertFalse(result.contains("client-cert-unused")); + } + } + + @Test + public void testGetAliasKeepsEnabledAndAttributelessCertificates() { + try (MockedStatic utilities = Mockito.mockStatic(HttpUtil.class)) { + utilities.when(() -> HttpUtil.validateUri(anyString(), anyString())).thenCallRealMethod(); + utilities.when(() -> HttpUtil.addTrailingSlashIfRequired(anyString())).thenCallRealMethod(); + + // Certificate explicitly enabled. + CertificateItemAttributes enabledAttributes = new CertificateItemAttributes(); + enabledAttributes.setEnabled(true); + CertificateItem enabledCertificate = new CertificateItem(); + enabledCertificate.setId("certificates/enabledCertificate"); + enabledCertificate.setAttributes(enabledAttributes); + + // Certificate without attributes, which must be treated as enabled for backward compatibility. + CertificateItem attributelessCertificate = new CertificateItem(); + attributelessCertificate.setId("certificates/attributelessCertificate"); + + CertificateListResult certificateListResult = new CertificateListResult(); + certificateListResult.setValue(Arrays.asList(enabledCertificate, attributelessCertificate)); + + String certificateListResultString = JsonConverterUtil.toJson(certificateListResult); + utilities.when(() -> HttpUtil.get(notNull(), anyMap())).thenReturn(certificateListResultString); + + KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null); + List result = keyVaultClient.getAliases(); + + assertEquals(2, result.size()); + assertTrue(result.containsAll(Arrays.asList("enabledCertificate", "attributelessCertificate"))); + } + } + + @Test + public void testGetAliasFiltersDisabledCertificateFromRawResponse() { + try (MockedStatic utilities = Mockito.mockStatic(HttpUtil.class)) { + utilities.when(() -> HttpUtil.validateUri(anyString(), anyString())).thenCallRealMethod(); + utilities.when(() -> HttpUtil.addTrailingSlashIfRequired(anyString())).thenCallRealMethod(); + + // A response that mirrors the shape returned by the Azure Key Vault "list certificates" REST API, with one + // enabled and one disabled certificate. + String rawResponse = "{\"value\":[" + + "{\"id\":\"https://fake.vault.azure.net/certificates/client-cert-active\"," + + "\"attributes\":{\"enabled\":true,\"nbf\":1783324860,\"exp\":1814861460}}," + + "{\"id\":\"https://fake.vault.azure.net/certificates/client-cert-unused\"," + + "\"attributes\":{\"enabled\":false,\"nbf\":1783324860,\"exp\":1814861460}}]," + "\"nextLink\":null}"; + + utilities.when(() -> HttpUtil.get(notNull(), anyMap())).thenReturn(rawResponse); + + KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null); + List result = keyVaultClient.getAliases(); + + assertEquals(1, result.size()); + assertTrue(result.contains("client-cert-active")); + assertFalse(result.contains("client-cert-unused")); + } + } + @Test public void testCacheToken() { try (MockedStatic tokenUtilMockedStatic = Mockito.mockStatic(AccessTokenUtil.class);