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:

Reply via email to