This is an automated email from the ASF dual-hosted git repository.
adutra pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push:
new a7cdbd71 Simplify the environment variables needed for credential
bootstrapping (#633)
a7cdbd71 is described below
commit a7cdbd7136ad334ddbdc817903833cbee97ed0b6
Author: Alexandre Dutra <[email protected]>
AuthorDate: Fri Jan 10 10:30:08 2025 +0100
Simplify the environment variables needed for credential bootstrapping
(#633)
---
.../persistence/PolarisCredentialsBootstrap.java | 110 +++++++++++++++++++++
.../persistence/PrincipalSecretsGenerator.java | 42 +++-----
.../PolarisCredentialsBootstrapTest.java | 106 ++++++++++++++++++++
.../persistence/PrincipalSecretsGeneratorTest.java | 11 +--
.../configuring-polaris-for-production.md | 19 ++--
5 files changed, 244 insertions(+), 44 deletions(-)
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrap.java
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrap.java
new file mode 100644
index 00000000..fb702812
--- /dev/null
+++
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrap.java
@@ -0,0 +1,110 @@
+/*
+ * 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 com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import jakarta.annotation.Nullable;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
+
+/**
+ * A utility to parse and provide credentials for Polaris realms and
principals during a bootstrap
+ * phase.
+ */
+public class PolarisCredentialsBootstrap {
+
+ /**
+ * Parse credentials from the system property {@code
polaris.bootstrap.credentials} or the
+ * environment variable {@code POLARIS_BOOTSTRAP_CREDENTIALS}, whichever is
set.
+ *
+ * <p>See {@link #fromString(String)} for the expected format.
+ */
+ public static PolarisCredentialsBootstrap fromEnvironment() {
+ return fromString(
+ System.getProperty(
+ "polaris.bootstrap.credentials",
System.getenv().get("POLARIS_BOOTSTRAP_CREDENTIALS")));
+ }
+
+ /**
+ * Parse a string of credentials in the format:
+ *
+ * <pre>
+ *
realm1,user1a,client1a,secret1a;realm1,user1b,client1b,secret1b;realm2,user2a,client2a,secret2a;...
+ * </pre>
+ */
+ public static PolarisCredentialsBootstrap fromString(@Nullable String
credentialsString) {
+ Map<String, Map<String, Map.Entry<String, String>>> credentials = new
HashMap<>();
+ if (credentialsString != null && !credentialsString.isBlank()) {
+ Splitter.on(';')
+ .trimResults()
+ .splitToList(credentialsString)
+ .forEach(
+ quadruple -> {
+ if (!quadruple.isBlank()) {
+ List<String> parts =
Splitter.on(',').trimResults().splitToList(quadruple);
+ if (parts.size() != 4) {
+ throw new IllegalArgumentException("Invalid credentials
format: " + quadruple);
+ }
+ String realmName = parts.get(0);
+ String principalName = parts.get(1);
+ String clientId = parts.get(2);
+ String clientSecret = parts.get(3);
+ credentials
+ .computeIfAbsent(realmName, k -> new HashMap<>())
+ .merge(
+ principalName,
+ new SimpleEntry<>(clientId, clientSecret),
+ (a, b) -> {
+ throw new IllegalArgumentException(
+ "Duplicate principal: " + principalName);
+ });
+ }
+ });
+ }
+ return new PolarisCredentialsBootstrap(credentials);
+ }
+
+ @VisibleForTesting final Map<String, Map<String, Map.Entry<String, String>>>
credentials;
+
+ private PolarisCredentialsBootstrap(Map<String, Map<String, Entry<String,
String>>> credentials) {
+ this.credentials = credentials;
+ }
+
+ /**
+ * Get the secrets for the specified principal in the specified realm, if
available among the
+ * credentials that were supplied for bootstrap.
+ */
+ public Optional<PolarisPrincipalSecrets> getSecrets(
+ String realmName, long principalId, String principalName) {
+ return Optional.ofNullable(credentials.get(realmName))
+ .flatMap(principals ->
Optional.ofNullable(principals.get(principalName)))
+ .map(
+ credentials -> {
+ String clientId = credentials.getKey();
+ String secret = credentials.getValue();
+ return new PolarisPrincipalSecrets(principalId, clientId,
secret, secret);
+ });
+ }
+}
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
index 27ddacbe..270d8569 100644
---
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
@@ -19,8 +19,8 @@
package org.apache.polaris.core.persistence;
import jakarta.annotation.Nonnull;
-import java.util.Locale;
-import java.util.function.Function;
+import jakarta.annotation.Nullable;
+import java.util.Optional;
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
/**
@@ -28,18 +28,11 @@ import
org.apache.polaris.core.entity.PolarisPrincipalSecrets;
* 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.
+ * ID and secret overrides via system properties or 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}.
+ * <p>See {@link PolarisCredentialsBootstrap} for more information on the
expected environment
+ * variable name, and the format of the bootstrap credentials.
*/
@FunctionalInterface
public interface PrincipalSecretsGenerator {
@@ -63,23 +56,14 @@ public interface PrincipalSecretsGenerator {
PolarisPrincipalSecrets produceSecrets(@Nonnull String principalName, long
principalId);
static PrincipalSecretsGenerator bootstrap(String realmName) {
- return bootstrap(realmName, System.getenv()::get);
+ return bootstrap(realmName, PolarisCredentialsBootstrap.fromEnvironment());
}
- 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);
- }
- };
+ static PrincipalSecretsGenerator bootstrap(
+ String realmName, @Nullable PolarisCredentialsBootstrap
credentialsSupplier) {
+ return (principalName, principalId) ->
+ Optional.ofNullable(credentialsSupplier)
+ .flatMap(credentials -> credentials.getSecrets(realmName,
principalId, principalName))
+ .orElseGet(() -> RANDOM_SECRETS.produceSecrets(principalName,
principalId));
}
}
diff --git
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrapTest.java
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrapTest.java
new file mode 100644
index 00000000..bd4c57c9
--- /dev/null
+++
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrapTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Comparator;
+import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
+import org.junit.jupiter.api.Test;
+
+class PolarisCredentialsBootstrapTest {
+
+ private final Comparator<PolarisPrincipalSecrets> comparator =
+ (a, b) ->
+ a.getPrincipalId() == b.getPrincipalId()
+ && a.getPrincipalClientId().equals(b.getPrincipalClientId())
+ && a.getMainSecret().equals(b.getMainSecret())
+ && a.getSecondarySecret().equals(b.getSecondarySecret())
+ ? 0
+ : 1;
+
+ @Test
+ void nullString() {
+ PolarisCredentialsBootstrap credentials =
PolarisCredentialsBootstrap.fromString(null);
+ assertThat(credentials.credentials).isEmpty();
+ }
+
+ @Test
+ void emptyString() {
+ PolarisCredentialsBootstrap credentials =
PolarisCredentialsBootstrap.fromString("");
+ assertThat(credentials.credentials).isEmpty();
+ }
+
+ @Test
+ void blankString() {
+ PolarisCredentialsBootstrap credentials =
PolarisCredentialsBootstrap.fromString(" ");
+ assertThat(credentials.credentials).isEmpty();
+ }
+
+ @Test
+ void invalidString() {
+ assertThatThrownBy(() -> PolarisCredentialsBootstrap.fromString("test"))
+ .hasMessage("Invalid credentials format: test");
+ }
+
+ @Test
+ void duplicatePrincipal() {
+ assertThatThrownBy(
+ () ->
+ PolarisCredentialsBootstrap.fromString(
+
"realm1,user1a,client1a,secret1a;realm1,user1a,client1b,secret1b"))
+ .hasMessage("Duplicate principal: user1a");
+ }
+
+ @Test
+ void getSecretsValidString() {
+ PolarisCredentialsBootstrap credentials =
+ PolarisCredentialsBootstrap.fromString(
+ " ; realm1 , user1a , client1a , secret1a ; realm1 , user1b ,
client1b , secret1b ; realm2 , user2a , client2a , secret2a ; ");
+ assertThat(credentials.getSecrets("realm1", 123, "nonexistent")).isEmpty();
+ assertThat(credentials.getSecrets("nonexistent", 123, "user1a")).isEmpty();
+ assertThat(credentials.getSecrets("realm1", 123, "user1a"))
+ .usingValueComparator(comparator)
+ .contains(new PolarisPrincipalSecrets(123, "client1a", "secret1a",
"secret1a"));
+ assertThat(credentials.getSecrets("realm1", 123, "user1b"))
+ .usingValueComparator(comparator)
+ .contains(new PolarisPrincipalSecrets(123, "client1b", "secret1b",
"secret1b"));
+ assertThat(credentials.getSecrets("realm2", 123, "user2a"))
+ .usingValueComparator(comparator)
+ .contains(new PolarisPrincipalSecrets(123, "client2a", "secret2a",
"secret2a"));
+ }
+
+ @Test
+ void getSecretsValidSystemProperty() {
+ PolarisCredentialsBootstrap credentials =
PolarisCredentialsBootstrap.fromEnvironment();
+ assertThat(credentials.credentials).isEmpty();
+ try {
+ System.setProperty("polaris.bootstrap.credentials",
"realm1,user1a,client1a,secret1a");
+ credentials = PolarisCredentialsBootstrap.fromEnvironment();
+ assertThat(credentials.getSecrets("realm1", 123,
"nonexistent")).isEmpty();
+ assertThat(credentials.getSecrets("nonexistent", 123,
"user1a")).isEmpty();
+ assertThat(credentials.getSecrets("realm1", 123, "user1a"))
+ .usingValueComparator(comparator)
+ .contains(new PolarisPrincipalSecrets(123, "client1a", "secret1a",
"secret1a"));
+ } finally {
+ System.clearProperty("polaris.bootstrap.credentials");
+ }
+ }
+}
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
index afbfbbbf..b03b368b 100644
---
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
@@ -21,7 +21,6 @@ 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;
@@ -29,7 +28,7 @@ class PrincipalSecretsGeneratorTest {
@Test
void testRandomSecrets() {
- PolarisPrincipalSecrets s = bootstrap("test", (name) ->
null).produceSecrets("name1", 123);
+ PolarisPrincipalSecrets s = bootstrap("test",
null).produceSecrets("name1", 123);
assertThat(s).isNotNull();
assertThat(s.getPrincipalId()).isEqualTo(123);
assertThat(s.getPrincipalClientId()).isNotNull();
@@ -41,13 +40,7 @@ class PrincipalSecretsGeneratorTest {
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);
+ "test-Realm",
PolarisCredentialsBootstrap.fromString("test-Realm,user1,client1,sec2"));
PolarisPrincipalSecrets s = gen.produceSecrets("user1", 123);
assertThat(s).isNotNull();
assertThat(s.getPrincipalId()).isEqualTo(123);
diff --git
a/site/content/in-dev/unreleased/configuring-polaris-for-production.md
b/site/content/in-dev/unreleased/configuring-polaris-for-production.md
index 152d12fd..e02be618 100644
--- a/site/content/in-dev/unreleased/configuring-polaris-for-production.md
+++ b/site/content/in-dev/unreleased/configuring-polaris-for-production.md
@@ -72,17 +72,24 @@ To use EclipseLink for metastore management, specify the
configuration `metaStor
Before using Polaris when using a metastore manager other than `in-memory`,
you must **bootstrap** the metastore manager. This is a manual operation that
must be performed **only once** in order to prepare the metastore manager to
integrate with Polaris. When the metastore manager is bootstrapped, any
existing Polaris entities in the metastore manager may be **purged**.
-By default, Polaris will create randomised `CLIENT_ID` and `CLIENT_SECRET` for
the `root` principal and store their hashes in the metastore backend. In order
to provide your own credentials for `root` principal (so you can request tokens
via `api/catalog/v1/oauth/tokens`), set the following envrionment variables for
realm name `my_realm`:
+By default, Polaris will create randomised `CLIENT_ID` and `CLIENT_SECRET` for
the `root` principal and store their hashes in the metastore backend. In order
to provide your own credentials for `root` principal (so you can request tokens
via `api/catalog/v1/oauth/tokens`), set the `POLARIS_BOOTSTRAP_CREDENTIALS`
environment variable as follows:
```
-export POLARIS_BOOTSTRAP_MY_REALM_ROOT_CLIENT_ID=my-client-id
-export POLARIS_BOOTSTRAP_MY_REALM_ROOT_CLIENT_SECRET=my-client-secret
+export
POLARIS_BOOTSTRAP_CREDENTIALS=my_realm,root,my-client-id,my-client-secret
```
-**IMPORTANT**: In case you use `default-realm` for metastore backend database,
you won't be able to use `export` command. Use this instead:
+The format of the environment variable is
`realm,principal,client_id,client_secret`. You can provide multiple credentials
separated by `;`. For example, to provide credentials for two realms `my_realm`
and `my_realm2`:
-```bash
-env POLARIS_BOOTSTRAP_DEFAULT-REALM_ROOT_CLIENT_ID=my-client-id
POLARIS_BOOTSTRAP_DEFAULT-REALM_ROOT_CLIENT_SECRET=my-client-secret <bootstrap
command>
+```
+export
POLARIS_BOOTSTRAP_CREDENTIALS=my_realm,root,my-client-id,my-client-secret;my_realm2,root,my-client-id2,my-client-secret2
+```
+
+You can also provide credentials for other users too.
+
+It is also possible to use system properties to provide the credentials:
+
+```
+java
-Dpolaris.bootstrap.credentials=my_realm,root,my-client-id,my-client-secret
-jar /path/to/jar/polaris-service-all.jar bootstrap polaris-server.yml
```
Now, to bootstrap Polaris, run: