Skip to content

Commit d3e4b79

Browse files
committed
docs(auth): clarify exchange-only polaris-cv wording
1 parent eef09e1 commit d3e4b79

4 files changed

Lines changed: 14 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
7171
and a subsequent `bootstrap` would create a second, empty set of tables in the other schema.
7272
Either remove the setting from the URL, or point it at the schema that already holds your
7373
Polaris tables.
74-
- Internal JWTs minted before credentials-generation binding (tokens without the `polaris-cv` claim) can no longer be used as subject tokens in token exchange; they remain valid as bearer tokens until expiry. Clients that keep exchanging long-lived tokens (including during a rolling upgrade, where a not-yet-restarted node may still mint claim-less tokens) will see the exchange rejected with `invalid_grant` once all servers run the new version.
74+
- Internal JWTs minted before credentials-generation binding (tokens without the `polaris-cv` claim) can no longer be used as subject tokens in token exchange; they remain valid as bearer tokens until expiry. During a rolling upgrade, an old node may still mint claim-less tokens: exchanging such a token on any already-upgraded node fails with `invalid_grant`, so clients can see intermittent exchange failures until the last old node is gone; after that, rejection is consistent.
7575

7676
### New Features
7777

polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisPrincipalSecrets.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,10 @@ public boolean matchesSecret(String potentialSecret) {
181181
* Credentials-generation fingerprint corresponding to the secret that matches {@code
182182
* potentialSecret}: the main generation when it matches the main secret hash, the secondary
183183
* generation when it matches the secondary secret hash, or empty when it matches neither. Newly
184-
* minted tokens carry this fingerprint in the {@code polaris-cv} claim; credential-generation is
185-
* enforced on token exchange, while bearer verify checks only the JWT signature and claims.
184+
* minted tokens carry this fingerprint in the {@code polaris-cv} claim. That claim gates
185+
* <em>token-exchange</em> eligibility against the current (and secondary) generation; bearer
186+
* verify checks only the JWT signature and claims, so a token remains usable as a bearer until
187+
* JWT expiry even after its generation is no longer current.
186188
*
187189
* <p>Comparisons are constant-time, as in {@link #matchesCredentialsVersion(String)}.
188190
*/

runtime/service/src/main/java/org/apache/polaris/service/auth/internal/broker/JWTBroker.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ public class JWTBroker implements TokenBroker {
5454
private static final String CLAIM_KEY_SCOPE = "scope";
5555

5656
/**
57-
* Credentials-generation fingerprint bound to the principal's current credentials version (see
58-
* {@link PolarisPrincipalSecrets#getCredentialsVersion()}). When secrets are rotated or reset,
59-
* the version changes and tokens carrying a prior value are rejected on token exchange. Bearer
60-
* verify checks only the JWT signature and claims. The claim name is collision-resistant per RFC
61-
* 7519.
57+
* Credentials-generation fingerprint carried on newly minted tokens (see {@link
58+
* PolarisPrincipalSecrets#getCredentialsVersion()}). When secrets are rotated or reset, the
59+
* version changes and tokens carrying a prior value are rejected on <em>token exchange</em> only.
60+
* Bearer verify checks only the JWT signature and claims, so those tokens remain usable as
61+
* bearers until JWT expiry. The claim name is collision-resistant per RFC 7519.
6262
*/
6363
@VisibleForTesting static final String CLAIM_KEY_CREDENTIALS_VERSION = "polaris-cv";
6464

@@ -195,8 +195,8 @@ public TokenResponse generateFromToken(
195195
}
196196
tokenScope = scope;
197197
}
198-
// Keep the subject token's credentials generation so the re-minted token does not outlive the
199-
// generation it was exchanged against.
198+
// Preserve the subject token's credentials generation on the re-minted token so later exchanges
199+
// keep checking the same generation fingerprint.
200200
String tokenString =
201201
generateTokenString(
202202
decodedToken.getPrincipalName(),

runtime/service/src/test/java/org/apache/polaris/service/ratelimiter/RateLimiterFilterTest.java

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,14 @@
3030
import jakarta.inject.Inject;
3131
import jakarta.ws.rs.client.Client;
3232
import jakarta.ws.rs.client.ClientBuilder;
33-
import jakarta.ws.rs.client.Entity;
3433
import jakarta.ws.rs.core.MediaType;
35-
import jakarta.ws.rs.core.MultivaluedHashMap;
3634
import jakarta.ws.rs.core.Response;
3735
import jakarta.ws.rs.core.Response.Status;
3836
import java.util.Map;
3937
import java.util.Set;
4038
import java.util.function.Consumer;
4139
import org.apache.iceberg.rest.responses.ErrorResponse;
4240
import org.apache.iceberg.rest.responses.ErrorResponseParser;
43-
import org.apache.iceberg.rest.responses.OAuthTokenResponse;
4441
import org.apache.polaris.service.events.EventAttributes;
4542
import org.apache.polaris.service.events.PolarisEvent;
4643
import org.apache.polaris.service.events.PolarisEventType;
@@ -83,11 +80,6 @@ public Map<String, String> getConfigOverrides() {
8380
.put("polaris.rate-limiter.token-bucket.type", "default")
8481
.put("polaris.metrics.tags.environment", "prod")
8582
.put("polaris.realm-context.realms", "POLARIS,POLARIS2")
86-
// Bootstrapping both realms with known credentials so each realm can mint its own
87-
// access token. Tokens are bound to per-realm secret hashes and cannot be shared.
88-
.put(
89-
"polaris.bootstrap.credentials",
90-
"POLARIS,test-admin,test-secret;POLARIS2,test-admin2,test-secret2")
9183
.put("polaris.metrics.realm-id-tag.enable-in-api-metrics", "true")
9284
.put("polaris.metrics.realm-id-tag.enable-in-http-metrics", "true")
9385
.put("polaris.authentication.token-broker.type", "symmetric-key")
@@ -146,14 +138,10 @@ public void testRateLimiter() {
146138
requestAsserter.accept(Status.TOO_MANY_REQUESTS);
147139
}
148140

149-
// Ensure that a different realm identifier gets a separate limit. Access tokens are
150-
// bound to per-realm principal secrets, so mint a token for POLARIS2 rather than reusing
151-
// the POLARIS admin token (which would correctly return 401 after credentials binding).
141+
// Ensure that a different realm identifier gets a separate limit
152142
MockRateLimiter.allowProceed = true;
153-
String polaris2Token =
154-
obtainAccessTokenForRealm(polarisEndpoints, "POLARIS2", "test-admin2", "test-secret2");
155143
Consumer<Status> requestAsserter2 =
156-
constructRequestAsserter(polarisEndpoints, polaris2Token, "POLARIS2");
144+
constructRequestAsserter(polarisEndpoints, adminToken, "POLARIS2");
157145
requestAsserter2.accept(Status.OK);
158146
}
159147

@@ -255,27 +243,4 @@ private static Consumer<Status> constructRequestAsserter(
255243
}
256244
};
257245
}
258-
259-
/**
260-
* Mints an access token in the given realm using client credentials. Realm must be bootstrapped
261-
* with those credentials (see {@link Profile}).
262-
*/
263-
private static String obtainAccessTokenForRealm(
264-
PolarisApiEndpoints endpoints, String realm, String clientId, String clientSecret) {
265-
MultivaluedHashMap<String, String> form = new MultivaluedHashMap<>();
266-
form.add("grant_type", "client_credentials");
267-
form.add("client_id", clientId);
268-
form.add("client_secret", clientSecret);
269-
form.add("scope", "PRINCIPAL_ROLE:ALL");
270-
try (Client httpClient = ClientBuilder.newBuilder().build();
271-
Response response =
272-
httpClient
273-
.target(String.format("%s/v1/oauth/tokens", endpoints.catalogApiEndpoint()))
274-
.request(MediaType.APPLICATION_JSON_TYPE)
275-
.header("Polaris-Realm", realm)
276-
.post(Entity.form(form))) {
277-
assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
278-
return response.readEntity(OAuthTokenResponse.class).token();
279-
}
280-
}
281246
}

0 commit comments

Comments
 (0)