mirror of
https://github.com/google/nomulus.git
synced 2025-07-09 04:33:28 +02:00
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:
parent
2c6ee6dae9
commit
83ed448741
14 changed files with 675 additions and 48 deletions
|
@ -415,6 +415,14 @@ public final class RegistryConfig {
|
||||||
return config.cloudSql.instanceConnectionName;
|
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
|
@Provides
|
||||||
@Config("cloudDnsRootUrl")
|
@Config("cloudDnsRootUrl")
|
||||||
public static Optional<String> getCloudDnsRootUrl(RegistryConfigSettings config) {
|
public static Optional<String> getCloudDnsRootUrl(RegistryConfigSettings config) {
|
||||||
|
|
|
@ -22,6 +22,9 @@ import java.util.Optional;
|
||||||
/** A Cloud Secret Manager client for Nomulus, bound to a specific GCP project. */
|
/** A Cloud Secret Manager client for Nomulus, bound to a specific GCP project. */
|
||||||
public interface SecretManagerClient {
|
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.
|
* Creates a new secret in the Cloud Secret Manager with no data.
|
||||||
*
|
*
|
||||||
|
@ -32,6 +35,9 @@ public interface SecretManagerClient {
|
||||||
*/
|
*/
|
||||||
void createSecret(String secretId);
|
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. */
|
/** Returns all secret IDs in the Cloud Secret Manager. */
|
||||||
Iterable<String> listSecrets();
|
Iterable<String> listSecrets();
|
||||||
|
|
||||||
|
@ -67,6 +73,24 @@ public interface SecretManagerClient {
|
||||||
*/
|
*/
|
||||||
String getSecretData(String secretId, Optional<String> version);
|
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.
|
* Destroys a secret version.
|
||||||
*
|
*
|
||||||
|
|
|
@ -29,12 +29,11 @@ import com.google.cloud.secretmanager.v1.SecretName;
|
||||||
import com.google.cloud.secretmanager.v1.SecretPayload;
|
import com.google.cloud.secretmanager.v1.SecretPayload;
|
||||||
import com.google.cloud.secretmanager.v1.SecretVersion;
|
import com.google.cloud.secretmanager.v1.SecretVersion;
|
||||||
import com.google.cloud.secretmanager.v1.SecretVersionName;
|
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 com.google.protobuf.ByteString;
|
||||||
import google.registry.util.Retrier;
|
import google.registry.util.Retrier;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
/** Implements {@link SecretManagerClient} on Google Cloud Platform. */
|
/** Implements {@link SecretManagerClient} on Google Cloud Platform. */
|
||||||
public class SecretManagerClientImpl implements SecretManagerClient {
|
public class SecretManagerClientImpl implements SecretManagerClient {
|
||||||
|
@ -42,13 +41,17 @@ public class SecretManagerClientImpl implements SecretManagerClient {
|
||||||
private final SecretManagerServiceClient csmClient;
|
private final SecretManagerServiceClient csmClient;
|
||||||
private final Retrier retrier;
|
private final Retrier retrier;
|
||||||
|
|
||||||
@Inject
|
|
||||||
SecretManagerClientImpl(String project, SecretManagerServiceClient csmClient, Retrier retrier) {
|
SecretManagerClientImpl(String project, SecretManagerServiceClient csmClient, Retrier retrier) {
|
||||||
this.project = project;
|
this.project = project;
|
||||||
this.csmClient = csmClient;
|
this.csmClient = csmClient;
|
||||||
this.retrier = retrier;
|
this.retrier = retrier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProject() {
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createSecret(String secretId) {
|
public void createSecret(String secretId) {
|
||||||
checkNotNull(secretId, "secretId");
|
checkNotNull(secretId, "secretId");
|
||||||
|
@ -57,12 +60,25 @@ public class SecretManagerClientImpl implements SecretManagerClient {
|
||||||
() -> csmClient.createSecret(ProjectName.of(project), secretId, secretSettings));
|
() -> 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
|
@Override
|
||||||
public Iterable<String> listSecrets() {
|
public Iterable<String> listSecrets() {
|
||||||
ListSecretsPagedResponse response =
|
ListSecretsPagedResponse response =
|
||||||
callSecretManager(() -> csmClient.listSecrets(ProjectName.of(project)));
|
callSecretManager(() -> csmClient.listSecrets(ProjectName.of(project)));
|
||||||
return Iterables.transform(
|
return () ->
|
||||||
response.iterateAll(), secret -> SecretName.parse(secret.getName()).getSecret());
|
Streams.stream(response.iterateAll())
|
||||||
|
.map(secret -> SecretName.parse(secret.getName()).getSecret())
|
||||||
|
.iterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -70,8 +86,10 @@ public class SecretManagerClientImpl implements SecretManagerClient {
|
||||||
checkNotNull(secretId, "secretId");
|
checkNotNull(secretId, "secretId");
|
||||||
ListSecretVersionsPagedResponse response =
|
ListSecretVersionsPagedResponse response =
|
||||||
callSecretManager(() -> csmClient.listSecretVersions(SecretName.of(project, secretId)));
|
callSecretManager(() -> csmClient.listSecretVersions(SecretName.of(project, secretId)));
|
||||||
return Iterables.transform(
|
return () ->
|
||||||
response.iterateAll(), SecretManagerClientImpl::toSecretVersionState);
|
Streams.stream(response.iterateAll())
|
||||||
|
.map(SecretManagerClientImpl::toSecretVersionState)
|
||||||
|
.iterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SecretVersionState toSecretVersionState(SecretVersion secretVersion) {
|
private static SecretVersionState toSecretVersionState(SecretVersion secretVersion) {
|
||||||
|
@ -108,6 +126,22 @@ public class SecretManagerClientImpl implements SecretManagerClient {
|
||||||
.toStringUtf8());
|
.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
|
@Override
|
||||||
public void destroySecretVersion(String secretId, String version) {
|
public void destroySecretVersion(String secretId, String version) {
|
||||||
checkNotNull(secretId, "secretId");
|
checkNotNull(secretId, "secretId");
|
||||||
|
|
|
@ -14,12 +14,12 @@
|
||||||
|
|
||||||
package google.registry.privileges.secretmanager;
|
package google.registry.privileges.secretmanager;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
|
||||||
|
|
||||||
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
|
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
|
||||||
import dagger.Component;
|
import dagger.Component;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
import google.registry.config.RegistryConfig.Config;
|
||||||
import google.registry.config.RegistryConfig.ConfigModule;
|
import google.registry.config.RegistryConfig.ConfigModule;
|
||||||
import google.registry.util.Retrier;
|
import google.registry.util.Retrier;
|
||||||
import google.registry.util.UtilsModule;
|
import google.registry.util.UtilsModule;
|
||||||
|
@ -28,19 +28,16 @@ import javax.inject.Singleton;
|
||||||
|
|
||||||
/** Provides bindings for {@link SecretManagerClient}. */
|
/** Provides bindings for {@link SecretManagerClient}. */
|
||||||
@Module
|
@Module
|
||||||
public class SecretManagerModule {
|
public abstract class SecretManagerModule {
|
||||||
|
|
||||||
private final String project;
|
|
||||||
|
|
||||||
public SecretManagerModule(String project) {
|
|
||||||
this.project = checkNotNull(project, "project");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
SecretManagerClient provideSecretManagerClient(Retrier retrier) {
|
static SecretManagerClient provideSecretManagerClient(
|
||||||
|
@Config("projectId") String project, Retrier retrier) {
|
||||||
try {
|
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) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,6 +78,7 @@ public final class RegistryTool {
|
||||||
.put("get_routing_map", GetRoutingMapCommand.class)
|
.put("get_routing_map", GetRoutingMapCommand.class)
|
||||||
.put("get_schema", GetSchemaCommand.class)
|
.put("get_schema", GetSchemaCommand.class)
|
||||||
.put("get_schema_tree", GetSchemaTreeCommand.class)
|
.put("get_schema_tree", GetSchemaTreeCommand.class)
|
||||||
|
.put("get_sql_credential", GetSqlCredentialCommand.class)
|
||||||
.put("get_tld", GetTldCommand.class)
|
.put("get_tld", GetTldCommand.class)
|
||||||
.put("ghostryde", GhostrydeCommand.class)
|
.put("ghostryde", GhostrydeCommand.class)
|
||||||
.put("hash_certificate", HashCertificateCommand.class)
|
.put("hash_certificate", HashCertificateCommand.class)
|
||||||
|
@ -104,6 +105,7 @@ public final class RegistryTool {
|
||||||
.put("resave_entities", ResaveEntitiesCommand.class)
|
.put("resave_entities", ResaveEntitiesCommand.class)
|
||||||
.put("resave_environment_entities", ResaveEnvironmentEntitiesCommand.class)
|
.put("resave_environment_entities", ResaveEnvironmentEntitiesCommand.class)
|
||||||
.put("resave_epp_resource", ResaveEppResourceCommand.class)
|
.put("resave_epp_resource", ResaveEppResourceCommand.class)
|
||||||
|
.put("save_sql_credential", SaveSqlCredentialCommand.class)
|
||||||
.put("send_escrow_report_to_icann", SendEscrowReportToIcannCommand.class)
|
.put("send_escrow_report_to_icann", SendEscrowReportToIcannCommand.class)
|
||||||
.put("set_num_instances", SetNumInstancesCommand.class)
|
.put("set_num_instances", SetNumInstancesCommand.class)
|
||||||
.put("setup_ote", SetupOteCommand.class)
|
.put("setup_ote", SetupOteCommand.class)
|
||||||
|
|
|
@ -34,6 +34,7 @@ import google.registry.keyring.kms.KmsModule;
|
||||||
import google.registry.persistence.PersistenceModule;
|
import google.registry.persistence.PersistenceModule;
|
||||||
import google.registry.persistence.PersistenceModule.NomulusToolJpaTm;
|
import google.registry.persistence.PersistenceModule.NomulusToolJpaTm;
|
||||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||||
|
import google.registry.privileges.secretmanager.SecretManagerModule;
|
||||||
import google.registry.rde.RdeModule;
|
import google.registry.rde.RdeModule;
|
||||||
import google.registry.request.Modules.DatastoreServiceModule;
|
import google.registry.request.Modules.DatastoreServiceModule;
|
||||||
import google.registry.request.Modules.Jackson2Module;
|
import google.registry.request.Modules.Jackson2Module;
|
||||||
|
@ -74,6 +75,7 @@ import javax.inject.Singleton;
|
||||||
PersistenceModule.class,
|
PersistenceModule.class,
|
||||||
RdeModule.class,
|
RdeModule.class,
|
||||||
RequestFactoryModule.class,
|
RequestFactoryModule.class,
|
||||||
|
SecretManagerModule.class,
|
||||||
URLFetchServiceModule.class,
|
URLFetchServiceModule.class,
|
||||||
UrlFetchTransportModule.class,
|
UrlFetchTransportModule.class,
|
||||||
UserServiceModule.class,
|
UserServiceModule.class,
|
||||||
|
@ -118,6 +120,8 @@ interface RegistryToolComponent {
|
||||||
|
|
||||||
void inject(GetOperationStatusCommand command);
|
void inject(GetOperationStatusCommand command);
|
||||||
|
|
||||||
|
void inject(GetSqlCredentialCommand command);
|
||||||
|
|
||||||
void inject(GhostrydeCommand command);
|
void inject(GhostrydeCommand command);
|
||||||
|
|
||||||
void inject(ImportDatastoreCommand command);
|
void inject(ImportDatastoreCommand command);
|
||||||
|
@ -138,6 +142,8 @@ interface RegistryToolComponent {
|
||||||
|
|
||||||
void inject(RenewDomainCommand command);
|
void inject(RenewDomainCommand command);
|
||||||
|
|
||||||
|
void inject(SaveSqlCredentialCommand command);
|
||||||
|
|
||||||
void inject(SendEscrowReportToIcannCommand command);
|
void inject(SendEscrowReportToIcannCommand command);
|
||||||
|
|
||||||
void inject(SetNumInstancesCommand command);
|
void inject(SetNumInstancesCommand command);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,11 @@ public class FakeSecretManagerClient implements SecretManagerClient {
|
||||||
@Inject
|
@Inject
|
||||||
FakeSecretManagerClient() {}
|
FakeSecretManagerClient() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProject() {
|
||||||
|
return "fake_project";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createSecret(String secretId) {
|
public void createSecret(String secretId) {
|
||||||
checkNotNull(secretId, "secretId");
|
checkNotNull(secretId, "secretId");
|
||||||
|
@ -41,6 +46,12 @@ public class FakeSecretManagerClient implements SecretManagerClient {
|
||||||
secrets.put(secretId, new SecretEntry(secretId));
|
secrets.put(secretId, new SecretEntry(secretId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean secretExists(String secretId) {
|
||||||
|
checkNotNull(secretId, "secretId");
|
||||||
|
return secrets.containsKey(secretId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Iterable<String> listSecrets() {
|
public Iterable<String> listSecrets() {
|
||||||
return ImmutableSet.copyOf(secrets.keySet());
|
return ImmutableSet.copyOf(secrets.keySet());
|
||||||
|
@ -78,6 +89,28 @@ public class FakeSecretManagerClient implements SecretManagerClient {
|
||||||
return secretEntry.getVersion(version).getData();
|
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
|
@Override
|
||||||
public void destroySecretVersion(String secretId, String version) {
|
public void destroySecretVersion(String secretId, String version) {
|
||||||
checkNotNull(secretId, "secretId");
|
checkNotNull(secretId, "secretId");
|
||||||
|
@ -118,6 +151,20 @@ public class FakeSecretManagerClient implements SecretManagerClient {
|
||||||
return state;
|
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() {
|
void destroy() {
|
||||||
data = null;
|
data = null;
|
||||||
state = State.DESTROYED;
|
state = State.DESTROYED;
|
||||||
|
@ -145,6 +192,8 @@ public class FakeSecretManagerClient implements SecretManagerClient {
|
||||||
return versions.get(index);
|
return versions.get(index);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
throw new IllegalArgumentException("Invalid version " + version.get());
|
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();
|
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) {
|
void destroyVersion(String version) {
|
||||||
try {
|
getVersion(Optional.of(version)).destroy();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,17 @@ import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
import com.google.cloud.secretmanager.v1.SecretVersion.State;
|
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.SecretAlreadyExistsException;
|
||||||
import google.registry.privileges.secretmanager.SecretManagerClient.SecretManagerException;
|
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.Optional;
|
||||||
import java.util.UUID;
|
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.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,52 +48,50 @@ public class SecretManagerClientTest {
|
||||||
private static final String SECRET_ID_PREFIX = "TEST_" + UUID.randomUUID() + "_";
|
private static final String SECRET_ID_PREFIX = "TEST_" + UUID.randomUUID() + "_";
|
||||||
// Used for unique secret id generation.
|
// Used for unique secret id generation.
|
||||||
private static int seqno = 0;
|
private static int seqno = 0;
|
||||||
|
|
||||||
private static SecretManagerClient secretManagerClient;
|
private static SecretManagerClient secretManagerClient;
|
||||||
private static boolean isUnitTest = true;
|
private static boolean isUnitTest = true;
|
||||||
|
|
||||||
private static String nextSecretId() {
|
private String secretId;
|
||||||
return SECRET_ID_PREFIX + seqno++;
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
static void beforeAll() {
|
static void beforeAll() {
|
||||||
String environmentName = System.getProperty("test.gcp_integration.env");
|
String environmentName = System.getProperty("test.gcp_integration.env");
|
||||||
if (environmentName != null) {
|
if (environmentName != null) {
|
||||||
secretManagerClient =
|
secretManagerClient =
|
||||||
DaggerSecretManagerModule_SecretManagerComponent.builder()
|
SecretManagerModule.provideSecretManagerClient(
|
||||||
.secretManagerModule(
|
String.format("domain-registry-%s", environmentName),
|
||||||
new SecretManagerModule(String.format("domain-registry-%s", environmentName)))
|
new Retrier(new SystemSleeper(), 1));
|
||||||
.build()
|
|
||||||
.secretManagerClient();
|
|
||||||
isUnitTest = false;
|
isUnitTest = false;
|
||||||
} else {
|
} else {
|
||||||
secretManagerClient = new FakeSecretManagerClient();
|
secretManagerClient = new FakeSecretManagerClient();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
@BeforeEach
|
||||||
static void afterAll() {
|
void beforeEach() {
|
||||||
|
secretId = SECRET_ID_PREFIX + seqno++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void afterEach() throws IOException {
|
||||||
if (isUnitTest) {
|
if (isUnitTest) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (String secretId : secretManagerClient.listSecrets()) {
|
try {
|
||||||
if (secretId.startsWith(SECRET_ID_PREFIX)) {
|
secretManagerClient.deleteSecret(secretId);
|
||||||
secretManagerClient.deleteSecret(secretId);
|
} catch (NoSuchSecretResourceException e) {
|
||||||
}
|
// deleteSecret() deleted it already.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createSecret_success() {
|
void createSecret_success() {
|
||||||
String secretId = nextSecretId();
|
|
||||||
secretManagerClient.createSecret(secretId);
|
secretManagerClient.createSecret(secretId);
|
||||||
assertThat(secretManagerClient.listSecrets()).contains(secretId);
|
assertThat(secretManagerClient.listSecrets()).contains(secretId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createSecret_duplicate() {
|
void createSecret_duplicate() {
|
||||||
String secretId = nextSecretId();
|
|
||||||
secretManagerClient.createSecret(secretId);
|
secretManagerClient.createSecret(secretId);
|
||||||
assertThrows(
|
assertThrows(
|
||||||
SecretAlreadyExistsException.class, () -> secretManagerClient.createSecret(secretId));
|
SecretAlreadyExistsException.class, () -> secretManagerClient.createSecret(secretId));
|
||||||
|
@ -96,16 +99,25 @@ public class SecretManagerClientTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void addSecretVersion() {
|
void addSecretVersion() {
|
||||||
String secretId = nextSecretId();
|
|
||||||
secretManagerClient.createSecret(secretId);
|
secretManagerClient.createSecret(secretId);
|
||||||
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
|
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
|
||||||
assertThat(secretManagerClient.listSecretVersions(secretId, State.ENABLED))
|
assertThat(secretManagerClient.listSecretVersions(secretId, State.ENABLED))
|
||||||
.containsExactly(version);
|
.containsExactly(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void secretExists_true() {
|
||||||
|
secretManagerClient.createSecret(secretId);
|
||||||
|
assertThat(secretManagerClient.secretExists(secretId)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void secretExists_False() {
|
||||||
|
assertThat(secretManagerClient.secretExists(secretId)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSecretData_byVersion() {
|
void getSecretData_byVersion() {
|
||||||
String secretId = nextSecretId();
|
|
||||||
secretManagerClient.createSecret(secretId);
|
secretManagerClient.createSecret(secretId);
|
||||||
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
|
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
|
||||||
assertThat(secretManagerClient.getSecretData(secretId, Optional.of(version)))
|
assertThat(secretManagerClient.getSecretData(secretId, Optional.of(version)))
|
||||||
|
@ -114,15 +126,70 @@ public class SecretManagerClientTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSecretData_latestVersion() {
|
void getSecretData_latestVersion() {
|
||||||
String secretId = nextSecretId();
|
|
||||||
secretManagerClient.createSecret(secretId);
|
secretManagerClient.createSecret(secretId);
|
||||||
secretManagerClient.addSecretVersion(secretId, "mydata");
|
secretManagerClient.addSecretVersion(secretId, "mydata");
|
||||||
assertThat(secretManagerClient.getSecretData(secretId, Optional.empty())).isEqualTo("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
|
@Test
|
||||||
void destroySecretVersion() {
|
void destroySecretVersion() {
|
||||||
String secretId = nextSecretId();
|
|
||||||
secretManagerClient.createSecret(secretId);
|
secretManagerClient.createSecret(secretId);
|
||||||
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
|
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
|
||||||
secretManagerClient.destroySecretVersion(secretId, version);
|
secretManagerClient.destroySecretVersion(secretId, version);
|
||||||
|
@ -134,7 +201,6 @@ public class SecretManagerClientTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void deleteSecret() {
|
void deleteSecret() {
|
||||||
String secretId = nextSecretId();
|
|
||||||
secretManagerClient.createSecret(secretId);
|
secretManagerClient.createSecret(secretId);
|
||||||
assertThat(secretManagerClient.listSecrets()).contains(secretId);
|
assertThat(secretManagerClient.listSecrets()).contains(secretId);
|
||||||
secretManagerClient.deleteSecret(secretId);
|
secretManagerClient.deleteSecret(secretId);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue