Skip to content

Commit

Permalink
Support providing root client ID via env. variables when bootstrapping (
Browse files Browse the repository at this point in the history
#422)

* Support providing root client ID via env. variables for bootstrapping

Introduce a `PrincipalSecretsGenerator` interface to isolate secrets
generation from principal management code.

Update meta store factories to allow the user to define the root
client ID and secret via environment variables during bootstrapping.

* review: add @NotNull

---------

Co-authored-by: Eric Maynard <[email protected]>
  • Loading branch information
dimas-b and eric-maynard authored Nov 25, 2024
1 parent 769424f commit 29a9828
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 20 deletions.
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 {

/**
* 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) {
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

0 comments on commit 29a9828

Please sign in to comment.