diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index 034766972361..66ee2ed5782c 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md +++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md @@ -3,6 +3,8 @@ ## 2.12.0-beta.1 (Unreleased) ### Features Added +- Added lazy loading for Key Vault certificate details in the JCA keystore. Certificate details are now loaded by alias when requested, avoiding unnecessary reads for unconfigured certificates. ([#49774](https://github.com/Azure/azure-sdk-for-java/pull/49774)) +- Added support for `azure.keyvault.jca.certificate-alias-filter-patterns` to filter Key Vault certificate aliases with include/exclude regex patterns. Include patterns are configured directly and exclude patterns are prefixed with `!`. If the property is not configured, alias filtering is disabled and all discovered Key Vault aliases remain eligible for lazy loading. ([#39487](https://github.com/Azure/azure-sdk-for-java/issues/39487)) ### Breaking Changes diff --git a/sdk/keyvault/azure-security-keyvault-jca/README.md b/sdk/keyvault/azure-security-keyvault-jca/README.md index f31a7ca5e90a..fdcfcb0e528a 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -141,6 +141,7 @@ The JCA library supports configuring the following options: * `azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate`: Indicates whether to refresh certificates when have untrusted certificate. * `azure.keyvault.jca.certificates-refresh-interval`: The refresh interval time. * `azure.keyvault.jca.certificates-refresh-interval-in-ms`: The refresh interval time. +* `azure.keyvault.jca.certificate-alias-filter-patterns`: Comma-separated Key Vault alias regex filters. Use include regexes directly and exclude regexes with a `!` prefix. Regex patterns use full-alias matching (`Pattern.matcher(alias).matches()`). An alias is loaded only if it matches at least one include regex (or if no include regex is configured) and matches no exclude regex. Example: `^prod-.*,!^prod-deprecated$,!.*-old$`. If this property is not configured, all discovered Key Vault aliases are eligible for lazy loading. * `azure.keyvault.disable-challenge-resource-verification`: Indicates whether to disable verification that the authentication challenge resource matches the Key Vault or Managed HSM domain. You can configure these properties using: diff --git a/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml b/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml index 1796f495a42b..790fdff01ec1 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml +++ b/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml @@ -13,6 +13,7 @@ + diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java index a87b1ef8ef78..1e8cf8dbcbbf 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java @@ -29,7 +29,9 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.logging.Logger; +import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.logging.Level.FINE; @@ -56,6 +58,9 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { */ private static final Logger LOGGER = Logger.getLogger(KeyVaultKeyStore.class.getName()); + static final String CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY + = "azure.keyvault.jca.certificate-alias-filter-patterns"; + /** * Stores the Jre key store certificates. */ @@ -146,8 +151,9 @@ public KeyVaultKeyStore() { customCertificates = SpecificPathCertificates.getSpecificPathCertificates(customPath); LOGGER.log(FINE, String.format("Loaded custom certificates: %s.", customCertificates.getAliases())); - keyVaultCertificates = new KeyVaultCertificates(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret, - managedIdentity, accessToken, disableChallengeResourceVerification); + keyVaultCertificates + = new KeyVaultCertificates(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, + accessToken, disableChallengeResourceVerification, getKeyVaultCertificateAliasFilterPatterns()); LOGGER.log(FINE, String.format("Loaded Key Vault certificates: %s.", keyVaultCertificates.getAliases())); classpathCertificates = new ClasspathCertificates(); @@ -168,6 +174,15 @@ Long getRefreshInterval() { .orElse(0L); } + Set getKeyVaultCertificateAliasFilterPatterns() { + return Optional.ofNullable(System.getProperty(CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY)) + .map(value -> Stream.of(value.split(",")) + .map(String::trim) + .filter(pattern -> !pattern.isEmpty()) + .collect(Collectors.toSet())) + .orElse(Collections.emptySet()); + } + /** * get key vault key store by system property * @@ -254,16 +269,22 @@ public boolean engineEntryInstanceOf(String alias, Class a.containsKey(alias)) - .findFirst() - .map(certificates -> certificates.get(alias)) - .orElse(null); + Certificate certificate = null; + for (AzureCertificates certificatesSource : allCertificates) { + if (certificatesSource instanceof KeyVaultCertificates) { + certificate = ((KeyVaultCertificates) certificatesSource).getCertificate(alias); + } else { + certificate = certificatesSource.getCertificates().get(alias); + } + + if (certificate != null) { + break; + } + } if (refreshCertificatesWhenHaveUnTrustCertificate && certificate == null) { keyVaultCertificates.refreshCertificates(); - certificate = keyVaultCertificates.getCertificates().get(alias); + certificate = keyVaultCertificates.getCertificate(alias); } return certificate; @@ -307,16 +328,22 @@ public String engineGetCertificateAlias(Certificate cert) { */ @Override public Certificate[] engineGetCertificateChain(String alias) { - Certificate[] certificates = allCertificates.stream() - .map(AzureCertificates::getCertificateChains) - .filter(Objects::nonNull) - .filter(a -> a.containsKey(alias)) - .findFirst() - .map(m -> m.get(alias)) - .orElse(null); + Certificate[] certificates = null; + for (AzureCertificates certificatesSource : allCertificates) { + if (certificatesSource instanceof KeyVaultCertificates) { + certificates = ((KeyVaultCertificates) certificatesSource).getCertificateChain(alias); + } else { + certificates = certificatesSource.getCertificateChains().get(alias); + } + + if (certificates != null) { + break; + } + } + if (refreshCertificatesWhenHaveUnTrustCertificate && certificates == null) { keyVaultCertificates.refreshCertificates(); - return keyVaultCertificates.getCertificateChains().get(alias); + return keyVaultCertificates.getCertificateChain(alias); } return certificates; } @@ -358,12 +385,20 @@ public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter */ @Override public Key engineGetKey(String alias, char[] password) { - return allCertificates.stream() - .map(AzureCertificates::getCertificateKeys) - .filter(a -> a.containsKey(alias)) - .findFirst() - .map(certificateKeys -> certificateKeys.get(alias)) - .orElse(null); + Key key = null; + for (AzureCertificates certificatesSource : allCertificates) { + if (certificatesSource instanceof KeyVaultCertificates) { + key = ((KeyVaultCertificates) certificatesSource).getCertificateKey(alias); + } else { + key = certificatesSource.getCertificateKeys().get(alias); + } + + if (key != null) { + break; + } + } + + return key; } /** diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificates.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificates.java index f4636abb2e0b..3313d8aa7e7f 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificates.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificates.java @@ -11,20 +11,45 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import static java.util.logging.Level.WARNING; /** * Store certificates loaded from KeyVault. */ public final class KeyVaultCertificates implements AzureCertificates { + private static final Logger LOGGER = Logger.getLogger(KeyVaultCertificates.class.getName()); + /** * Stores the list of aliases. */ private List aliases = new ArrayList<>(); + /** + * Stores aliases whose certificate has already been loaded. + */ + private final Set loadedCertificateAliases = new HashSet<>(); + + /** + * Stores aliases whose certificate chain has already been loaded. + */ + private final Set loadedCertificateChainAliases = new HashSet<>(); + + /** + * Stores aliases whose private key has already been loaded. + */ + private final Set loadedCertificateKeyAliases = new HashSet<>(); + /** * Stores the certificates by alias. */ @@ -49,17 +74,95 @@ public final class KeyVaultCertificates implements AzureCertificates { private final long refreshInterval; + private final Set certificateFilterPatterns; + + private final List includeAliasPatterns; + + private final List excludeAliasPatterns; + public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String tenantId, String clientId, String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification) { + this(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, + disableChallengeResourceVerification, Collections.emptySet()); + } + + public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String tenantId, String clientId, + String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification, + Set certificateFilterPatterns) { this.refreshInterval = refreshInterval; + this.certificateFilterPatterns + = new HashSet<>(Optional.ofNullable(certificateFilterPatterns).orElse(Collections.emptySet())); + this.includeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, false); + this.excludeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, true); updateKeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, disableChallengeResourceVerification); } public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient) { + this(refreshInterval, keyVaultClient, Collections.emptySet()); + } + + public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient, + Set certificateFilterPatterns) { this.refreshInterval = refreshInterval; + setKeyVaultClient(keyVaultClient); + this.certificateFilterPatterns + = new HashSet<>(Optional.ofNullable(certificateFilterPatterns).orElse(Collections.emptySet())); + this.includeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, false); + this.excludeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, true); + } + + private List getAliasPatterns(Set filterPatterns, boolean excludePatterns) { + return Optional.ofNullable(filterPatterns) + .orElse(Collections.emptySet()) + .stream() + .filter(Objects::nonNull) + .map(String::trim) + .filter(pattern -> !pattern.isEmpty()) + .filter(pattern -> pattern.startsWith("!") == excludePatterns) + .map(pattern -> { + if (excludePatterns) { + return pattern.substring(1); + } + if (pattern.startsWith("+")) { + return pattern.substring(1); + } + return pattern; + }) + .filter(pattern -> !pattern.isEmpty()) + .map(this::compileRegexPattern) + .collect(Collectors.toList()); + } + + private Pattern compileRegexPattern(String regexPattern) { + try { + return Pattern.compile(regexPattern); + } catch (PatternSyntaxException exception) { + throw new IllegalArgumentException("Invalid certificate filter regex pattern: " + regexPattern, exception); + } + } + + private boolean shouldIncludeAlias(String alias) { + if (alias == null) { + return false; + } + + boolean included = includeAliasPatterns.isEmpty() + || includeAliasPatterns.stream().anyMatch(pattern -> pattern.matcher(alias).matches()); + if (!included) { + return false; + } + + return excludeAliasPatterns.stream().noneMatch(pattern -> pattern.matcher(alias).matches()); + } + + private synchronized KeyVaultClient getKeyVaultClient() { + return keyVaultClient; + } + + private synchronized void setKeyVaultClient(KeyVaultClient keyVaultClient) { this.keyVaultClient = keyVaultClient; } @@ -74,19 +177,32 @@ public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient) * @param accessToken Access token. * @param disableChallengeResourceVerification Indicates if the challenge resource verification should be disabled. */ - public void updateKeyVaultClient(String keyVaultUri, String tenantId, String clientId, String clientSecret, - String managedIdentity, String accessToken, boolean disableChallengeResourceVerification) { + public synchronized void updateKeyVaultClient(String keyVaultUri, String tenantId, String clientId, + String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification) { if (keyVaultUri != null) { - keyVaultClient = new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, - accessToken, disableChallengeResourceVerification); + setKeyVaultClient(new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, + accessToken, disableChallengeResourceVerification)); } else { - keyVaultClient = null; + setKeyVaultClient(null); } + + clearCachedState(); + } + + private synchronized void clearCachedState() { + aliases = new ArrayList<>(); + loadedCertificateAliases.clear(); + loadedCertificateChainAliases.clear(); + loadedCertificateKeyAliases.clear(); + certificateKeys.clear(); + certificates.clear(); + certificateChains.clear(); + lastRefreshTime = null; } - boolean certificatesNeedRefresh() { - if (keyVaultClient == null) { + synchronized boolean certificatesNeedRefresh() { + if (getKeyVaultClient() == null) { return false; } if (lastRefreshTime == null) { @@ -140,6 +256,45 @@ public Map getCertificateKeys() { return certificateKeys; } + /** + * Get key by alias. + * + * @param alias The alias. + * @return The key, or {@code null}. + */ + public Key getCertificateKey(String alias) { + loadCertificateKeyIfNeeded(alias); + synchronized (this) { + return certificateKeys.get(alias); + } + } + + /** + * Get certificate by alias. + * + * @param alias The alias. + * @return The certificate, or {@code null}. + */ + public Certificate getCertificate(String alias) { + loadCertificateIfNeeded(alias); + synchronized (this) { + return certificates.get(alias); + } + } + + /** + * Get certificate chain by alias. + * + * @param alias The alias. + * @return The certificate chain, or {@code null}. + */ + public Certificate[] getCertificateChain(String alias) { + loadCertificateChainIfNeeded(alias); + synchronized (this) { + return certificateChains.get(alias); + } + } + private void refreshCertificatesIfNeeded() { if (certificatesNeedRefresh()) { // Avoid acquiring the lock as much as possible. synchronized (this) { @@ -151,31 +306,133 @@ private void refreshCertificatesIfNeeded() { } /** - * Refresh certificates. Including certificates, aliases, certificate keys, certificate chains. + * Refresh aliases and invalidate cached certificate details. */ public synchronized void refreshCertificates() { - // When refreshing certificates, the update of the 3 variables should be an atomic operation. - aliases = keyVaultClient.getAliases(); + KeyVaultClient currentKeyVaultClient = getKeyVaultClient(); + if (currentKeyVaultClient == null) { + clearCachedState(); + return; + } + + // Discover aliases from Key Vault and apply include/exclude regex filters. + aliases = Optional.ofNullable(currentKeyVaultClient.getAliases()) + .orElse(Collections.emptyList()) + .stream() + .filter(this::shouldIncludeAlias) + .sorted() + .collect(Collectors.toCollection(ArrayList::new)); + + loadedCertificateAliases.clear(); + loadedCertificateChainAliases.clear(); + loadedCertificateKeyAliases.clear(); certificateKeys.clear(); certificates.clear(); certificateChains.clear(); - Optional.ofNullable(aliases).orElse(Collections.emptyList()).forEach(alias -> { - Key key = keyVaultClient.getKey(alias, null); - if (!Objects.isNull(key)) { - certificateKeys.put(alias, key); + lastRefreshTime = new Date(); + } + + private void loadCertificateIfNeeded(String alias) { + refreshCertificatesIfNeeded(); + KeyVaultClient currentKeyVaultClient = getKeyVaultClient(); + + if (alias == null || currentKeyVaultClient == null) { + return; + } + + synchronized (this) { + if (loadedCertificateAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; } - Certificate certificate = keyVaultClient.getCertificate(alias); - if (!Objects.isNull(certificate)) { - certificates.put(alias, certificate); + } + + try { + Certificate loadedCertificate = currentKeyVaultClient.getCertificate(alias); + synchronized (this) { + if (currentKeyVaultClient != keyVaultClient + || loadedCertificateAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; + } + + if (loadedCertificate != null) { + certificates.put(alias, loadedCertificate); + loadedCertificateAliases.add(alias); + } } - Certificate[] certificateChain = keyVaultClient.getCertificateChain(alias); - if (!Objects.isNull(certificateChain)) { - certificateChains.put(alias, certificateChain); + } catch (RuntimeException exception) { + LOGGER.log(WARNING, exception, () -> "Failed to load certificate for alias: " + alias); + } + } + + private void loadCertificateChainIfNeeded(String alias) { + refreshCertificatesIfNeeded(); + KeyVaultClient currentKeyVaultClient = getKeyVaultClient(); + + if (alias == null || currentKeyVaultClient == null) { + return; + } + + synchronized (this) { + if (loadedCertificateChainAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; } - }); + } - lastRefreshTime = new Date(); + try { + Certificate[] loadedCertificateChain = currentKeyVaultClient.getCertificateChain(alias); + synchronized (this) { + if (currentKeyVaultClient != keyVaultClient + || loadedCertificateChainAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; + } + + if (loadedCertificateChain != null && loadedCertificateChain.length > 0) { + certificateChains.put(alias, loadedCertificateChain); + loadedCertificateChainAliases.add(alias); + } + } + } catch (RuntimeException exception) { + LOGGER.log(WARNING, exception, () -> "Failed to load certificate chain for alias: " + alias); + } + } + + private void loadCertificateKeyIfNeeded(String alias) { + refreshCertificatesIfNeeded(); + KeyVaultClient currentKeyVaultClient = getKeyVaultClient(); + + if (alias == null || currentKeyVaultClient == null) { + return; + } + + synchronized (this) { + if (loadedCertificateKeyAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; + } + } + + try { + Key loadedKey = currentKeyVaultClient.getKey(alias, null); + synchronized (this) { + if (currentKeyVaultClient != keyVaultClient + || loadedCertificateKeyAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; + } + + if (loadedKey != null) { + certificateKeys.put(alias, loadedKey); + loadedCertificateKeyAliases.add(alias); + } + } + } catch (RuntimeException exception) { + LOGGER.log(WARNING, exception, () -> "Failed to load certificate key for alias: " + alias); + } } /** @@ -186,8 +443,25 @@ public synchronized void refreshCertificates() { * @return Certificate alias if it exists. */ public String refreshAndGetAliasByCertificate(Certificate certificate) { + if (certificate == null) { + return null; + } + refreshCertificates(); - return getCertificates().entrySet() + + List aliasesSnapshot; + synchronized (this) { + aliasesSnapshot = new ArrayList<>(Optional.ofNullable(aliases).orElse(Collections.emptyList())); + } + + aliasesSnapshot.forEach(this::loadCertificateIfNeeded); + + Map certificatesSnapshot; + synchronized (this) { + certificatesSnapshot = new HashMap<>(certificates); + } + + return certificatesSnapshot.entrySet() .stream() .filter(entry -> certificate.equals(entry.getValue())) .findFirst() @@ -202,10 +476,13 @@ public String refreshAndGetAliasByCertificate(Certificate certificate) { * @param alias Deleted certificate. */ @Override - public void deleteEntry(String alias) { + public synchronized void deleteEntry(String alias) { if (aliases != null) { aliases.remove(alias); } + loadedCertificateAliases.remove(alias); + loadedCertificateChainAliases.remove(alias); + loadedCertificateKeyAliases.remove(alias); certificates.remove(alias); certificateChains.remove(alias); certificateKeys.remove(alias); diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreUnitTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreUnitTest.java index 9e290eaf45fc..0f3880199234 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreUnitTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreUnitTest.java @@ -4,6 +4,8 @@ package com.azure.security.keyvault.jca; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; import java.io.ByteArrayInputStream; import java.security.ProviderException; @@ -11,10 +13,13 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Base64; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +@ResourceLock(Resources.SYSTEM_PROPERTIES) public class KeyVaultKeyStoreUnitTest { /** @@ -91,4 +96,30 @@ public void testEngineSetCertificateEntry() { assertNotNull(keystore.engineGetCertificate("setcert")); } + @Test + public void testGetKeyVaultCertificateAliasFilterPatterns() { + String originalFilterPatterns = System.getProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY); + try { + System.clearProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY); + KeyVaultKeyStore keyVaultKeyStore = new KeyVaultKeyStore(); + assertTrue(keyVaultKeyStore.getKeyVaultCertificateAliasFilterPatterns().isEmpty()); + + System.setProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY, + "^prod-.*,!^prod-deprecated-.*,,myalias"); + keyVaultKeyStore = new KeyVaultKeyStore(); + Set patterns = keyVaultKeyStore.getKeyVaultCertificateAliasFilterPatterns(); + + assertEquals(3, patterns.size()); + assertTrue(patterns.contains("^prod-.*")); + assertTrue(patterns.contains("!^prod-deprecated-.*")); + assertTrue(patterns.contains("myalias")); + } finally { + if (originalFilterPatterns == null) { + System.clearProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY); + } else { + System.setProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY, originalFilterPatterns); + } + } + } + } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java index 2791d1742e6a..1c8e3872f3fa 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java @@ -4,13 +4,20 @@ package com.azure.security.keyvault.jca.implementation.certificates; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.azure.security.keyvault.jca.implementation.KeyVaultClient; import java.security.Key; import java.security.cert.Certificate; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -23,6 +30,8 @@ public class KeyVaultCertificatesTest { private final Certificate certificate = mock(Certificate.class); + private final Certificate[] certificateChain = new Certificate[] { certificate }; + private KeyVaultCertificates keyVaultCertificates; @BeforeEach @@ -32,6 +41,7 @@ public void beforeEach() { when(keyVaultClient.getAliases()).thenReturn(aliases); when(keyVaultClient.getKey("myalias", null)).thenReturn(key); when(keyVaultClient.getCertificate("myalias")).thenReturn(certificate); + when(keyVaultClient.getCertificateChain("myalias")).thenReturn(certificateChain); keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient); } @@ -42,12 +52,17 @@ public void testGetAliases() { @Test public void testGetKey() { - Assertions.assertTrue(keyVaultCertificates.getCertificateKeys().containsValue(key)); + Assertions.assertEquals(key, keyVaultCertificates.getCertificateKey("myalias")); } @Test public void testGetCertificate() { - Assertions.assertTrue(keyVaultCertificates.getCertificates().containsValue(certificate)); + Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias")); + } + + @Test + public void testGetCertificateChain() { + Assertions.assertArrayEquals(certificateChain, keyVaultCertificates.getCertificateChain("myalias")); } @Test @@ -59,6 +74,11 @@ public void testRefreshAndGetAliasByCertificate() { Assertions.assertNull(keyVaultCertificates.getCertificates().get("myalias")); } + @Test + public void testRefreshAndGetAliasByCertificateWithNullCertificate() { + Assertions.assertNull(keyVaultCertificates.refreshAndGetAliasByCertificate(null)); + } + @Test public void testDeleteAlias() { Assertions.assertTrue(keyVaultCertificates.getAliases().contains("myalias")); @@ -66,4 +86,233 @@ public void testDeleteAlias() { Assertions.assertFalse(keyVaultCertificates.getAliases().contains("myalias")); } + @Test + public void testGetAliasesDoesNotLoadCertificateDetailsEagerly() { + keyVaultCertificates.getAliases(); + + verify(keyVaultClient, never()).getKey("myalias", null); + verify(keyVaultClient, never()).getCertificate("myalias"); + verify(keyVaultClient, never()).getCertificateChain("myalias"); + } + + @Test + public void testLoadCertificateDetailsForRequestedAliasOnly() { + List aliases = new ArrayList<>(); + aliases.add("myalias"); + aliases.add("otheralias"); + + Key otherKey = mock(Key.class); + Certificate otherCertificate = mock(Certificate.class); + + when(keyVaultClient.getAliases()).thenReturn(aliases); + when(keyVaultClient.getKey("otheralias", null)).thenReturn(otherKey); + when(keyVaultClient.getCertificate("otheralias")).thenReturn(otherCertificate); + + keyVaultCertificates.getCertificate("myalias"); + + verify(keyVaultClient, times(1)).getCertificate("myalias"); + verify(keyVaultClient, never()).getKey("myalias", null); + verify(keyVaultClient, never()).getCertificateChain("myalias"); + verify(keyVaultClient, never()).getKey("otheralias", null); + verify(keyVaultClient, never()).getCertificate("otheralias"); + verify(keyVaultClient, never()).getCertificateChain("otheralias"); + } + + @Test + public void testGetKeyLoadsOnlyKeyForRequestedAlias() { + keyVaultCertificates.getCertificateKey("myalias"); + + verify(keyVaultClient, times(1)).getKey("myalias", null); + verify(keyVaultClient, never()).getCertificate("myalias"); + verify(keyVaultClient, never()).getCertificateChain("myalias"); + } + + @Test + public void testGetCertificateChainLoadsOnlyChainForRequestedAlias() { + keyVaultCertificates.getCertificateChain("myalias"); + + verify(keyVaultClient, times(1)).getCertificateChain("myalias"); + verify(keyVaultClient, never()).getKey("myalias", null); + verify(keyVaultClient, never()).getCertificate("myalias"); + } + + @Test + public void testConfiguredAliasesFilter() { + List aliases = new ArrayList<>(); + aliases.add("myalias"); + aliases.add("otheralias"); + when(keyVaultClient.getAliases()).thenReturn(aliases); + + keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias")); + + List result = keyVaultCertificates.getAliases(); + Assertions.assertEquals(1, result.size()); + Assertions.assertTrue(result.contains("myalias")); + Assertions.assertFalse(result.contains("otheralias")); + } + + @Test + public void testFilterPatternsIncludeRegex() { + List aliases = new ArrayList<>(); + aliases.add("prod-cert"); + aliases.add("dev-cert"); + when(keyVaultClient.getAliases()).thenReturn(aliases); + + keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("^prod-.*")); + + Assertions.assertEquals(Collections.singletonList("prod-cert"), keyVaultCertificates.getAliases()); + verify(keyVaultClient, times(1)).getAliases(); + } + + @Test + public void testFilterPatternsExcludeRegex() { + List aliases = new ArrayList<>(); + aliases.add("prod-active"); + aliases.add("prod-deprecated"); + when(keyVaultClient.getAliases()).thenReturn(aliases); + + Set filterPatterns = new HashSet<>(Arrays.asList("^prod-.*", "!^prod-deprecated$")); + keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, filterPatterns); + + Assertions.assertEquals(Collections.singletonList("prod-active"), keyVaultCertificates.getAliases()); + verify(keyVaultClient, times(1)).getAliases(); + } + + @Test + public void testConfiguredAliasesFilterAfterRefresh() { + keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias")); + + Assertions.assertEquals(Collections.singletonList("myalias"), keyVaultCertificates.getAliases()); + + keyVaultCertificates.refreshCertificates(); + + List refreshedAliases = keyVaultCertificates.getAliases(); + Assertions.assertEquals(1, refreshedAliases.size()); + Assertions.assertTrue(refreshedAliases.contains("myalias")); + Assertions.assertFalse(refreshedAliases.contains("otheralias")); + Assertions.assertFalse(refreshedAliases.contains("new")); + verify(keyVaultClient, times(2)).getAliases(); + } + + @Test + public void testConfiguredAliasesFilterUsesListApi() { + when(keyVaultClient.getAliases()).thenReturn(Arrays.asList("configured-alias", "other-alias")); + + keyVaultCertificates + = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("configured-alias")); + + Assertions.assertEquals(Collections.singletonList("configured-alias"), keyVaultCertificates.getAliases()); + verify(keyVaultClient, times(1)).getAliases(); + } + + @Test + public void testConfiguredAliasesIgnoreNullEntries() { + Set configuredAliases = new HashSet<>(Arrays.asList("myalias", null)); + keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, configuredAliases); + + Assertions.assertEquals(Collections.singletonList("myalias"), keyVaultCertificates.getAliases()); + verify(keyVaultClient, times(1)).getAliases(); + } + + @Test + public void testInvalidFilterPatternThrows() { + Set filterPatterns = new HashSet<>(Collections.singletonList("[invalid")); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> new KeyVaultCertificates(60_000, keyVaultClient, filterPatterns)); + } + + @Test + public void testGetCertificateWithUnconfiguredAliasDoesNotFetchDetails() { + keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias")); + + Assertions.assertNull(keyVaultCertificates.getCertificate("otheralias")); + + verify(keyVaultClient, never()).getKey("otheralias", null); + verify(keyVaultClient, never()).getCertificate("otheralias"); + verify(keyVaultClient, never()).getCertificateChain("otheralias"); + } + + @Test + public void testAliasCertificateLoadFailureIsRetriedOnNextAccess() { + when(keyVaultClient.getCertificate("myalias")).thenThrow(new RuntimeException("transient error")) + .thenReturn(certificate); + + Assertions.assertNull(keyVaultCertificates.getCertificate("myalias")); + Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias")); + verify(keyVaultClient, times(2)).getCertificate("myalias"); + verify(keyVaultClient, never()).getKey("myalias", null); + verify(keyVaultClient, never()).getCertificateChain("myalias"); + } + + @Test + public void testAliasCertificateNullLoadIsRetriedOnNextAccess() { + when(keyVaultClient.getCertificate("myalias")).thenReturn(null).thenReturn(certificate); + + Assertions.assertNull(keyVaultCertificates.getCertificate("myalias")); + Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias")); + verify(keyVaultClient, times(2)).getCertificate("myalias"); + verify(keyVaultClient, never()).getKey("myalias", null); + verify(keyVaultClient, never()).getCertificateChain("myalias"); + } + + @Test + public void testAliasKeyLoadFailureIsRetriedOnNextAccess() { + when(keyVaultClient.getKey("myalias", null)).thenThrow(new RuntimeException("transient error")).thenReturn(key); + + Assertions.assertNull(keyVaultCertificates.getCertificateKey("myalias")); + Assertions.assertEquals(key, keyVaultCertificates.getCertificateKey("myalias")); + verify(keyVaultClient, times(2)).getKey("myalias", null); + verify(keyVaultClient, never()).getCertificate("myalias"); + verify(keyVaultClient, never()).getCertificateChain("myalias"); + } + + @Test + public void testAliasKeyNullLoadIsRetriedOnNextAccess() { + when(keyVaultClient.getKey("myalias", null)).thenReturn(null).thenReturn(key); + + Assertions.assertNull(keyVaultCertificates.getCertificateKey("myalias")); + Assertions.assertEquals(key, keyVaultCertificates.getCertificateKey("myalias")); + verify(keyVaultClient, times(2)).getKey("myalias", null); + verify(keyVaultClient, never()).getCertificate("myalias"); + verify(keyVaultClient, never()).getCertificateChain("myalias"); + } + + @Test + public void testAliasChainLoadFailureIsRetriedOnNextAccess() { + when(keyVaultClient.getCertificateChain("myalias")).thenThrow(new RuntimeException("transient error")) + .thenReturn(certificateChain); + + Assertions.assertNull(keyVaultCertificates.getCertificateChain("myalias")); + Assertions.assertArrayEquals(certificateChain, keyVaultCertificates.getCertificateChain("myalias")); + verify(keyVaultClient, times(2)).getCertificateChain("myalias"); + verify(keyVaultClient, never()).getCertificate("myalias"); + verify(keyVaultClient, never()).getKey("myalias", null); + } + + @Test + public void testAliasChainEmptyLoadIsRetriedOnNextAccess() { + when(keyVaultClient.getCertificateChain("myalias")).thenReturn(new Certificate[0]).thenReturn(certificateChain); + + Assertions.assertNull(keyVaultCertificates.getCertificateChain("myalias")); + Assertions.assertArrayEquals(certificateChain, keyVaultCertificates.getCertificateChain("myalias")); + verify(keyVaultClient, times(2)).getCertificateChain("myalias"); + verify(keyVaultClient, never()).getCertificate("myalias"); + verify(keyVaultClient, never()).getKey("myalias", null); + } + + @Test + public void testUpdateKeyVaultClientClearsCachedState() { + Assertions.assertTrue(keyVaultCertificates.getAliases().contains("myalias")); + Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias")); + + keyVaultCertificates.updateKeyVaultClient(null, null, null, null, null, null, false); + + Assertions.assertTrue(keyVaultCertificates.getAliases().isEmpty()); + Assertions.assertTrue(keyVaultCertificates.getCertificates().isEmpty()); + Assertions.assertTrue(keyVaultCertificates.getCertificateChains().isEmpty()); + Assertions.assertTrue(keyVaultCertificates.getCertificateKeys().isEmpty()); + Assertions.assertNull(keyVaultCertificates.getCertificate("myalias")); + } + }