mirror of
https://github.com/google/nomulus.git
synced 2025-07-08 20:23:24 +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
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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