-
Notifications
You must be signed in to change notification settings - Fork 4.6k
[Java] Support dynamic secret provider registration via SecretRegistrar #39940
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.apache.beam.sdk.util; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import java.util.Map; | ||
| import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; | ||
|
|
||
| /** {@link AutoService} registrar for the {@link GcpHsmGeneratedSecret}. */ | ||
| @AutoService(SecretRegistrar.class) | ||
| public class GcpHsmGeneratedSecretRegistrar implements SecretRegistrar { | ||
|
|
||
| @Override | ||
| public Map<String, SecretFactory> getSecretFactories() { | ||
| return ImmutableMap.of( | ||
| "googlecloudhsmgeneratedsecretmanager", GcpHsmGeneratedSecret::fromMap, | ||
| "gcphsmgeneratedsecret", GcpHsmGeneratedSecret::fromMap); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.apache.beam.sdk.util; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import java.util.Map; | ||
| import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; | ||
|
|
||
| /** {@link AutoService} registrar for the {@link GcpSecret}. */ | ||
| @AutoService(SecretRegistrar.class) | ||
| public class GcpSecretRegistrar implements SecretRegistrar { | ||
|
|
||
| @Override | ||
| public Map<String, SecretFactory> getSecretFactories() { | ||
| return ImmutableMap.of( | ||
| "googlecloudsecretmanager", GcpSecret::fromMap, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error readability: previously, the error displayed the canonical PascalCase names: now SECRET_FACTORIES.keySet() returns all lower cases. consider retaining the original camel case? |
||
| "gcpsecret", GcpSecret::fromMap); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,8 +21,11 @@ | |
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import java.io.Serializable; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import org.apache.beam.sdk.util.common.ReflectHelpers; | ||
| import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; | ||
| import org.checkerframework.checker.nullness.qual.Nullable; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
@@ -35,6 +38,30 @@ | |
| * should be able to return a valid byte array representing the secret. | ||
| */ | ||
| public abstract class Secret implements Serializable { | ||
| private static final Logger LOG = LoggerFactory.getLogger(Secret.class); | ||
| private static final Map<String, SecretRegistrar.SecretFactory> SECRET_FACTORIES = | ||
| loadSecretFactories(); | ||
|
|
||
| private static Map<String, SecretRegistrar.SecretFactory> loadSecretFactories() { | ||
| Map<String, SecretRegistrar.SecretFactory> factories = new HashMap<>(); | ||
| for (SecretRegistrar registrar : ReflectHelpers.loadServicesOrdered(SecretRegistrar.class)) { | ||
| for (Map.Entry<String, SecretRegistrar.SecretFactory> entry : | ||
| registrar.getSecretFactories().entrySet()) { | ||
| String key = entry.getKey().toLowerCase(); | ||
| if (factories.containsKey(key)) { | ||
| throw new IllegalStateException( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general, AutoService scans all classes loaded to JVM and if there are malformed SecretRegistrar (e.g. from unit test, etc) leaked into class path, it will crash the whole Since here it dynamically loads class and execute codes, consider a fail safe handling here. |
||
| String.format( | ||
| "Duplicate SecretRegistrar for secret manager name '%s': %s and %s", | ||
| key, | ||
| factories.get(key).getClass().getName(), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When using method references like Checked by test: We should print actual SecretRegistrar implementations. A way to do this is to track the registering registrar in a separate map to report the actual conflicting SecretRegistrar classes ========= If user shade Beam and relocated packages, there is still risk of duplicating classes. Not sure how should we handle them. |
||
| entry.getValue().getClass().getName())); | ||
| } | ||
| factories.put(key, entry.getValue()); | ||
| } | ||
| } | ||
| return ImmutableMap.copyOf(factories); | ||
| } | ||
|
|
||
| private transient byte @Nullable [] cachedSecretBytes = null; | ||
|
|
||
| /** | ||
|
|
@@ -104,29 +131,23 @@ public static Secret parseSecretOption(String secretOption) { | |
| } | ||
|
|
||
| String secretType = rawType.toLowerCase(); | ||
| String secretManager; | ||
| switch (secretType) { | ||
| case "gcpsecret": | ||
| secretManager = "GoogleCloudSecretManager"; | ||
| break; | ||
| case "gcphsmgeneratedsecret": | ||
| secretManager = "GoogleCloudHsmGeneratedSecretManager"; | ||
| break; | ||
| default: | ||
| throw new IllegalArgumentException( | ||
| String.format( | ||
| "Invalid secret type %s, currently only GcpSecret and GcpHsmGeneratedSecret are supported", | ||
| secretType)); | ||
| SecretRegistrar.SecretFactory factory = SECRET_FACTORIES.get(secretType); | ||
| if (factory == null) { | ||
| throw new IllegalArgumentException( | ||
| String.format( | ||
| "Invalid secret type %s, currently supported types: %s", | ||
| rawType, SECRET_FACTORIES.keySet())); | ||
| } | ||
|
|
||
| try { | ||
| ObjectMapper mapper = new ObjectMapper(); | ||
| String jsonSpec = mapper.writeValueAsString(paramMap); | ||
| return fromJson(jsonSpec, secretManager); | ||
| return factory.createSecret(paramMap); | ||
| } catch (Exception e) { | ||
| if (e instanceof IllegalArgumentException) { | ||
| throw (IllegalArgumentException) e; | ||
| } | ||
| if (e instanceof NullPointerException) { | ||
| throw (NullPointerException) e; | ||
| } | ||
| throw new RuntimeException("Failed to parse secret option", e); | ||
| } | ||
| } | ||
|
|
@@ -139,7 +160,6 @@ public static Secret parseSecretOption(String secretOption) { | |
| * @return An instance of Secret. | ||
| */ | ||
| public static Secret fromJson(@Nullable String spec, @Nullable String secretManager) { | ||
| Logger logger = LoggerFactory.getLogger(Secret.class); | ||
| String smManager = secretManager != null ? secretManager.trim() : null; | ||
| if (smManager != null && smManager.isEmpty()) { | ||
| smManager = null; | ||
|
|
@@ -152,38 +172,23 @@ public static Secret fromJson(@Nullable String spec, @Nullable String secretMana | |
| mapper.configure(com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); | ||
| specMap = mapper.readValue(spec, new TypeReference<Map<String, String>>() {}); | ||
| } catch (Exception e) { | ||
| logger.debug("Failed to parse secret spec as JSON map", e); | ||
| LOG.debug("Failed to parse secret spec as JSON map", e); | ||
| } | ||
| } | ||
|
|
||
| if (smManager != null) { | ||
| switch (smManager.toLowerCase()) { | ||
| case "googlecloudsecretmanager": | ||
| case "gcpsecret": | ||
| if (specMap != null) { | ||
| return GcpSecret.fromMap(specMap); | ||
| } else if (spec != null) { | ||
| return new GcpSecret(spec); | ||
| } else { | ||
| throw new IllegalArgumentException("Invalid spec for GcpSecret"); | ||
| } | ||
| case "googlecloudhsmgeneratedsecretmanager": | ||
| case "gcphsmgeneratedsecret": | ||
| if (specMap != null) { | ||
| return GcpHsmGeneratedSecret.fromMap(specMap); | ||
| } else { | ||
| throw new IllegalArgumentException("Invalid spec for GcpHsmGeneratedSecret"); | ||
| } | ||
| default: | ||
| throw new IllegalArgumentException( | ||
| String.format( | ||
| "Unsupported secret manager: '%s'. Currently supported options: 'GoogleCloudSecretManager', 'GoogleCloudHsmGeneratedSecretManager'.", | ||
| smManager)); | ||
| SecretRegistrar.SecretFactory factory = SECRET_FACTORIES.get(smManager.toLowerCase()); | ||
| if (factory != null) { | ||
| return factory.createSecret(specMap != null ? specMap : Collections.emptyMap()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Previously when specMap = null (jackson parser throws) it falls back to |
||
| } | ||
| throw new IllegalArgumentException( | ||
| String.format( | ||
| "Unsupported secret manager: '%s'. Currently supported options: %s.", | ||
| smManager, SECRET_FACTORIES.keySet())); | ||
| } | ||
|
|
||
| if (specMap != null) { | ||
| logger.warn( | ||
| LOG.warn( | ||
| "The 'spec' parameter appears to be a JSON specification, but 'secret_manager' is not set. Defaulting to Raw."); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.apache.beam.sdk.util; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import java.util.Map; | ||
| import java.util.ServiceLoader; | ||
|
|
||
| /** | ||
| * A registrar that creates {@link Secret} instances from a spec parameter map. | ||
| * | ||
| * <p>{@link Secret} creators have the ability to provide a registrar by creating a {@link | ||
| * ServiceLoader} entry and a concrete implementation of this interface. | ||
| * | ||
| * <p>It is optional but recommended to use one of the many build time tools such as {@link | ||
| * AutoService} to generate the necessary META-INF files automatically. | ||
| */ | ||
| public interface SecretRegistrar { | ||
|
|
||
| /** Functional interface for creating a {@link Secret} from a specification map. */ | ||
| @FunctionalInterface | ||
| interface SecretFactory { | ||
| /** | ||
| * Creates a {@link Secret} instance from a spec parameter map. | ||
| * | ||
| * @param specMap The parsed map of key-value parameters. | ||
| * @return The constructed {@link Secret} instance. | ||
| */ | ||
| Secret createSecret(Map<String, String> specMap); | ||
| } | ||
|
|
||
| /** | ||
| * Returns a map from secret provider name / type (case-insensitive) to the corresponding {@link | ||
| * SecretFactory}. | ||
| */ | ||
| Map<String, SecretFactory> getSecretFactories(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.apache.beam.sdk.util; | ||
|
|
||
| import static org.hamcrest.MatcherAssert.assertThat; | ||
| import static org.hamcrest.Matchers.hasItems; | ||
| import static org.junit.Assert.fail; | ||
|
|
||
| import java.util.Map; | ||
| import java.util.ServiceLoader; | ||
| import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; | ||
| import org.junit.Test; | ||
| import org.junit.runner.RunWith; | ||
| import org.junit.runners.JUnit4; | ||
|
|
||
| /** Tests for {@link GcpSecretRegistrar} and {@link GcpHsmGeneratedSecretRegistrar}. */ | ||
| @RunWith(JUnit4.class) | ||
| public class GcpSecretRegistrarTest { | ||
|
|
||
| @Test | ||
| public void testGcpSecretRegistrarServiceLoader() { | ||
| for (SecretRegistrar registrar : | ||
| Lists.newArrayList(ServiceLoader.load(SecretRegistrar.class).iterator())) { | ||
| if (registrar instanceof GcpSecretRegistrar) { | ||
| Map<String, SecretRegistrar.SecretFactory> factories = registrar.getSecretFactories(); | ||
| assertThat(factories.keySet(), hasItems("googlecloudsecretmanager", "gcpsecret")); | ||
| return; | ||
| } | ||
| } | ||
| fail("Expected to find " + GcpSecretRegistrar.class); | ||
| } | ||
|
|
||
| @Test | ||
| public void testGcpHsmGeneratedSecretRegistrarServiceLoader() { | ||
| for (SecretRegistrar registrar : | ||
| Lists.newArrayList(ServiceLoader.load(SecretRegistrar.class).iterator())) { | ||
| if (registrar instanceof GcpHsmGeneratedSecretRegistrar) { | ||
| Map<String, SecretRegistrar.SecretFactory> factories = registrar.getSecretFactories(); | ||
| assertThat( | ||
| factories.keySet(), | ||
| hasItems("googlecloudhsmgeneratedsecretmanager", "gcphsmgeneratedsecret")); | ||
| return; | ||
| } | ||
| } | ||
| fail("Expected to find " + GcpHsmGeneratedSecretRegistrar.class); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.