diff --git a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java index 0415c0d5f..b18eb30bd 100644 --- a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java +++ b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java @@ -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)); } } diff --git a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java index 534d32a0b..77fa0e700 100644 --- a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java +++ b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java @@ -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; @@ -98,6 +99,7 @@ public class PolarisEclipseLinkMetaStoreSessionImpl implements PolarisMetaStoreS private final ThreadLocal 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. @@ -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); @@ -124,6 +127,7 @@ public PolarisEclipseLinkMetaStoreSessionImpl( this.store.initialize(session); } this.storageIntegrationProvider = storageIntegrationProvider; + this.secretsGenerator = secretsGenerator; } /** @@ -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 = diff --git a/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java b/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java index be2ff5bca..3b3bb9fda 100644 --- a/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java +++ b/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java @@ -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; @@ -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( @@ -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) { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java index a53c65dac..b373f5077 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java @@ -59,11 +59,21 @@ public abstract class LocalPolarisMetaStoreManagerFactory 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); @@ -79,15 +89,20 @@ private void initializeForRealm(RealmContext realmContext) { public synchronized Map bootstrapRealms(List realms) { Map 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; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java index bcf1a97f5..01611f3d2 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java @@ -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} */ @@ -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 = diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PrincipalSecretsGenerator.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PrincipalSecretsGenerator.java new file mode 100644 index 000000000..10de55d14 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PrincipalSecretsGenerator.java @@ -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.) + * + *

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. + * + *

The environment variable name follow this pattern: + * + *

    + *
  • {@code POLARIS_BOOTSTRAP___CLIENT_ID} + *
  • {@code POLARIS_BOOTSTRAP___CLIENT_SECRET} + *
+ * + * 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 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); + } + }; + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java index 01062f335..c47367769 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java @@ -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; @@ -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(); diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java index 6317c7016..e44b45577 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java @@ -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; @@ -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())); diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PrincipalSecretsGeneratorTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PrincipalSecretsGeneratorTest.java new file mode 100644 index 000000000..afbfbbbfe --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PrincipalSecretsGeneratorTest.java @@ -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"); + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java index 2c0678fa9..da4e47343 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java @@ -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; @@ -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(); diff --git a/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java b/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java index 804c282be..357ea2e31 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java @@ -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; @@ -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(); diff --git a/polaris-service/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java b/polaris-service/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java index a268fd6a6..2fa0d4e94 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java @@ -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