1 /* 2 * Copyright 2020 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.xds.internal.security.certprovider; 18 19 import com.google.common.annotations.VisibleForTesting; 20 import io.grpc.xds.internal.security.ReferenceCountingMap; 21 import io.grpc.xds.internal.security.certprovider.CertificateProvider.Watcher; 22 import java.io.Closeable; 23 import java.util.Objects; 24 import java.util.logging.Level; 25 import java.util.logging.Logger; 26 import javax.annotation.concurrent.ThreadSafe; 27 28 /** 29 * Global map of all ref-counted {@link CertificateProvider}s that have been instantiated in 30 * the application. Also propagates updates received from a {@link CertificateProvider} to all 31 * the {@link Watcher}s registered for that CertificateProvider. The Store is meant to be 32 * used internally by gRPC and *not* a public API. 33 */ 34 @ThreadSafe 35 public final class CertificateProviderStore { 36 private static final Logger logger = Logger.getLogger(CertificateProviderStore.class.getName()); 37 38 private static CertificateProviderStore instance; 39 private final CertificateProviderRegistry certificateProviderRegistry; 40 private final ReferenceCountingMap<CertProviderKey, CertificateProvider> certProviderMap; 41 42 /** Opaque Handle returned by {@link #createOrGetProvider}. */ 43 @VisibleForTesting 44 final class Handle implements Closeable { 45 private final CertProviderKey key; 46 private final Watcher watcher; 47 @VisibleForTesting 48 final CertificateProvider certProvider; 49 Handle(CertProviderKey key, Watcher watcher, CertificateProvider certProvider)50 private Handle(CertProviderKey key, Watcher watcher, CertificateProvider certProvider) { 51 this.key = key; 52 this.watcher = watcher; 53 this.certProvider = certProvider; 54 } 55 56 /** 57 * Removes the associated {@link Watcher} for the {@link CertificateProvider} and 58 * decrements the ref-count. Releases the {@link CertificateProvider} if the ref-count 59 * has reached 0. 60 */ 61 @Override close()62 public synchronized void close() { 63 CertificateProvider.DistributorWatcher distWatcher = certProvider.getWatcher(); 64 distWatcher.removeWatcher(watcher); 65 certProviderMap.release(key, certProvider); 66 } 67 } 68 69 private static final class CertProviderKey { 70 private final String certName; 71 private final String pluginName; 72 private final boolean notifyCertUpdates; 73 private final Object config; 74 CertProviderKey( String certName, String pluginName, boolean notifyCertUpdates, Object config)75 private CertProviderKey( 76 String certName, String pluginName, boolean notifyCertUpdates, Object config) { 77 this.certName = certName; 78 this.pluginName = pluginName; 79 this.notifyCertUpdates = notifyCertUpdates; 80 this.config = config; 81 } 82 83 @Override equals(Object o)84 public boolean equals(Object o) { 85 if (this == o) { 86 return true; 87 } 88 if (!(o instanceof CertProviderKey)) { 89 return false; 90 } 91 CertProviderKey that = (CertProviderKey) o; 92 return notifyCertUpdates == that.notifyCertUpdates 93 && Objects.equals(certName, that.certName) 94 && Objects.equals(pluginName, that.pluginName) 95 && Objects.equals(config, that.config); 96 } 97 98 @Override hashCode()99 public int hashCode() { 100 return Objects.hash(certName, pluginName, notifyCertUpdates, config); 101 } 102 103 @Override toString()104 public String toString() { 105 return "CertProviderKey{" 106 + "certName='" 107 + certName 108 + '\'' 109 + ", pluginName='" 110 + pluginName 111 + '\'' 112 + ", notifyCertUpdates=" 113 + notifyCertUpdates 114 + ", config=" 115 + config 116 + '}'; 117 } 118 } 119 120 private final class CertProviderFactory 121 implements ReferenceCountingMap.ValueFactory<CertProviderKey, CertificateProvider> { 122 CertProviderFactory()123 private CertProviderFactory() { 124 } 125 126 @Override create(CertProviderKey key)127 public CertificateProvider create(CertProviderKey key) { 128 CertificateProviderProvider certProviderProvider = 129 certificateProviderRegistry.getProvider(key.pluginName); 130 if (certProviderProvider == null) { 131 throw new IllegalArgumentException("Provider not found for " + key.pluginName); 132 } 133 CertificateProvider certProvider = certProviderProvider.createCertificateProvider( 134 key.config, new CertificateProvider.DistributorWatcher(), key.notifyCertUpdates); 135 certProvider.start(); 136 return certProvider; 137 } 138 } 139 140 @VisibleForTesting CertificateProviderStore(CertificateProviderRegistry certificateProviderRegistry)141 public CertificateProviderStore(CertificateProviderRegistry certificateProviderRegistry) { 142 this.certificateProviderRegistry = certificateProviderRegistry; 143 certProviderMap = new ReferenceCountingMap<>(new CertProviderFactory()); 144 } 145 146 /** 147 * Creates or retrieves a {@link CertificateProvider} instance, increments its ref-count and 148 * registers the watcher passed. Returns a {@link Handle} that can be {@link Handle#close()}d when 149 * the instance is no longer needed by the caller. 150 * 151 * @param notifyCertUpdates when true, the caller is interested in identity cert updates. When 152 * false, the caller cannot depend on receiving the {@link Watcher#updateCertificate} 153 * callbacks but may still receive these callbacks which should be ignored. 154 * @throws IllegalArgumentException in case of errors in processing config or the plugin is 155 * incapable of sending cert updates when notifyCertUpdates is true. 156 * @throws UnsupportedOperationException if the plugin is incapable of sending cert updates when 157 * notifyCertUpdates is true. 158 */ createOrGetProvider( String certName, String pluginName, Object config, Watcher watcher, boolean notifyCertUpdates)159 public synchronized Handle createOrGetProvider( 160 String certName, 161 String pluginName, 162 Object config, 163 Watcher watcher, 164 boolean notifyCertUpdates) { 165 if (!notifyCertUpdates) { 166 // we try to get a provider first for notifyCertUpdates==true always 167 try { 168 return createProviderHelper(certName, pluginName, config, watcher, true); 169 } catch (UnsupportedOperationException uoe) { 170 // ignore & log exception and fall thru to create a provider with actual value 171 logger.log(Level.FINE, "Trying to get provider for notifyCertUpdates==true", uoe); 172 } 173 } 174 return createProviderHelper(certName, pluginName, config, watcher, notifyCertUpdates); 175 } 176 createProviderHelper( String certName, String pluginName, Object config, Watcher watcher, boolean notifyCertUpdates)177 private synchronized Handle createProviderHelper( 178 String certName, 179 String pluginName, 180 Object config, 181 Watcher watcher, 182 boolean notifyCertUpdates) { 183 CertProviderKey key = new CertProviderKey(certName, pluginName, notifyCertUpdates, config); 184 CertificateProvider provider = certProviderMap.get(key); 185 CertificateProvider.DistributorWatcher distWatcher = provider.getWatcher(); 186 distWatcher.addWatcher(watcher); 187 return new Handle(key, watcher, provider); 188 } 189 190 /** Gets the CertificateProviderStore singleton instance. */ getInstance()191 public static synchronized CertificateProviderStore getInstance() { 192 if (instance == null) { 193 instance = new CertificateProviderStore(CertificateProviderRegistry.getInstance()); 194 } 195 return instance; 196 } 197 } 198