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

Support providing root client ID via env. variables when bootstrapping #422

Merged
merged 3 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ protected PolarisEclipseLinkStore createBackingStore(@NotNull PolarisDiagnostics
protected PolarisMetaStoreSession createMetaStoreSession(
@NotNull PolarisEclipseLinkStore store, @NotNull RealmContext realmContext) {
return new PolarisEclipseLinkMetaStoreSessionImpl(
store, storageIntegration, realmContext, confFile, persistenceUnitName);
store,
storageIntegration,
realmContext,
confFile,
persistenceUnitName,
secretsGenerator(realmContext));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import org.apache.polaris.core.exceptions.AlreadyExistsException;
import org.apache.polaris.core.persistence.PolarisMetaStoreManagerImpl;
import org.apache.polaris.core.persistence.PolarisMetaStoreSession;
import org.apache.polaris.core.persistence.PrincipalSecretsGenerator;
import org.apache.polaris.core.persistence.RetryOnConcurrencyException;
import org.apache.polaris.core.persistence.models.ModelEntity;
import org.apache.polaris.core.persistence.models.ModelEntityActive;
Expand Down Expand Up @@ -98,6 +99,7 @@ public class PolarisEclipseLinkMetaStoreSessionImpl implements PolarisMetaStoreS
private final ThreadLocal<EntityManager> localSession = new ThreadLocal<>();
private final PolarisEclipseLinkStore store;
private final PolarisStorageIntegrationProvider storageIntegrationProvider;
private final PrincipalSecretsGenerator secretsGenerator;

/**
* Create a meta store session against provided realm. Each realm has its own database.
Expand All @@ -113,7 +115,8 @@ public PolarisEclipseLinkMetaStoreSessionImpl(
@NotNull PolarisStorageIntegrationProvider storageIntegrationProvider,
@NotNull RealmContext realmContext,
@Nullable String confFile,
@Nullable String persistenceUnitName) {
@Nullable String persistenceUnitName,
@NotNull PrincipalSecretsGenerator secretsGenerator) {
LOGGER.debug(
"Creating EclipseLink Meta Store Session for realm {}", realmContext.getRealmIdentifier());
emf = createEntityManagerFactory(realmContext, confFile, persistenceUnitName);
Expand All @@ -124,6 +127,7 @@ public PolarisEclipseLinkMetaStoreSessionImpl(
this.store.initialize(session);
}
this.storageIntegrationProvider = storageIntegrationProvider;
this.secretsGenerator = secretsGenerator;
}

/**
Expand Down Expand Up @@ -651,7 +655,7 @@ public int lookupEntityGrantRecordsVersion(
ModelPrincipalSecrets lookupPrincipalSecrets;
do {
// generate new random client id and secrets
principalSecrets = new PolarisPrincipalSecrets(principalId);
principalSecrets = secretsGenerator.produceSecrets(principalName, principalId);

// load the existing secrets
lookupPrincipalSecrets =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.apache.polaris.extension.persistence.impl.eclipselink;

import static jakarta.persistence.Persistence.createEntityManagerFactory;
import static org.apache.polaris.core.persistence.PrincipalSecretsGenerator.RANDOM_SECRETS;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand Down Expand Up @@ -57,7 +58,7 @@ protected PolarisTestMetaStoreManager createPolarisTestMetaStoreManager() {
PolarisEclipseLinkStore store = new PolarisEclipseLinkStore(diagServices);
PolarisEclipseLinkMetaStoreSessionImpl session =
new PolarisEclipseLinkMetaStoreSessionImpl(
store, Mockito.mock(), () -> "realm", null, "polaris");
store, Mockito.mock(), () -> "realm", null, "polaris", RANDOM_SECRETS);
return new PolarisTestMetaStoreManager(
new PolarisMetaStoreManagerImpl(),
new PolarisCallContext(
Expand All @@ -78,7 +79,7 @@ void testCreateStoreSession(String confFile, boolean success) {
try {
var session =
new PolarisEclipseLinkMetaStoreSessionImpl(
store, Mockito.mock(), () -> "realm", confFile, "polaris");
store, Mockito.mock(), () -> "realm", confFile, "polaris", RANDOM_SECRETS);
assertNotNull(session);
assertTrue(success);
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,21 @@ public abstract class LocalPolarisMetaStoreManagerFactory<StoreType>
private static final Logger LOGGER =
LoggerFactory.getLogger(LocalPolarisMetaStoreManagerFactory.class);

private boolean bootstrap;

protected abstract StoreType createBackingStore(@NotNull PolarisDiagnostics diagnostics);

protected abstract PolarisMetaStoreSession createMetaStoreSession(
@NotNull StoreType store, @NotNull RealmContext realmContext);

protected PrincipalSecretsGenerator secretsGenerator(RealmContext realmContext) {
if (bootstrap) {
return PrincipalSecretsGenerator.bootstrap(realmContext.getRealmIdentifier());
} else {
return PrincipalSecretsGenerator.RANDOM_SECRETS;
}
}

private void initializeForRealm(RealmContext realmContext) {
final StoreType backingStore = createBackingStore(diagServices);
backingStoreMap.put(realmContext.getRealmIdentifier(), backingStore);
Expand All @@ -79,15 +89,20 @@ private void initializeForRealm(RealmContext realmContext) {
public synchronized Map<String, PrincipalSecretsResult> bootstrapRealms(List<String> realms) {
Map<String, PrincipalSecretsResult> results = new HashMap<>();

for (String realm : realms) {
RealmContext realmContext = () -> realm;
if (!metaStoreManagerMap.containsKey(realmContext.getRealmIdentifier())) {
initializeForRealm(realmContext);
PrincipalSecretsResult secretsResult =
bootstrapServiceAndCreatePolarisPrincipalForRealm(
realmContext, metaStoreManagerMap.get(realmContext.getRealmIdentifier()));
results.put(realmContext.getRealmIdentifier(), secretsResult);
bootstrap = true;
try {
for (String realm : realms) {
RealmContext realmContext = () -> realm;
if (!metaStoreManagerMap.containsKey(realmContext.getRealmIdentifier())) {
initializeForRealm(realmContext);
PrincipalSecretsResult secretsResult =
bootstrapServiceAndCreatePolarisPrincipalForRealm(
realmContext, metaStoreManagerMap.get(realmContext.getRealmIdentifier()));
results.put(realmContext.getRealmIdentifier(), secretsResult);
}
}
} finally {
bootstrap = false;
}

return results;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,17 @@ public class PolarisTreeMapMetaStoreSessionImpl implements PolarisMetaStoreSessi
// the TreeMap store to use
private final PolarisTreeMapStore store;
private final PolarisStorageIntegrationProvider storageIntegrationProvider;
private final PrincipalSecretsGenerator secretsGenerator;

public PolarisTreeMapMetaStoreSessionImpl(
@NotNull PolarisTreeMapStore store,
@NotNull PolarisStorageIntegrationProvider storageIntegrationProvider) {
@NotNull PolarisStorageIntegrationProvider storageIntegrationProvider,
@NotNull PrincipalSecretsGenerator secretsGenerator) {

// init store
this.store = store;
this.storageIntegrationProvider = storageIntegrationProvider;
this.secretsGenerator = secretsGenerator;
}

/** {@inheritDoc} */
Expand Down Expand Up @@ -454,7 +457,7 @@ public int lookupEntityGrantRecordsVersion(
PolarisPrincipalSecrets lookupPrincipalSecrets;
do {
// generate new random client id and secrets
principalSecrets = new PolarisPrincipalSecrets(principalId);
principalSecrets = secretsGenerator.produceSecrets(principalName, principalId);

// load the existing secrets
lookupPrincipalSecrets =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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;

import java.util.Locale;
import java.util.function.Function;
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
import org.jetbrains.annotations.NotNull;

/**
* An interface for generating principal secrets. It enables detaching the secret generation logic
* from services that actually manage principal objects (create, remove, rotate secrets, etc.)
*
* <p>The implementation statically available from {@link #bootstrap(String)} allows one-time client
* ID and secret overrides via environment variables, which can be useful for bootstrapping new
* realms.
*
* <p>The environment variable name follow this pattern:
*
* <ul>
* <li>{@code POLARIS_BOOTSTRAP_<REALM-NAME>_<PRINCIPAL-NAME>_CLIENT_ID}
* <li>{@code POLARIS_BOOTSTRAP_<REALM-NAME>_<PRINCIPAL-NAME>_CLIENT_SECRET}
* </ul>
*
* For example: {@code POLARIS_BOOTSTRAP_DEFAULT-REALM_ROOT_CLIENT_ID} and {@code
* POLARIS_BOOTSTRAP_DEFAULT-REALM_ROOT_CLIENT_SECRET}.
*/
@FunctionalInterface
public interface PrincipalSecretsGenerator {
Copy link
Contributor

Choose a reason for hiding this comment

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

Short javadoc here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added


/**
* A secret generator that produces cryptographically random client ID and client secret values.
*/
PrincipalSecretsGenerator RANDOM_SECRETS = (name, id) -> new PolarisPrincipalSecrets(id);

/**
* Produces a new {@link PolarisPrincipalSecrets} object for the given principal ID. The returned
* secrets may or may not be random, depending on context. In bootstrapping contexts, the returned
* secrets can be predefined. After bootstrapping, the returned secrets can be expected to be
* cryptographically random.
*
* @param principalName the name of the related principal. This parameter is a hint for
* pre-defined secrets lookup during bootstrapping it is not included in the returned data.
* @param principalId the ID of the related principal. This ID is part of the returned data.
* @return a new {@link PolarisPrincipalSecrets} instance for the specified principal.
*/
PolarisPrincipalSecrets produceSecrets(@NotNull String principalName, long principalId);

static PrincipalSecretsGenerator bootstrap(String realmName) {
return bootstrap(realmName, System.getenv()::get);
}

static PrincipalSecretsGenerator bootstrap(String realmName, Function<String, String> config) {
Comment on lines +65 to +69
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks a lot better, thanks!

return (principalName, principalId) -> {
String propId = String.format("POLARIS_BOOTSTRAP_%s_%s_CLIENT_ID", realmName, principalName);
String propSecret =
String.format("POLARIS_BOOTSTRAP_%s_%s_CLIENT_SECRET", realmName, principalName);

String clientId = config.apply(propId.toUpperCase(Locale.ROOT));
String secret = config.apply(propSecret.toUpperCase(Locale.ROOT));
// use config values at most once (do not interfere with secret rotation)
if (clientId != null && secret != null) {
return new PolarisPrincipalSecrets(principalId, clientId, secret, secret);
} else {
return RANDOM_SECRETS.produceSecrets(principalName, principalId);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
package org.apache.polaris.core.persistence;

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

import java.util.List;
import java.util.stream.Collectors;
import org.apache.polaris.core.PolarisCallContext;
Expand Down Expand Up @@ -82,7 +84,7 @@ public class EntityCacheTest {
public EntityCacheTest() {
diagServices = new PolarisDefaultDiagServiceImpl();
store = new PolarisTreeMapStore(diagServices);
metaStore = new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock());
metaStore = new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock(), RANDOM_SECRETS);
callCtx = new PolarisCallContext(metaStore, diagServices);
metaStoreManager = new PolarisMetaStoreManagerImpl();

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

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

import java.time.ZoneId;
import org.apache.polaris.core.PolarisCallContext;
import org.apache.polaris.core.PolarisConfigurationStore;
Expand All @@ -32,7 +34,7 @@ public PolarisTestMetaStoreManager createPolarisTestMetaStoreManager() {
PolarisTreeMapStore store = new PolarisTreeMapStore(diagServices);
PolarisCallContext callCtx =
new PolarisCallContext(
new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock()),
new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock(), RANDOM_SECRETS),
diagServices,
new PolarisConfigurationStore() {},
timeSource.withZone(ZoneId.systemDefault()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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;

import static org.apache.polaris.core.persistence.PrincipalSecretsGenerator.bootstrap;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.Map;
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
import org.junit.jupiter.api.Test;

class PrincipalSecretsGeneratorTest {

@Test
void testRandomSecrets() {
PolarisPrincipalSecrets s = bootstrap("test", (name) -> null).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 testSecretOverride() {
PrincipalSecretsGenerator gen =
bootstrap(
"test-Realm",
Map.of(
"POLARIS_BOOTSTRAP_TEST-REALM_USER1_CLIENT_ID",
"client1",
"POLARIS_BOOTSTRAP_TEST-REALM_USER1_CLIENT_SECRET",
"sec2")
::get);
PolarisPrincipalSecrets s = gen.produceSecrets("user1", 123);
assertThat(s).isNotNull();
assertThat(s.getPrincipalId()).isEqualTo(123);
assertThat(s.getPrincipalClientId()).isEqualTo("client1");
assertThat(s.getMainSecret()).isEqualTo("sec2");
assertThat(s.getSecondarySecret()).isEqualTo("sec2");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
package org.apache.polaris.core.persistence;

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

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
Expand Down Expand Up @@ -96,7 +98,7 @@ public class ResolverTest {
public ResolverTest() {
diagServices = new PolarisDefaultDiagServiceImpl();
store = new PolarisTreeMapStore(diagServices);
metaStore = new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock());
metaStore = new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock(), RANDOM_SECRETS);
callCtx = new PolarisCallContext(metaStore, diagServices);
metaStoreManager = new PolarisMetaStoreManagerImpl();

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

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

import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -66,7 +68,7 @@ public StorageCredentialCacheTest() {
PolarisTreeMapStore store = new PolarisTreeMapStore(diagServices);
// to interact with the metastore
PolarisMetaStoreSession metaStore =
new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock());
new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock(), RANDOM_SECRETS);
callCtx = new PolarisCallContext(metaStore, diagServices);
metaStoreManager = Mockito.mock(PolarisMetaStoreManagerImpl.class);
storageCredentialCache = new StorageCredentialCache();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ protected PolarisTreeMapStore createBackingStore(@NotNull PolarisDiagnostics dia
@Override
protected PolarisMetaStoreSession createMetaStoreSession(
@NotNull PolarisTreeMapStore store, @NotNull RealmContext realmContext) {
return new PolarisTreeMapMetaStoreSessionImpl(store, storageIntegration);
return new PolarisTreeMapMetaStoreSessionImpl(
store, storageIntegration, secretsGenerator(realmContext));
}

@Override
Expand Down