Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a flag to control whether credentials are printed during bootstrapping #461

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,28 @@
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;

/**
* A {@link PrincipalSecretsGenerator} implementation used for bootstrapping that uses an {@link
* EnvVariablePrincipalSecretsGenerator} if possible and falls back to a {@link
* RandomPrincipalSecretsGenerator} otherwise
*/
public class BootstrapPrincipalSecretsGenerator extends PrincipalSecretsGenerator {

public BootstrapPrincipalSecretsGenerator(@Nullable String realmName) {
super(realmName);
}

private PrincipalSecretsGenerator getDelegate(
@Nullable String realmName, @NotNull String principalName) {
var envVarGenerator = new EnvVariablePrincipalSecretsGenerator(realmName);
if (envVarGenerator.systemGeneratedSecrets(principalName)) {
@VisibleForTesting
protected PrincipalSecretsGenerator buildEnvVariablePrincipalSecretsGenerator(String realmName) {
return new EnvVariablePrincipalSecretsGenerator(realmName);
}

@VisibleForTesting
protected PrincipalSecretsGenerator getDelegate(@NotNull String principalName) {
var envVarGenerator = buildEnvVariablePrincipalSecretsGenerator(principalName);
if (!envVarGenerator.systemGeneratedSecrets(principalName)) {
return new RandomPrincipalSecretsGenerator(realmName);
} else {
return envVarGenerator;
Expand All @@ -40,13 +51,13 @@ private PrincipalSecretsGenerator getDelegate(

@Override
public PolarisPrincipalSecrets produceSecrets(@NotNull String principalName, long principalId) {
PrincipalSecretsGenerator delegate = getDelegate(realmName, principalName);
PrincipalSecretsGenerator delegate = getDelegate(principalName);
return delegate.produceSecrets(principalName, principalId);
}

@Override
public boolean systemGeneratedSecrets(@NotNull String principalName) {
PrincipalSecretsGenerator delegate = getDelegate(realmName, principalName);
PrincipalSecretsGenerator delegate = getDelegate(principalName);
return delegate.systemGeneratedSecrets(principalName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;

public class EnvVariablePrincipalSecretsGenerator extends PrincipalSecretsGenerator {

Expand Down Expand Up @@ -53,7 +54,8 @@ public boolean systemGeneratedSecrets(@NotNull String principalName) {
}

/** Load a single environment variable */
private static String getEnvironmentVariable(String key) {
@VisibleForTesting
String getEnvironmentVariable(String key) {
return System.getenv(key);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ public RandomPrincipalSecretsGenerator(@Nullable String realmName) {
super(realmName);
}

public RandomPrincipalSecretsGenerator() {
super(null);
}

/** {@inheritDoc} */
@Override
public PolarisPrincipalSecrets produceSecrets(@NotNull String principalName, long principalId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
*/
package org.apache.polaris.core.persistence;

import static org.apache.polaris.core.persistence.secrets.PrincipalSecretsGenerator.RANDOM_SECRETS;

import java.util.List;
import java.util.stream.Collectors;
import org.apache.polaris.core.PolarisCallContext;
Expand All @@ -34,6 +32,7 @@
import org.apache.polaris.core.persistence.cache.EntityCacheByNameKey;
import org.apache.polaris.core.persistence.cache.EntityCacheEntry;
import org.apache.polaris.core.persistence.cache.EntityCacheLookupResult;
import org.apache.polaris.core.persistence.secrets.RandomPrincipalSecretsGenerator;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
Expand Down Expand Up @@ -84,7 +83,9 @@ public class EntityCacheTest {
public EntityCacheTest() {
diagServices = new PolarisDefaultDiagServiceImpl();
store = new PolarisTreeMapStore(diagServices);
metaStore = new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock(), RANDOM_SECRETS);
metaStore =
new PolarisTreeMapMetaStoreSessionImpl(
store, Mockito.mock(), new RandomPrincipalSecretsGenerator());
callCtx = new PolarisCallContext(metaStore, diagServices);
metaStoreManager = new PolarisMetaStoreManagerImpl();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@
*/
package org.apache.polaris.core.persistence;

import static org.apache.polaris.core.persistence.secrets.PrincipalSecretsGenerator.RANDOM_SECRETS;

import java.time.ZoneId;
import org.apache.polaris.core.PolarisCallContext;
import org.apache.polaris.core.PolarisConfigurationStore;
import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
import org.apache.polaris.core.PolarisDiagnostics;
import org.apache.polaris.core.persistence.secrets.RandomPrincipalSecretsGenerator;
import org.mockito.Mockito;

public class PolarisTreeMapMetaStoreManagerTest extends BasePolarisMetaStoreManagerTest {
Expand All @@ -34,7 +33,8 @@ public PolarisTestMetaStoreManager createPolarisTestMetaStoreManager() {
PolarisTreeMapStore store = new PolarisTreeMapStore(diagServices);
PolarisCallContext callCtx =
new PolarisCallContext(
new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock(), RANDOM_SECRETS),
new PolarisTreeMapMetaStoreSessionImpl(
store, Mockito.mock(), new RandomPrincipalSecretsGenerator()),
diagServices,
new PolarisConfigurationStore() {},
timeSource.withZone(ZoneId.systemDefault()));
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
*/
package org.apache.polaris.core.persistence;

import static org.apache.polaris.core.persistence.secrets.PrincipalSecretsGenerator.RANDOM_SECRETS;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
Expand All @@ -41,6 +39,7 @@
import org.apache.polaris.core.persistence.resolver.Resolver;
import org.apache.polaris.core.persistence.resolver.ResolverPath;
import org.apache.polaris.core.persistence.resolver.ResolverStatus;
import org.apache.polaris.core.persistence.secrets.RandomPrincipalSecretsGenerator;
import org.assertj.core.api.Assertions;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -98,7 +97,9 @@ public class ResolverTest {
public ResolverTest() {
diagServices = new PolarisDefaultDiagServiceImpl();
store = new PolarisTreeMapStore(diagServices);
metaStore = new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock(), RANDOM_SECRETS);
metaStore =
new PolarisTreeMapMetaStoreSessionImpl(
store, Mockito.mock(), new RandomPrincipalSecretsGenerator());
callCtx = new PolarisCallContext(metaStore, diagServices);
metaStoreManager = new PolarisMetaStoreManagerImpl();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.polaris.core.persistence.secrets;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;

import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class PrincipalSecretsGeneratorTest {

@Test
void testRandomSecrets() {
RandomPrincipalSecretsGenerator rpsg = new RandomPrincipalSecretsGenerator("realm");
PolarisPrincipalSecrets s = rpsg.produceSecrets("name1", 123);
assertThat(s).isNotNull();
assertThat(s.getPrincipalId()).isEqualTo(123);
assertThat(s.getPrincipalClientId()).isNotNull();
assertThat(s.getMainSecret()).isNotNull();
assertThat(s.getSecondarySecret()).isNotNull();
}

@Test
void testRandomSecretsNullRealm() {
RandomPrincipalSecretsGenerator rpsg = new RandomPrincipalSecretsGenerator(null);
PolarisPrincipalSecrets s = rpsg.produceSecrets("name1", 123);
assertThat(s).isNotNull();
assertThat(s.getPrincipalId()).isEqualTo(123);
assertThat(s.getPrincipalClientId()).isNotNull();
assertThat(s.getMainSecret()).isNotNull();
assertThat(s.getSecondarySecret()).isNotNull();
}

@Test
void testEnvVariableSecrets() {
EnvVariablePrincipalSecretsGenerator psg =
Mockito.spy(new EnvVariablePrincipalSecretsGenerator("REALM"));

String clientIdKey = "POLARIS_BOOTSTRAP_REALM_PRINCIPAL_CLIENT_ID";
String clientSecretKey = "POLARIS_BOOTSTRAP_REALM_PRINCIPAL_CLIENT_SECRET";

doReturn("test-id").when(psg).getEnvironmentVariable(clientIdKey);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nicer to allow EnvVariablePrincipalSecretsGenerator to take an explicit Map (or a Function<String, String>) as a constructor argument. The the "environment" part cab be just a static factory method like EnvVariablePrincipalSecretsGenerator.fromEnv().

Testing the static method would not really be necessary because it would be a one-line redirect to System.env(), but it would allow for nicer design that is testable without Mockito.

Just my 2 cents :)

Copy link
Contributor Author

@eric-maynard eric-maynard Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea, but my concern is that EnvVariablePrincipalSecretsGenerator essentially becomes MapPrincipalSecretsGenerator with that approach. I would rather not place the burden on its callers to do the fromEnv / getEnv; this seems like the exact responsibility we would like EnvVariablePrincipalSecretsGenerator to take on.

For example: if we later add command-line options to bootstrap that allow you to provide credentials, would we use the EnvVariablePrincipalSecretsGenerator for that? If it takes a map, we could.

Taking this further, we could even pass in a map with random secrets to EnvVariablePrincipalSecretsGenerator!

And so its role, as well as that of RandomPrincipalSecretsGenerator, can quickly become quite unclear

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I'm fine with the current code in this PR.

doReturn("test-secret").when(psg).getEnvironmentVariable(clientSecretKey);

// Invoke the method
PolarisPrincipalSecrets secrets = psg.produceSecrets("PRINCIPAL", 123);

// Verify the result
Assertions.assertNotNull(secrets);
Assertions.assertEquals(123, secrets.getPrincipalId());
Assertions.assertEquals("test-id", secrets.getPrincipalClientId());
Assertions.assertEquals("test-secret", secrets.getMainSecret());
}

@Test
void testBoostrapGeneratorDelegationToRandomPrincipalSecrets() {
EnvVariablePrincipalSecretsGenerator mockedEnvVariablePrincipalSecretsGenerator =
Mockito.spy(new EnvVariablePrincipalSecretsGenerator("REALM"));

String clientIdKey = "POLARIS_BOOTSTRAP_REALM_PRINCIPAL_CLIENT_ID";
String clientSecretKey = "POLARIS_BOOTSTRAP_REALM_PRINCIPAL_CLIENT_SECRET";

doReturn("test-id")
.when(mockedEnvVariablePrincipalSecretsGenerator)
.getEnvironmentVariable(clientIdKey);
doReturn("test-secret")
.when(mockedEnvVariablePrincipalSecretsGenerator)
.getEnvironmentVariable(clientSecretKey);

class ExposingPrincipalSecretsGenerator extends BootstrapPrincipalSecretsGenerator {
public ExposingPrincipalSecretsGenerator(@Nullable String realmName) {
super(realmName);
}

@Override
protected PrincipalSecretsGenerator buildEnvVariablePrincipalSecretsGenerator(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here: I think we could have a simple factory that bind to System.env() in runtime, but use explicit constructor parameters in tests... This will remove reliance on Mockito.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case what I think we really want is DI 😔

String realmName) {
return mockedEnvVariablePrincipalSecretsGenerator;
}

public PrincipalSecretsGenerator seeDelegate(String principalName) {
return this.getDelegate(principalName);
}
}

ExposingPrincipalSecretsGenerator fallback = new ExposingPrincipalSecretsGenerator(null);
Assertions.assertInstanceOf(RandomPrincipalSecretsGenerator.class, fallback.seeDelegate("p"));

ExposingPrincipalSecretsGenerator hasVars = new ExposingPrincipalSecretsGenerator("REALM");
Assertions.assertInstanceOf(
EnvVariablePrincipalSecretsGenerator.class, hasVars.seeDelegate("PRINCIPAL"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
*/
package org.apache.polaris.core.storage.cache;

import static org.apache.polaris.core.persistence.secrets.PrincipalSecretsGenerator.RANDOM_SECRETS;

import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -43,6 +41,7 @@
import org.apache.polaris.core.persistence.PolarisObjectMapperUtil;
import org.apache.polaris.core.persistence.PolarisTreeMapMetaStoreSessionImpl;
import org.apache.polaris.core.persistence.PolarisTreeMapStore;
import org.apache.polaris.core.persistence.secrets.RandomPrincipalSecretsGenerator;
import org.apache.polaris.core.storage.PolarisCredentialProperty;
import org.apache.polaris.core.storage.PolarisCredentialVendor.ScopedCredentialsResult;
import org.assertj.core.api.Assertions;
Expand All @@ -68,7 +67,8 @@ public StorageCredentialCacheTest() {
PolarisTreeMapStore store = new PolarisTreeMapStore(diagServices);
// to interact with the metastore
PolarisMetaStoreSession metaStore =
new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock(), RANDOM_SECRETS);
new PolarisTreeMapMetaStoreSessionImpl(
store, Mockito.mock(), new RandomPrincipalSecretsGenerator());
callCtx = new PolarisCallContext(metaStore, diagServices);
metaStoreManager = Mockito.mock(PolarisMetaStoreManagerImpl.class);
storageCredentialCache = new StorageCredentialCache();
Expand Down
Loading