Add a Secret Manager client for Nomulus (#872)

* Add a Secret Manager client for Nomulus
This commit is contained in:
Weimin Yu
2020-11-12 17:12:52 -05:00
committed by GitHub
parent de20334a66
commit ae6b414b82
146 changed files with 2666 additions and 1873 deletions
@@ -0,0 +1,126 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.privileges.secretmanager;
import com.google.auto.value.AutoValue;
import com.google.cloud.secretmanager.v1.SecretVersion;
import com.google.common.collect.Streams;
import java.util.Optional;
/** A Cloud Secret Manager client for Nomulus, bound to a specific GCP project. */
public interface SecretManagerClient {
/**
* Creates a new secret in the Cloud Secret Manager with no data.
*
* <p>Use addVersion to add data to this secret.
*
* @param secretId The ID of the secret, must be unique in a project
* @throws SecretAlreadyExistsException A secret with this secretId already exists
*/
void createSecret(String secretId);
/** Returns all secret IDs in the Cloud Secret Manager. */
Iterable<String> listSecrets();
/** Returns the {@link SecretVersionState} of all secrets with {@code secretId}. */
Iterable<SecretVersionState> listSecretVersions(String secretId);
/**
* Returns the version strings of all secrets in the given {@code state} with {@code secretId}.
*/
default Iterable<String> listSecretVersions(String secretId, SecretVersion.State state) {
return () ->
Streams.stream(listSecretVersions(secretId))
.filter(secretVersionState -> secretVersionState.state().equals(state))
.map(SecretVersionState::version)
.iterator();
}
/**
* Adds a new version of data to a secret.
*
* @param secretId The ID of the secret
* @param data The secret data to be stored in Cloud Secret Manager, encoded in utf-8 charset
* @return The version string of the newly added secret data
*/
String addSecretVersion(String secretId, String data);
/**
* Returns the data of a secret at the given version.
*
* @param secretId The ID of the secret
* @param version The version of the secret to fetch. If not provided, the {@code latest} version
* will be returned
*/
String getSecretData(String secretId, Optional<String> version);
/**
* Destroys a secret version.
*
* @param secretId The ID of the secret
* @param version The version of the secret to destroy
*/
void destroySecretVersion(String secretId, String version);
/**
* Deletes a secret from the Secret Manager. All versions of this secret will be destroyed.
*
* @param secretId The ID of the secret to be deleted
*/
void deleteSecret(String secretId);
/** Contains the {@link SecretVersion.State State} of an secret version. */
@AutoValue
abstract class SecretVersionState {
public abstract String secretId();
public abstract String version();
public abstract SecretVersion.State state();
public static SecretVersionState of(
String secretId, String version, SecretVersion.State state) {
return new AutoValue_SecretManagerClient_SecretVersionState(secretId, version, state);
}
}
/** Catch-all class for all SecretManager exceptions. */
class SecretManagerException extends RuntimeException {
SecretManagerException(Throwable cause) {
super(cause);
}
}
/** The secret to be created already exists. */
class SecretAlreadyExistsException extends SecretManagerException {
SecretAlreadyExistsException(Throwable cause) {
super(cause);
}
}
/**
* The resource being requested in the Secret Manager does not exist.
*
* <p>The missing resource may be a secret version or the secret itself. They are grouped together
* because it is not always possible to identify the type of the missing resource.
*/
class NoSuchSecretResourceException extends SecretManagerException {
NoSuchSecretResourceException(Throwable cause) {
super(cause);
}
}
}
@@ -0,0 +1,151 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.privileges.secretmanager;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.api.gax.rpc.ApiException;
import com.google.cloud.secretmanager.v1.ProjectName;
import com.google.cloud.secretmanager.v1.Replication;
import com.google.cloud.secretmanager.v1.Replication.Automatic;
import com.google.cloud.secretmanager.v1.Secret;
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient.ListSecretVersionsPagedResponse;
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient.ListSecretsPagedResponse;
import com.google.cloud.secretmanager.v1.SecretName;
import com.google.cloud.secretmanager.v1.SecretPayload;
import com.google.cloud.secretmanager.v1.SecretVersion;
import com.google.cloud.secretmanager.v1.SecretVersionName;
import com.google.common.collect.Iterables;
import com.google.protobuf.ByteString;
import google.registry.util.Retrier;
import java.util.Optional;
import java.util.concurrent.Callable;
import javax.inject.Inject;
/** Implements {@link SecretManagerClient} on Google Cloud Platform. */
public class SecretManagerClientImpl implements SecretManagerClient {
private final String project;
private final SecretManagerServiceClient csmClient;
private final Retrier retrier;
@Inject
SecretManagerClientImpl(String project, SecretManagerServiceClient csmClient, Retrier retrier) {
this.project = project;
this.csmClient = csmClient;
this.retrier = retrier;
}
@Override
public void createSecret(String secretId) {
checkNotNull(secretId, "secretId");
Secret secretSettings = Secret.newBuilder().setReplication(defaultReplicationPolicy()).build();
callSecretManager(
() -> csmClient.createSecret(ProjectName.of(project), secretId, secretSettings));
}
@Override
public Iterable<String> listSecrets() {
ListSecretsPagedResponse response =
callSecretManager(() -> csmClient.listSecrets(ProjectName.of(project)));
return Iterables.transform(
response.iterateAll(), secret -> SecretName.parse(secret.getName()).getSecret());
}
@Override
public Iterable<SecretVersionState> listSecretVersions(String secretId) {
checkNotNull(secretId, "secretId");
ListSecretVersionsPagedResponse response =
callSecretManager(() -> csmClient.listSecretVersions(SecretName.of(project, secretId)));
return Iterables.transform(
response.iterateAll(), SecretManagerClientImpl::toSecretVersionState);
}
private static SecretVersionState toSecretVersionState(SecretVersion secretVersion) {
SecretVersionName name = SecretVersionName.parse(secretVersion.getName());
return SecretVersionState.of(
name.getSecret(), name.getSecretVersion(), secretVersion.getState());
}
@Override
public String addSecretVersion(String secretId, String data) {
checkNotNull(secretId, "secretId");
checkNotNull(data, "data");
SecretName secretName = SecretName.of(project, secretId);
SecretPayload secretPayload =
SecretPayload.newBuilder().setData(ByteString.copyFromUtf8(data)).build();
SecretVersion response =
callSecretManager(() -> csmClient.addSecretVersion(secretName, secretPayload));
checkState(SecretVersionName.isParsableFrom(response.getName()));
SecretVersionName secretVersionName = SecretVersionName.parse(response.getName());
return secretVersionName.getSecretVersion();
}
@Override
public String getSecretData(String secretId, Optional<String> version) {
checkNotNull(secretId, "secretId");
checkNotNull(version, "version");
return callSecretManager(
() ->
csmClient
.accessSecretVersion(
SecretVersionName.of(project, secretId, version.orElse("latest")))
.getPayload()
.getData()
.toStringUtf8());
}
@Override
public void destroySecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
checkNotNull(version, "version");
callSecretManager(
() -> csmClient.destroySecretVersion(SecretVersionName.of(project, secretId, version)));
}
@Override
public void deleteSecret(String secretId) {
checkNotNull(secretId, "secretId");
callSecretManager(
() -> {
csmClient.deleteSecret(SecretName.of(project, secretId));
return null;
});
}
private <T> T callSecretManager(Callable<T> callable) {
try {
return retrier.callWithRetry(callable, SecretManagerClientImpl::isRetryableException);
} catch (ApiException e) {
switch (e.getStatusCode().getCode()) {
case ALREADY_EXISTS:
throw new SecretAlreadyExistsException(e);
case NOT_FOUND:
throw new NoSuchSecretResourceException(e);
default:
throw new SecretManagerException(e);
}
}
}
private static boolean isRetryableException(Throwable e) {
return e instanceof ApiException && ((ApiException) e).isRetryable();
}
private static Replication defaultReplicationPolicy() {
return Replication.newBuilder().setAutomatic(Automatic.newBuilder().build()).build();
}
}
@@ -0,0 +1,54 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.privileges.secretmanager;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
import dagger.Component;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.util.Retrier;
import google.registry.util.UtilsModule;
import java.io.IOException;
import javax.inject.Singleton;
/** Provides bindings for {@link SecretManagerClient}. */
@Module
public class SecretManagerModule {
private final String project;
public SecretManagerModule(String project) {
this.project = checkNotNull(project, "project");
}
@Provides
@Singleton
SecretManagerClient provideSecretManagerClient(Retrier retrier) {
try {
return new SecretManagerClientImpl(project, SecretManagerServiceClient.create(), retrier);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Singleton
@Component(modules = {ConfigModule.class, SecretManagerModule.class, UtilsModule.class})
public interface SecretManagerComponent {
SecretManagerClient secretManagerClient();
}
}