Skip to content

Commit a7cdbd7

Browse files
authored
Simplify the environment variables needed for credential bootstrapping (#633)
1 parent b12a706 commit a7cdbd7

File tree

5 files changed

+244
-44
lines changed

5 files changed

+244
-44
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.core.persistence;
20+
21+
import com.google.common.annotations.VisibleForTesting;
22+
import com.google.common.base.Splitter;
23+
import jakarta.annotation.Nullable;
24+
import java.util.AbstractMap.SimpleEntry;
25+
import java.util.HashMap;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Map.Entry;
29+
import java.util.Optional;
30+
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
31+
32+
/**
33+
* A utility to parse and provide credentials for Polaris realms and principals during a bootstrap
34+
* phase.
35+
*/
36+
public class PolarisCredentialsBootstrap {
37+
38+
/**
39+
* Parse credentials from the system property {@code polaris.bootstrap.credentials} or the
40+
* environment variable {@code POLARIS_BOOTSTRAP_CREDENTIALS}, whichever is set.
41+
*
42+
* <p>See {@link #fromString(String)} for the expected format.
43+
*/
44+
public static PolarisCredentialsBootstrap fromEnvironment() {
45+
return fromString(
46+
System.getProperty(
47+
"polaris.bootstrap.credentials", System.getenv().get("POLARIS_BOOTSTRAP_CREDENTIALS")));
48+
}
49+
50+
/**
51+
* Parse a string of credentials in the format:
52+
*
53+
* <pre>
54+
* realm1,user1a,client1a,secret1a;realm1,user1b,client1b,secret1b;realm2,user2a,client2a,secret2a;...
55+
* </pre>
56+
*/
57+
public static PolarisCredentialsBootstrap fromString(@Nullable String credentialsString) {
58+
Map<String, Map<String, Map.Entry<String, String>>> credentials = new HashMap<>();
59+
if (credentialsString != null && !credentialsString.isBlank()) {
60+
Splitter.on(';')
61+
.trimResults()
62+
.splitToList(credentialsString)
63+
.forEach(
64+
quadruple -> {
65+
if (!quadruple.isBlank()) {
66+
List<String> parts = Splitter.on(',').trimResults().splitToList(quadruple);
67+
if (parts.size() != 4) {
68+
throw new IllegalArgumentException("Invalid credentials format: " + quadruple);
69+
}
70+
String realmName = parts.get(0);
71+
String principalName = parts.get(1);
72+
String clientId = parts.get(2);
73+
String clientSecret = parts.get(3);
74+
credentials
75+
.computeIfAbsent(realmName, k -> new HashMap<>())
76+
.merge(
77+
principalName,
78+
new SimpleEntry<>(clientId, clientSecret),
79+
(a, b) -> {
80+
throw new IllegalArgumentException(
81+
"Duplicate principal: " + principalName);
82+
});
83+
}
84+
});
85+
}
86+
return new PolarisCredentialsBootstrap(credentials);
87+
}
88+
89+
@VisibleForTesting final Map<String, Map<String, Map.Entry<String, String>>> credentials;
90+
91+
private PolarisCredentialsBootstrap(Map<String, Map<String, Entry<String, String>>> credentials) {
92+
this.credentials = credentials;
93+
}
94+
95+
/**
96+
* Get the secrets for the specified principal in the specified realm, if available among the
97+
* credentials that were supplied for bootstrap.
98+
*/
99+
public Optional<PolarisPrincipalSecrets> getSecrets(
100+
String realmName, long principalId, String principalName) {
101+
return Optional.ofNullable(credentials.get(realmName))
102+
.flatMap(principals -> Optional.ofNullable(principals.get(principalName)))
103+
.map(
104+
credentials -> {
105+
String clientId = credentials.getKey();
106+
String secret = credentials.getValue();
107+
return new PolarisPrincipalSecrets(principalId, clientId, secret, secret);
108+
});
109+
}
110+
}

polaris-core/src/main/java/org/apache/polaris/core/persistence/PrincipalSecretsGenerator.java

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,20 @@
1919
package org.apache.polaris.core.persistence;
2020

2121
import jakarta.annotation.Nonnull;
22-
import java.util.Locale;
23-
import java.util.function.Function;
22+
import jakarta.annotation.Nullable;
23+
import java.util.Optional;
2424
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
2525

2626
/**
2727
* An interface for generating principal secrets. It enables detaching the secret generation logic
2828
* from services that actually manage principal objects (create, remove, rotate secrets, etc.)
2929
*
3030
* <p>The implementation statically available from {@link #bootstrap(String)} allows one-time client
31-
* ID and secret overrides via environment variables, which can be useful for bootstrapping new
32-
* realms.
31+
* ID and secret overrides via system properties or environment variables, which can be useful for
32+
* bootstrapping new realms.
3333
*
34-
* <p>The environment variable name follow this pattern:
35-
*
36-
* <ul>
37-
* <li>{@code POLARIS_BOOTSTRAP_<REALM-NAME>_<PRINCIPAL-NAME>_CLIENT_ID}
38-
* <li>{@code POLARIS_BOOTSTRAP_<REALM-NAME>_<PRINCIPAL-NAME>_CLIENT_SECRET}
39-
* </ul>
40-
*
41-
* For example: {@code POLARIS_BOOTSTRAP_DEFAULT-REALM_ROOT_CLIENT_ID} and {@code
42-
* POLARIS_BOOTSTRAP_DEFAULT-REALM_ROOT_CLIENT_SECRET}.
34+
* <p>See {@link PolarisCredentialsBootstrap} for more information on the expected environment
35+
* variable name, and the format of the bootstrap credentials.
4336
*/
4437
@FunctionalInterface
4538
public interface PrincipalSecretsGenerator {
@@ -63,23 +56,14 @@ public interface PrincipalSecretsGenerator {
6356
PolarisPrincipalSecrets produceSecrets(@Nonnull String principalName, long principalId);
6457

6558
static PrincipalSecretsGenerator bootstrap(String realmName) {
66-
return bootstrap(realmName, System.getenv()::get);
59+
return bootstrap(realmName, PolarisCredentialsBootstrap.fromEnvironment());
6760
}
6861

69-
static PrincipalSecretsGenerator bootstrap(String realmName, Function<String, String> config) {
70-
return (principalName, principalId) -> {
71-
String propId = String.format("POLARIS_BOOTSTRAP_%s_%s_CLIENT_ID", realmName, principalName);
72-
String propSecret =
73-
String.format("POLARIS_BOOTSTRAP_%s_%s_CLIENT_SECRET", realmName, principalName);
74-
75-
String clientId = config.apply(propId.toUpperCase(Locale.ROOT));
76-
String secret = config.apply(propSecret.toUpperCase(Locale.ROOT));
77-
// use config values at most once (do not interfere with secret rotation)
78-
if (clientId != null && secret != null) {
79-
return new PolarisPrincipalSecrets(principalId, clientId, secret, secret);
80-
} else {
81-
return RANDOM_SECRETS.produceSecrets(principalName, principalId);
82-
}
83-
};
62+
static PrincipalSecretsGenerator bootstrap(
63+
String realmName, @Nullable PolarisCredentialsBootstrap credentialsSupplier) {
64+
return (principalName, principalId) ->
65+
Optional.ofNullable(credentialsSupplier)
66+
.flatMap(credentials -> credentials.getSecrets(realmName, principalId, principalName))
67+
.orElseGet(() -> RANDOM_SECRETS.produceSecrets(principalName, principalId));
8468
}
8569
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.core.persistence;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
23+
24+
import java.util.Comparator;
25+
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
26+
import org.junit.jupiter.api.Test;
27+
28+
class PolarisCredentialsBootstrapTest {
29+
30+
private final Comparator<PolarisPrincipalSecrets> comparator =
31+
(a, b) ->
32+
a.getPrincipalId() == b.getPrincipalId()
33+
&& a.getPrincipalClientId().equals(b.getPrincipalClientId())
34+
&& a.getMainSecret().equals(b.getMainSecret())
35+
&& a.getSecondarySecret().equals(b.getSecondarySecret())
36+
? 0
37+
: 1;
38+
39+
@Test
40+
void nullString() {
41+
PolarisCredentialsBootstrap credentials = PolarisCredentialsBootstrap.fromString(null);
42+
assertThat(credentials.credentials).isEmpty();
43+
}
44+
45+
@Test
46+
void emptyString() {
47+
PolarisCredentialsBootstrap credentials = PolarisCredentialsBootstrap.fromString("");
48+
assertThat(credentials.credentials).isEmpty();
49+
}
50+
51+
@Test
52+
void blankString() {
53+
PolarisCredentialsBootstrap credentials = PolarisCredentialsBootstrap.fromString(" ");
54+
assertThat(credentials.credentials).isEmpty();
55+
}
56+
57+
@Test
58+
void invalidString() {
59+
assertThatThrownBy(() -> PolarisCredentialsBootstrap.fromString("test"))
60+
.hasMessage("Invalid credentials format: test");
61+
}
62+
63+
@Test
64+
void duplicatePrincipal() {
65+
assertThatThrownBy(
66+
() ->
67+
PolarisCredentialsBootstrap.fromString(
68+
"realm1,user1a,client1a,secret1a;realm1,user1a,client1b,secret1b"))
69+
.hasMessage("Duplicate principal: user1a");
70+
}
71+
72+
@Test
73+
void getSecretsValidString() {
74+
PolarisCredentialsBootstrap credentials =
75+
PolarisCredentialsBootstrap.fromString(
76+
" ; realm1 , user1a , client1a , secret1a ; realm1 , user1b , client1b , secret1b ; realm2 , user2a , client2a , secret2a ; ");
77+
assertThat(credentials.getSecrets("realm1", 123, "nonexistent")).isEmpty();
78+
assertThat(credentials.getSecrets("nonexistent", 123, "user1a")).isEmpty();
79+
assertThat(credentials.getSecrets("realm1", 123, "user1a"))
80+
.usingValueComparator(comparator)
81+
.contains(new PolarisPrincipalSecrets(123, "client1a", "secret1a", "secret1a"));
82+
assertThat(credentials.getSecrets("realm1", 123, "user1b"))
83+
.usingValueComparator(comparator)
84+
.contains(new PolarisPrincipalSecrets(123, "client1b", "secret1b", "secret1b"));
85+
assertThat(credentials.getSecrets("realm2", 123, "user2a"))
86+
.usingValueComparator(comparator)
87+
.contains(new PolarisPrincipalSecrets(123, "client2a", "secret2a", "secret2a"));
88+
}
89+
90+
@Test
91+
void getSecretsValidSystemProperty() {
92+
PolarisCredentialsBootstrap credentials = PolarisCredentialsBootstrap.fromEnvironment();
93+
assertThat(credentials.credentials).isEmpty();
94+
try {
95+
System.setProperty("polaris.bootstrap.credentials", "realm1,user1a,client1a,secret1a");
96+
credentials = PolarisCredentialsBootstrap.fromEnvironment();
97+
assertThat(credentials.getSecrets("realm1", 123, "nonexistent")).isEmpty();
98+
assertThat(credentials.getSecrets("nonexistent", 123, "user1a")).isEmpty();
99+
assertThat(credentials.getSecrets("realm1", 123, "user1a"))
100+
.usingValueComparator(comparator)
101+
.contains(new PolarisPrincipalSecrets(123, "client1a", "secret1a", "secret1a"));
102+
} finally {
103+
System.clearProperty("polaris.bootstrap.credentials");
104+
}
105+
}
106+
}

polaris-core/src/test/java/org/apache/polaris/core/persistence/PrincipalSecretsGeneratorTest.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,14 @@
2121
import static org.apache.polaris.core.persistence.PrincipalSecretsGenerator.bootstrap;
2222
import static org.assertj.core.api.Assertions.assertThat;
2323

24-
import java.util.Map;
2524
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
2625
import org.junit.jupiter.api.Test;
2726

2827
class PrincipalSecretsGeneratorTest {
2928

3029
@Test
3130
void testRandomSecrets() {
32-
PolarisPrincipalSecrets s = bootstrap("test", (name) -> null).produceSecrets("name1", 123);
31+
PolarisPrincipalSecrets s = bootstrap("test", null).produceSecrets("name1", 123);
3332
assertThat(s).isNotNull();
3433
assertThat(s.getPrincipalId()).isEqualTo(123);
3534
assertThat(s.getPrincipalClientId()).isNotNull();
@@ -41,13 +40,7 @@ void testRandomSecrets() {
4140
void testSecretOverride() {
4241
PrincipalSecretsGenerator gen =
4342
bootstrap(
44-
"test-Realm",
45-
Map.of(
46-
"POLARIS_BOOTSTRAP_TEST-REALM_USER1_CLIENT_ID",
47-
"client1",
48-
"POLARIS_BOOTSTRAP_TEST-REALM_USER1_CLIENT_SECRET",
49-
"sec2")
50-
::get);
43+
"test-Realm", PolarisCredentialsBootstrap.fromString("test-Realm,user1,client1,sec2"));
5144
PolarisPrincipalSecrets s = gen.produceSecrets("user1", 123);
5245
assertThat(s).isNotNull();
5346
assertThat(s.getPrincipalId()).isEqualTo(123);

site/content/in-dev/unreleased/configuring-polaris-for-production.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,24 @@ To use EclipseLink for metastore management, specify the configuration `metaStor
7272

7373
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**.
7474

75-
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`:
75+
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:
7676

7777
```
78-
export POLARIS_BOOTSTRAP_MY_REALM_ROOT_CLIENT_ID=my-client-id
79-
export POLARIS_BOOTSTRAP_MY_REALM_ROOT_CLIENT_SECRET=my-client-secret
78+
export POLARIS_BOOTSTRAP_CREDENTIALS=my_realm,root,my-client-id,my-client-secret
8079
```
8180

82-
**IMPORTANT**: In case you use `default-realm` for metastore backend database, you won't be able to use `export` command. Use this instead:
81+
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`:
8382

84-
```bash
85-
env POLARIS_BOOTSTRAP_DEFAULT-REALM_ROOT_CLIENT_ID=my-client-id POLARIS_BOOTSTRAP_DEFAULT-REALM_ROOT_CLIENT_SECRET=my-client-secret <bootstrap command>
83+
```
84+
export POLARIS_BOOTSTRAP_CREDENTIALS=my_realm,root,my-client-id,my-client-secret;my_realm2,root,my-client-id2,my-client-secret2
85+
```
86+
87+
You can also provide credentials for other users too.
88+
89+
It is also possible to use system properties to provide the credentials:
90+
91+
```
92+
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
8693
```
8794

8895
Now, to bootstrap Polaris, run:

0 commit comments

Comments
 (0)