Add a credential store backed by Secret Manager (#901)

* Add a credential store backed by Secret Manager

Added a SqlCredentialStore that stores user credentials with one level
of indirection: for each credential, an addtional secret is used to
identify the 'live' version of the credential. This is a work in
progress and the overall design is explained in
go/dr-sql-security.

Also added two nomulus commands for credential management. They are
stop-gap measures that will be deprecated by the planned privilege
management system.
This commit is contained in:
Weimin Yu 2020-12-10 11:29:44 -05:00 committed by GitHub
parent 2c6ee6dae9
commit 83ed448741
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 675 additions and 48 deletions

View file

@ -415,6 +415,14 @@ public final class RegistryConfig {
return config.cloudSql.instanceConnectionName;
}
@Provides
@Config("cloudSqlDbInstanceName")
public static String providesCloudSqlDbInstance(RegistryConfigSettings config) {
// Format of instanceConnectionName: project-id:region:instance-name
int lastColonIndex = config.cloudSql.instanceConnectionName.lastIndexOf(':');
return config.cloudSql.instanceConnectionName.substring(lastColonIndex + 1);
}
@Provides
@Config("cloudDnsRootUrl")
public static Optional<String> getCloudDnsRootUrl(RegistryConfigSettings config) {

View file

@ -22,6 +22,9 @@ import java.util.Optional;
/** A Cloud Secret Manager client for Nomulus, bound to a specific GCP project. */
public interface SecretManagerClient {
/** Returns the project name with which this client is associated. */
String getProject();
/**
* Creates a new secret in the Cloud Secret Manager with no data.
*
@ -32,6 +35,9 @@ public interface SecretManagerClient {
*/
void createSecret(String secretId);
/** Checks if a secret with the given {@code secretId} already exists. */
boolean secretExists(String secretId);
/** Returns all secret IDs in the Cloud Secret Manager. */
Iterable<String> listSecrets();
@ -67,6 +73,24 @@ public interface SecretManagerClient {
*/
String getSecretData(String secretId, Optional<String> version);
/**
* Enables a secret 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
*/
void enableSecretVersion(String secretId, String version);
/**
* Disables a secret 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
*/
void disableSecretVersion(String secretId, String version);
/**
* Destroys a secret version.
*

View file

@ -29,12 +29,11 @@ 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.common.collect.Streams;
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 {
@ -42,13 +41,17 @@ public class SecretManagerClientImpl implements SecretManagerClient {
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 String getProject() {
return project;
}
@Override
public void createSecret(String secretId) {
checkNotNull(secretId, "secretId");
@ -57,12 +60,25 @@ public class SecretManagerClientImpl implements SecretManagerClient {
() -> csmClient.createSecret(ProjectName.of(project), secretId, secretSettings));
}
@Override
public boolean secretExists(String secretId) {
checkNotNull(secretId, "secretId");
try {
callSecretManager(() -> csmClient.getSecret(SecretName.of(project, secretId)));
return true;
} catch (NoSuchSecretResourceException e) {
return false;
}
}
@Override
public Iterable<String> listSecrets() {
ListSecretsPagedResponse response =
callSecretManager(() -> csmClient.listSecrets(ProjectName.of(project)));
return Iterables.transform(
response.iterateAll(), secret -> SecretName.parse(secret.getName()).getSecret());
return () ->
Streams.stream(response.iterateAll())
.map(secret -> SecretName.parse(secret.getName()).getSecret())
.iterator();
}
@Override
@ -70,8 +86,10 @@ public class SecretManagerClientImpl implements SecretManagerClient {
checkNotNull(secretId, "secretId");
ListSecretVersionsPagedResponse response =
callSecretManager(() -> csmClient.listSecretVersions(SecretName.of(project, secretId)));
return Iterables.transform(
response.iterateAll(), SecretManagerClientImpl::toSecretVersionState);
return () ->
Streams.stream(response.iterateAll())
.map(SecretManagerClientImpl::toSecretVersionState)
.iterator();
}
private static SecretVersionState toSecretVersionState(SecretVersion secretVersion) {
@ -108,6 +126,22 @@ public class SecretManagerClientImpl implements SecretManagerClient {
.toStringUtf8());
}
@Override
public void enableSecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
checkNotNull(version, "version");
callSecretManager(
() -> csmClient.enableSecretVersion(SecretVersionName.of(project, secretId, version)));
}
@Override
public void disableSecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
checkNotNull(version, "version");
callSecretManager(
() -> csmClient.disableSecretVersion(SecretVersionName.of(project, secretId, version)));
}
@Override
public void destroySecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");

View file

@ -14,12 +14,12 @@
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.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.util.Retrier;
import google.registry.util.UtilsModule;
@ -28,19 +28,16 @@ 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");
}
public abstract class SecretManagerModule {
@Provides
@Singleton
SecretManagerClient provideSecretManagerClient(Retrier retrier) {
static SecretManagerClient provideSecretManagerClient(
@Config("projectId") String project, Retrier retrier) {
try {
return new SecretManagerClientImpl(project, SecretManagerServiceClient.create(), retrier);
SecretManagerServiceClient stub = SecretManagerServiceClient.create();
Runtime.getRuntime().addShutdownHook(new Thread(stub::close));
return new SecretManagerClientImpl(project, stub, retrier);
} catch (IOException e) {
throw new RuntimeException(e);
}

View file

@ -0,0 +1,55 @@
// 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 avro.shaded.com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
import java.util.List;
/**
* Contains the login name and password of a Cloud SQL user.
*
* <p>User must take care not to include the {@link #SEPARATOR} in property values.
*/
@AutoValue
public abstract class SqlCredential {
public static final Character SEPARATOR = ' ';
public abstract String login();
public abstract String password();
@Override
public final String toString() {
// Use Object.toString(), which does not show object data.
return super.toString();
}
public final String toFormattedString() {
return String.format("%s%c%s", login(), SEPARATOR, password());
}
public static SqlCredential fromFormattedString(String sqlCredential) {
List<String> items = com.google.common.base.Splitter.on(SEPARATOR).splitToList(sqlCredential);
checkState(items.size() == 2, "Invalid SqlCredential string.");
return of(items.get(0), items.get(1));
}
public static SqlCredential of(String login, String password) {
return new AutoValue_SqlCredential(login, password);
}
}

View file

@ -0,0 +1,118 @@
// 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.cloud.secretmanager.v1.SecretVersionName;
import google.registry.config.RegistryConfig.Config;
import google.registry.privileges.secretmanager.SecretManagerClient.NoSuchSecretResourceException;
import google.registry.privileges.secretmanager.SecretManagerClient.SecretAlreadyExistsException;
import java.util.Optional;
import javax.inject.Inject;
/**
* Storage of SQL users' login credentials, backed by Cloud Secret Manager.
*
* <p>A user's credential is stored with one level of indirection using two secret IDs: Each version
* of the <em>credential data</em> is stored as follows: its secret ID is determined by {@link
* #getCredentialDataSecretId(SqlUser, String dbInstance)}, and the value of each version is a
* {@link SqlCredential}, serialized using {@link SqlCredential#toFormattedString}. The 'live'
* version of the credential is saved under the 'live pointer' secret explained below.
*
* <p>The pointer to the 'live' version of the credential data is stored as follows: its secret ID
* is determined by {@link #getLiveLabelSecretId(SqlUser, String dbInstance)}; and the value of each
* version is a {@link SecretVersionName} in String form, pointing to a version of the credential
* data. Only the 'latest' version of this secret should be used. It is guaranteed to be valid.
*
* <p>The indirection in credential storage makes it easy to handle failures in the credential
* change process.
*/
public class SqlCredentialStore {
private final SecretManagerClient csmClient;
private final String dbInstance;
@Inject
SqlCredentialStore(
SecretManagerClient csmClient, @Config("cloudSqlDbInstanceName") String dbInstance) {
this.csmClient = csmClient;
this.dbInstance = dbInstance;
}
public SqlCredential getCredential(SqlUser user) {
SecretVersionName credentialName = getLiveCredentialSecretVersion(user);
return SqlCredential.fromFormattedString(
csmClient.getSecretData(
credentialName.getSecret(), Optional.of(credentialName.getSecretVersion())));
}
public void createOrUpdateCredential(SqlUser user, String password) {
SecretVersionName dataName = saveCredentialData(user, password);
saveLiveLabel(user, dataName);
}
public void deleteCredential(SqlUser user) {
try {
csmClient.deleteSecret(getCredentialDataSecretId(user, dbInstance));
} catch (NoSuchSecretResourceException e) {
// ok
}
try {
csmClient.deleteSecret(getLiveLabelSecretId(user, dbInstance));
} catch (NoSuchSecretResourceException e) {
// ok.
}
}
private void createSecretIfAbsent(String secretId) {
try {
csmClient.createSecret(secretId);
} catch (SecretAlreadyExistsException ignore) {
// Not a problem.
}
}
private SecretVersionName saveCredentialData(SqlUser user, String password) {
String credentialDataSecretId = getCredentialDataSecretId(user, dbInstance);
createSecretIfAbsent(credentialDataSecretId);
String credentialVersion =
csmClient.addSecretVersion(
credentialDataSecretId,
SqlCredential.of(createDatabaseLoginName(user), password).toFormattedString());
return SecretVersionName.of(csmClient.getProject(), credentialDataSecretId, credentialVersion);
}
private void saveLiveLabel(SqlUser user, SecretVersionName dataVersionName) {
String liveLabelSecretId = getLiveLabelSecretId(user, dbInstance);
createSecretIfAbsent(liveLabelSecretId);
csmClient.addSecretVersion(liveLabelSecretId, dataVersionName.toString());
}
private SecretVersionName getLiveCredentialSecretVersion(SqlUser user) {
return SecretVersionName.parse(
csmClient.getSecretData(getLiveLabelSecretId(user, dbInstance), Optional.empty()));
}
private static String getLiveLabelSecretId(SqlUser user, String dbInstance) {
return String.format("sql-cred-live-label-%s-%s", user.geUserName(), dbInstance);
}
private static String getCredentialDataSecretId(SqlUser user, String dbInstance) {
return String.format("sql-cred-data-%s-%s", user.geUserName(), dbInstance);
}
// WIP: when b/170230882 is complete, login will be versioned.
private static String createDatabaseLoginName(SqlUser user) {
return user.geUserName();
}
}

View file

@ -0,0 +1,62 @@
// 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.common.base.Ascii;
/**
* SQL user information for privilege management purposes.
*
* <p>A {@link RobotUser} represents a software system accessing the database using its own
* credential. Robots are well known and enumerated in {@link RobotId}.
*/
public abstract class SqlUser {
private final UserType type;
private final String userName;
protected SqlUser(UserType type, String userName) {
this.type = type;
this.userName = userName;
}
public UserType getType() {
return type;
}
public String geUserName() {
return userName;
}
/** Cloud SQL user types. Please see class javadoc of {@link SqlUser} for more information. */
enum UserType {
// Work in progress. Human user will be added.
ROBOT
}
/** Enumerates the {@link RobotUser RobotUsers} in the system. */
public enum RobotId {
NOMULUS;
}
/** Information of a RobotUser for privilege management purposes. */
// Work in progress. Eventually will be provided based on configuration.
public static class RobotUser extends SqlUser {
public RobotUser(RobotId robot) {
super(UserType.ROBOT, Ascii.toLowerCase(robot.name()));
}
}
}

View file

@ -0,0 +1,73 @@
// 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.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Ascii;
import google.registry.privileges.secretmanager.SecretManagerClient.SecretManagerException;
import google.registry.privileges.secretmanager.SqlCredential;
import google.registry.privileges.secretmanager.SqlCredentialStore;
import google.registry.privileges.secretmanager.SqlUser;
import google.registry.privileges.secretmanager.SqlUser.RobotUser;
import google.registry.tools.params.PathParameter;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import javax.inject.Inject;
/**
* Command to get a Cloud SQL credential in the Secret Manager.
*
* <p>This command is a short-term tool that will be deprecated by the planned privilege server.
*/
@Parameters(separators = " =", commandDescription = "Get the Cloud SQL Credential for a given user")
public class GetSqlCredentialCommand implements Command {
@Inject SqlCredentialStore store;
@Parameter(names = "--user", description = "The Cloud SQL user.", required = true)
private String user;
@Parameter(
names = {"-o", "--output"},
description = "Name of output file for key data.",
validateWith = PathParameter.OutputFile.class)
private Path outputPath = null;
@Inject
GetSqlCredentialCommand() {}
@Override
public void run() throws Exception {
SqlUser sqlUser = new RobotUser(SqlUser.RobotId.valueOf(Ascii.toUpperCase(user)));
SqlCredential credential;
try {
credential = store.getCredential(sqlUser);
} catch (SecretManagerException e) {
System.out.println(e.getMessage());
return;
}
if (outputPath == null) {
System.out.printf("[%s]\n", credential.toFormattedString());
return;
}
try (FileOutputStream out = new FileOutputStream(outputPath.toFile())) {
out.write(credential.toFormattedString().getBytes(StandardCharsets.UTF_8));
}
}
}

View file

@ -78,6 +78,7 @@ public final class RegistryTool {
.put("get_routing_map", GetRoutingMapCommand.class)
.put("get_schema", GetSchemaCommand.class)
.put("get_schema_tree", GetSchemaTreeCommand.class)
.put("get_sql_credential", GetSqlCredentialCommand.class)
.put("get_tld", GetTldCommand.class)
.put("ghostryde", GhostrydeCommand.class)
.put("hash_certificate", HashCertificateCommand.class)
@ -104,6 +105,7 @@ public final class RegistryTool {
.put("resave_entities", ResaveEntitiesCommand.class)
.put("resave_environment_entities", ResaveEnvironmentEntitiesCommand.class)
.put("resave_epp_resource", ResaveEppResourceCommand.class)
.put("save_sql_credential", SaveSqlCredentialCommand.class)
.put("send_escrow_report_to_icann", SendEscrowReportToIcannCommand.class)
.put("set_num_instances", SetNumInstancesCommand.class)
.put("setup_ote", SetupOteCommand.class)

View file

@ -34,6 +34,7 @@ import google.registry.keyring.kms.KmsModule;
import google.registry.persistence.PersistenceModule;
import google.registry.persistence.PersistenceModule.NomulusToolJpaTm;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.rde.RdeModule;
import google.registry.request.Modules.DatastoreServiceModule;
import google.registry.request.Modules.Jackson2Module;
@ -74,6 +75,7 @@ import javax.inject.Singleton;
PersistenceModule.class,
RdeModule.class,
RequestFactoryModule.class,
SecretManagerModule.class,
URLFetchServiceModule.class,
UrlFetchTransportModule.class,
UserServiceModule.class,
@ -118,6 +120,8 @@ interface RegistryToolComponent {
void inject(GetOperationStatusCommand command);
void inject(GetSqlCredentialCommand command);
void inject(GhostrydeCommand command);
void inject(ImportDatastoreCommand command);
@ -138,6 +142,8 @@ interface RegistryToolComponent {
void inject(RenewDomainCommand command);
void inject(SaveSqlCredentialCommand command);
void inject(SendEscrowReportToIcannCommand command);
void inject(SetNumInstancesCommand command);

View file

@ -0,0 +1,69 @@
// 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.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Ascii;
import google.registry.privileges.secretmanager.SqlCredentialStore;
import google.registry.privileges.secretmanager.SqlUser;
import google.registry.privileges.secretmanager.SqlUser.RobotUser;
import google.registry.tools.params.PathParameter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
/**
* Command to create or update a Cloud SQL credential in the Secret Manager.
*
* <p>This command is a short-term tool that will be deprecated by the planned privilege server.
*/
@Parameters(
separators = " =",
commandDescription = "Create or update the Cloud SQL Credential for a given user")
public class SaveSqlCredentialCommand implements Command {
@Inject SqlCredentialStore store;
@Parameter(names = "--user", description = "The Cloud SQL user.", required = true)
private String user;
@Parameter(
names = {"--input"},
description =
"Name of input file for the password. If absent, command will prompt for "
+ "password in console.",
validateWith = PathParameter.InputFile.class)
private Path inputPath = null;
@Inject
SaveSqlCredentialCommand() {}
@Override
public void run() throws Exception {
String password = getPassword();
SqlUser sqlUser = new RobotUser(SqlUser.RobotId.valueOf(Ascii.toUpperCase(user)));
store.createOrUpdateCredential(sqlUser, password);
System.out.printf("\nDone:[%s]\n", password);
}
private String getPassword() throws Exception {
if (inputPath != null) {
return Files.readAllLines(inputPath, StandardCharsets.UTF_8).get(0);
}
return System.console().readLine("Please enter the password: ").trim();
}
}

View file

@ -32,6 +32,11 @@ public class FakeSecretManagerClient implements SecretManagerClient {
@Inject
FakeSecretManagerClient() {}
@Override
public String getProject() {
return "fake_project";
}
@Override
public void createSecret(String secretId) {
checkNotNull(secretId, "secretId");
@ -41,6 +46,12 @@ public class FakeSecretManagerClient implements SecretManagerClient {
secrets.put(secretId, new SecretEntry(secretId));
}
@Override
public boolean secretExists(String secretId) {
checkNotNull(secretId, "secretId");
return secrets.containsKey(secretId);
}
@Override
public Iterable<String> listSecrets() {
return ImmutableSet.copyOf(secrets.keySet());
@ -78,6 +89,28 @@ public class FakeSecretManagerClient implements SecretManagerClient {
return secretEntry.getVersion(version).getData();
}
@Override
public void enableSecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
checkNotNull(version, "version");
SecretEntry secretEntry = secrets.get(secretId);
if (secretEntry == null) {
throw new NoSuchSecretResourceException(null);
}
secretEntry.enableVersion(version);
}
@Override
public void disableSecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
checkNotNull(version, "version");
SecretEntry secretEntry = secrets.get(secretId);
if (secretEntry == null) {
throw new NoSuchSecretResourceException(null);
}
secretEntry.disableVersion(version);
}
@Override
public void destroySecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
@ -118,6 +151,20 @@ public class FakeSecretManagerClient implements SecretManagerClient {
return state;
}
void enable() {
if (state.equals(State.DESTROYED)) {
throw new SecretManagerException(null);
}
state = State.ENABLED;
}
void disable() {
if (state.equals(State.DESTROYED)) {
throw new SecretManagerException(null);
}
state = State.DISABLED;
}
void destroy() {
data = null;
state = State.DESTROYED;
@ -145,6 +192,8 @@ public class FakeSecretManagerClient implements SecretManagerClient {
return versions.get(index);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid version " + version.get());
} catch (ArrayIndexOutOfBoundsException e) {
throw new NoSuchSecretResourceException(null);
}
}
@ -156,15 +205,16 @@ public class FakeSecretManagerClient implements SecretManagerClient {
return builder.build();
}
void enableVersion(String version) {
getVersion(Optional.of(version)).enable();
}
void disableVersion(String version) {
getVersion(Optional.of(version)).disable();
}
void destroyVersion(String version) {
try {
int index = Integer.valueOf(version);
versions.get(index).destroy();
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid version " + version);
} catch (ArrayIndexOutOfBoundsException e) {
throw new NoSuchSecretResourceException(null);
}
getVersion(Optional.of(version)).destroy();
}
}
}

View file

@ -18,12 +18,17 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.cloud.secretmanager.v1.SecretVersion.State;
import google.registry.privileges.secretmanager.SecretManagerClient.NoSuchSecretResourceException;
import google.registry.privileges.secretmanager.SecretManagerClient.SecretAlreadyExistsException;
import google.registry.privileges.secretmanager.SecretManagerClient.SecretManagerException;
import google.registry.util.Retrier;
import google.registry.util.SystemSleeper;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
@ -43,52 +48,50 @@ public class SecretManagerClientTest {
private static final String SECRET_ID_PREFIX = "TEST_" + UUID.randomUUID() + "_";
// Used for unique secret id generation.
private static int seqno = 0;
private static SecretManagerClient secretManagerClient;
private static boolean isUnitTest = true;
private static String nextSecretId() {
return SECRET_ID_PREFIX + seqno++;
}
private String secretId;
@BeforeAll
static void beforeAll() {
String environmentName = System.getProperty("test.gcp_integration.env");
if (environmentName != null) {
secretManagerClient =
DaggerSecretManagerModule_SecretManagerComponent.builder()
.secretManagerModule(
new SecretManagerModule(String.format("domain-registry-%s", environmentName)))
.build()
.secretManagerClient();
SecretManagerModule.provideSecretManagerClient(
String.format("domain-registry-%s", environmentName),
new Retrier(new SystemSleeper(), 1));
isUnitTest = false;
} else {
secretManagerClient = new FakeSecretManagerClient();
}
}
@AfterAll
static void afterAll() {
@BeforeEach
void beforeEach() {
secretId = SECRET_ID_PREFIX + seqno++;
}
@AfterEach
void afterEach() throws IOException {
if (isUnitTest) {
return;
}
for (String secretId : secretManagerClient.listSecrets()) {
if (secretId.startsWith(SECRET_ID_PREFIX)) {
secretManagerClient.deleteSecret(secretId);
}
try {
secretManagerClient.deleteSecret(secretId);
} catch (NoSuchSecretResourceException e) {
// deleteSecret() deleted it already.
}
}
@Test
void createSecret_success() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
assertThat(secretManagerClient.listSecrets()).contains(secretId);
}
@Test
void createSecret_duplicate() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
assertThrows(
SecretAlreadyExistsException.class, () -> secretManagerClient.createSecret(secretId));
@ -96,16 +99,25 @@ public class SecretManagerClientTest {
@Test
void addSecretVersion() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
assertThat(secretManagerClient.listSecretVersions(secretId, State.ENABLED))
.containsExactly(version);
}
@Test
void secretExists_true() {
secretManagerClient.createSecret(secretId);
assertThat(secretManagerClient.secretExists(secretId)).isTrue();
}
@Test
void secretExists_False() {
assertThat(secretManagerClient.secretExists(secretId)).isFalse();
}
@Test
void getSecretData_byVersion() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
assertThat(secretManagerClient.getSecretData(secretId, Optional.of(version)))
@ -114,15 +126,70 @@ public class SecretManagerClientTest {
@Test
void getSecretData_latestVersion() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
secretManagerClient.addSecretVersion(secretId, "mydata");
assertThat(secretManagerClient.getSecretData(secretId, Optional.empty())).isEqualTo("mydata");
}
@Test
void disableSecretVersion() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.disableSecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.DISABLED)).contains(version);
}
@Test
void disableSecretVersion_ignoreAlreadyDisabled() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.disableSecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.DISABLED)).contains(version);
secretManagerClient.disableSecretVersion(secretId, version);
}
@Test
void disableSecretVersion_destroyed() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.destroySecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.DESTROYED)).contains(version);
assertThrows(
SecretManagerException.class,
() -> secretManagerClient.disableSecretVersion(secretId, version));
}
@Test
void enableSecretVersion_ignoreAlreadyEnabled() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
assertThat(secretManagerClient.listSecretVersions(secretId, State.ENABLED)).contains(version);
secretManagerClient.enableSecretVersion(secretId, version);
}
@Test
void enableSecretVersion() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.disableSecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.DISABLED)).contains(version);
secretManagerClient.enableSecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.ENABLED)).contains(version);
}
@Test
void enableSecretVersion_destroyed() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.destroySecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.DESTROYED)).contains(version);
assertThrows(
SecretManagerException.class,
() -> secretManagerClient.enableSecretVersion(secretId, version));
}
@Test
void destroySecretVersion() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.destroySecretVersion(secretId, version);
@ -134,7 +201,6 @@ public class SecretManagerClientTest {
@Test
void deleteSecret() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
assertThat(secretManagerClient.listSecrets()).contains(secretId);
secretManagerClient.deleteSecret(secretId);

View file

@ -0,0 +1,63 @@
// 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.truth.Truth.assertThat;
import com.google.cloud.secretmanager.v1.SecretVersionName;
import google.registry.privileges.secretmanager.SqlUser.RobotId;
import google.registry.privileges.secretmanager.SqlUser.RobotUser;
import java.util.Optional;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link SqlCredentialStore}. */
public class SqlCredentialStoreTest {
private final SecretManagerClient client = new FakeSecretManagerClient();
private final SqlCredentialStore credentialStore = new SqlCredentialStore(client, "db");
private SqlUser user = new RobotUser(RobotId.NOMULUS);
@Test
void createSecret() {
credentialStore.createOrUpdateCredential(user, "password");
assertThat(client.secretExists("sql-cred-live-label-nomulus-db")).isTrue();
assertThat(
SecretVersionName.parse(
client.getSecretData("sql-cred-live-label-nomulus-db", Optional.empty()))
.getSecret())
.isEqualTo("sql-cred-data-nomulus-db");
assertThat(client.secretExists("sql-cred-data-nomulus-db")).isTrue();
assertThat(client.getSecretData("sql-cred-data-nomulus-db", Optional.empty()))
.isEqualTo("nomulus password");
}
@Test
void getCredential() {
credentialStore.createOrUpdateCredential(user, "password");
SqlCredential credential = credentialStore.getCredential(user);
assertThat(credential.login()).isEqualTo("nomulus");
assertThat(credential.password()).isEqualTo("password");
}
@Test
void deleteCredential() {
credentialStore.createOrUpdateCredential(user, "password");
assertThat(client.secretExists("sql-cred-live-label-nomulus-db")).isTrue();
assertThat(client.secretExists("sql-cred-data-nomulus-db")).isTrue();
credentialStore.deleteCredential(user);
assertThat(client.secretExists("sql-cred-live-label-nomulus-db")).isFalse();
assertThat(client.secretExists("sql-cred-data-nomulus-db")).isFalse();
}
}