-
Notifications
You must be signed in to change notification settings - Fork 319
Implement OpaPolarisAuthorizer #2680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sungwy
wants to merge
41
commits into
apache:main
Choose a base branch
from
sungwy:opa-authorizer
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,327
−5
Open
Changes from 20 commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
be48131
OpaPolarisAuthorizer
sungwy 317cdc4
add CDI AuthorizerProducer
sungwy 0739855
inject polarisAuthorizer in ServiceProducers CDI
sungwy 7be0482
add integration test
sungwy c18d4d2
Merge branch 'main' into opa-authorizer
sungwy c7701cb
license
sungwy ec3c142
add integration tests
sungwy eec60c2
Merge branch 'main' into opa-authorizer
sungwy ed6f265
minor fixes
sungwy 5caf1f4
adopt review feedback
sungwy 3935c6a
remove comment
sungwy 5ad1030
support https and bearer token authz
sungwy 0785bdb
file token provider and token refresh
sungwy 4b950e3
Merge branch 'main' into opa-authorizer
sungwy 85baedc
fix
sungwy 6421275
refactoring
sungwy 36f687c
refactor tests, disable ssl verification in integration tests
sungwy c1ae608
use http in integration tests
sungwy 6516726
remove properties from initial implementation
sungwy edfe61a
remove unused ssl dependencies
sungwy 723dec1
adopt review feedback
sungwy 479ac60
Notes about Beta
sungwy c946d0d
Merge branch 'main' into opa-authorizer
sungwy f46f97b
adopt more feedback
sungwy 81de61e
remove JwtDecoder in favor of auth0 java-jwt
sungwy d014a97
use httpclient 5
sungwy 944d005
opa http client factory refactoring
sungwy 477839a
extensions/auth/opa refactoring
sungwy 4252a44
fix opa tests
sungwy c0053f9
lint
sungwy 0eb0a97
refactoring and cleaning up dependencies
sungwy 7b61eee
remove old integration test files
sungwy 44921ea
adopt review feedback and move integration tests into extensions/auth…
sungwy 652f827
fix tests
sungwy 30bd623
adopt review feedback
sungwy 00d2e5d
fix tests
sungwy 0b030e6
fix
sungwy 5e02465
fix regtest
sungwy 80ec27c
adopt more feedback
sungwy fd38aa3
add comment
sungwy fe8e450
adopt feedback
sungwy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
229 changes: 229 additions & 0 deletions
229
polaris-core/src/main/java/org/apache/polaris/core/auth/FileTokenProvider.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
/* | ||
* 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.auth; | ||
|
||
import jakarta.annotation.Nullable; | ||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.time.Duration; | ||
import java.time.Instant; | ||
import java.util.Optional; | ||
import java.util.concurrent.locks.ReadWriteLock; | ||
import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* A token provider that reads tokens from a file and automatically reloads them based on a | ||
* configurable refresh interval or JWT expiration timing. | ||
* | ||
* <p>This is particularly useful in Kubernetes environments where tokens are mounted as files and | ||
* refreshed by external systems (e.g., service account tokens, projected volumes, etc.). | ||
* | ||
* <p>The token file is expected to contain the bearer token as plain text. Leading and trailing | ||
* whitespace will be trimmed. | ||
* | ||
* <p>If JWT expiration refresh is enabled and the token is a valid JWT with an 'exp' claim, the | ||
* provider will automatically refresh the token based on the expiration time minus a configurable | ||
* buffer, rather than using the fixed refresh interval. | ||
*/ | ||
public class FileTokenProvider implements TokenProvider { | ||
|
||
private static final Logger logger = LoggerFactory.getLogger(FileTokenProvider.class); | ||
|
||
private final Path tokenFilePath; | ||
private final Duration refreshInterval; | ||
private final boolean jwtExpirationRefresh; | ||
private final Duration jwtExpirationBuffer; | ||
private final ReadWriteLock lock = new ReentrantReadWriteLock(); | ||
|
||
private volatile String cachedToken; | ||
private volatile Instant lastRefresh; | ||
private volatile Instant nextRefresh; | ||
private volatile boolean closed = false; | ||
|
||
/** | ||
* Create a new file-based token provider with basic refresh interval. | ||
* | ||
* @param tokenFilePath path to the file containing the bearer token | ||
* @param refreshInterval how often to check for token file changes | ||
*/ | ||
public FileTokenProvider(String tokenFilePath, Duration refreshInterval) { | ||
this(tokenFilePath, refreshInterval, true, Duration.ofSeconds(60)); | ||
} | ||
|
||
/** | ||
* Create a new file-based token provider with JWT expiration support. | ||
* | ||
* @param tokenFilePath path to the file containing the bearer token | ||
* @param refreshInterval how often to check for token file changes (fallback for non-JWT tokens) | ||
* @param jwtExpirationRefresh whether to use JWT expiration for refresh timing | ||
* @param jwtExpirationBuffer buffer time before JWT expiration to refresh the token | ||
*/ | ||
public FileTokenProvider( | ||
String tokenFilePath, | ||
Duration refreshInterval, | ||
boolean jwtExpirationRefresh, | ||
Duration jwtExpirationBuffer) { | ||
this.tokenFilePath = Paths.get(tokenFilePath); | ||
this.refreshInterval = refreshInterval; | ||
this.jwtExpirationRefresh = jwtExpirationRefresh; | ||
this.jwtExpirationBuffer = jwtExpirationBuffer; | ||
this.lastRefresh = Instant.MIN; // Force initial load | ||
this.nextRefresh = Instant.MIN; // Force initial calculation | ||
|
||
logger.info( | ||
"Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}", | ||
tokenFilePath, | ||
refreshInterval, | ||
jwtExpirationRefresh, | ||
jwtExpirationBuffer); | ||
} | ||
|
||
@Override | ||
@Nullable | ||
public String getToken() { | ||
if (closed) { | ||
logger.warn("Token provider is closed, returning null"); | ||
return null; | ||
} | ||
|
||
// Check if we need to refresh | ||
if (shouldRefresh()) { | ||
refreshToken(); | ||
} | ||
|
||
lock.readLock().lock(); | ||
try { | ||
return cachedToken; | ||
} finally { | ||
lock.readLock().unlock(); | ||
} | ||
} | ||
|
||
@Override | ||
public void close() { | ||
closed = true; | ||
lock.writeLock().lock(); | ||
try { | ||
cachedToken = null; | ||
logger.info("File token provider closed"); | ||
} finally { | ||
lock.writeLock().unlock(); | ||
} | ||
} | ||
|
||
private boolean shouldRefresh() { | ||
return Instant.now().isAfter(nextRefresh); | ||
} | ||
|
||
private void refreshToken() { | ||
lock.writeLock().lock(); | ||
try { | ||
// Double-check pattern - another thread might have refreshed while we waited for the lock | ||
if (!shouldRefresh()) { | ||
return; | ||
} | ||
|
||
String newToken = loadTokenFromFile(); | ||
cachedToken = newToken; | ||
lastRefresh = Instant.now(); | ||
|
||
// Calculate next refresh time based on JWT expiration or fixed interval | ||
nextRefresh = calculateNextRefresh(newToken); | ||
|
||
if (logger.isDebugEnabled()) { | ||
logger.debug( | ||
"Token refreshed from file: {} (token present: {}), next refresh: {}", | ||
tokenFilePath, | ||
newToken != null && !newToken.isEmpty(), | ||
nextRefresh); | ||
} | ||
|
||
} finally { | ||
lock.writeLock().unlock(); | ||
} | ||
} | ||
|
||
/** Calculate when the next refresh should occur based on JWT expiration or fixed interval. */ | ||
private Instant calculateNextRefresh(@Nullable String token) { | ||
if (token == null || !jwtExpirationRefresh) { | ||
// Use fixed interval | ||
return lastRefresh.plus(refreshInterval); | ||
} | ||
|
||
// Attempt to parse as JWT and extract expiration | ||
Optional<Instant> expiration = JwtDecoder.getExpirationTime(token); | ||
|
||
if (expiration.isPresent()) { | ||
// Refresh before expiration minus buffer | ||
Instant refreshTime = expiration.get().minus(jwtExpirationBuffer); | ||
|
||
// Ensure refresh time is in the future and not too soon (at least 1 second) | ||
Instant minRefreshTime = Instant.now().plus(Duration.ofSeconds(1)); | ||
if (refreshTime.isBefore(minRefreshTime)) { | ||
logger.warn( | ||
"JWT expires too soon ({}), using minimum refresh interval instead", expiration.get()); | ||
return lastRefresh.plus(refreshInterval); | ||
} | ||
|
||
logger.debug( | ||
"Using JWT expiration-based refresh: token expires at {}, refreshing at {}", | ||
expiration.get(), | ||
refreshTime); | ||
return refreshTime; | ||
} | ||
|
||
// Fall back to fixed interval (token is not a valid JWT or has no expiration) | ||
logger.debug("Token is not a valid JWT or has no expiration, using fixed refresh interval"); | ||
return lastRefresh.plus(refreshInterval); | ||
} | ||
|
||
@Nullable | ||
private String loadTokenFromFile() { | ||
try { | ||
if (!Files.exists(tokenFilePath)) { | ||
logger.warn("Token file does not exist: {}", tokenFilePath); | ||
return null; | ||
} | ||
|
||
if (!Files.isReadable(tokenFilePath)) { | ||
logger.warn("Token file is not readable: {}", tokenFilePath); | ||
return null; | ||
} | ||
|
||
String content = Files.readString(tokenFilePath, StandardCharsets.UTF_8); | ||
String token = content.trim(); | ||
|
||
if (token.isEmpty()) { | ||
logger.warn("Token file is empty: {}", tokenFilePath); | ||
return null; | ||
} | ||
|
||
return token; | ||
|
||
} catch (IOException e) { | ||
logger.error("Failed to read token from file: {}", tokenFilePath, e); | ||
return null; | ||
} | ||
} | ||
} |
94 changes: 94 additions & 0 deletions
94
polaris-core/src/main/java/org/apache/polaris/core/auth/JwtDecoder.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/* | ||
* 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.auth; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import java.nio.charset.StandardCharsets; | ||
import java.time.Instant; | ||
import java.util.Base64; | ||
import java.util.Optional; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* Simple JWT decoder that extracts claims without signature verification. This is used solely for | ||
* reading the expiration time from JWT tokens to determine refresh timing. | ||
*/ | ||
public class JwtDecoder { | ||
flyrain marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
private static final Logger LOG = LoggerFactory.getLogger(JwtDecoder.class); | ||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||
|
||
/** | ||
* Decode a JWT token and extract the expiration time if present. | ||
* | ||
* @param token the JWT token string | ||
* @return the expiration time as an Instant, or empty if not present or invalid | ||
*/ | ||
public static Optional<Instant> getExpirationTime(String token) { | ||
return decodePayload(token).flatMap(JwtDecoder::getExpirationTime); | ||
} | ||
|
||
/** | ||
* Decode the payload of a JWT token without signature verification. | ||
* | ||
* @param token the JWT token string | ||
* @return the decoded payload as a JsonNode, or empty if invalid | ||
*/ | ||
public static Optional<JsonNode> decodePayload(String token) { | ||
try { | ||
String[] parts = token.split("\\."); | ||
if (parts.length != 3) { | ||
LOG.debug("Invalid JWT format: expected 3 parts separated by dots"); | ||
return Optional.empty(); | ||
} | ||
|
||
// Decode the payload (second part) | ||
String payload = parts[1]; | ||
byte[] decodedBytes = Base64.getUrlDecoder().decode(payload); | ||
String payloadJson = new String(decodedBytes, StandardCharsets.UTF_8); | ||
|
||
// Parse JSON | ||
JsonNode payloadNode = OBJECT_MAPPER.readTree(payloadJson); | ||
return Optional.of(payloadNode); | ||
|
||
} catch (Exception e) { | ||
LOG.debug("Failed to decode JWT token: {}", e.getMessage()); | ||
return Optional.empty(); | ||
} | ||
} | ||
|
||
/** | ||
* Extract the expiration time from a decoded JWT payload. | ||
* | ||
* @param payloadNode the decoded JWT payload | ||
* @return the expiration time as an Instant, or empty if not present or invalid | ||
*/ | ||
public static Optional<Instant> getExpirationTime(JsonNode payloadNode) { | ||
JsonNode expNode = payloadNode.get("exp"); | ||
|
||
if (expNode == null || !expNode.isNumber()) { | ||
LOG.debug("JWT does not contain a valid 'exp' claim"); | ||
return Optional.empty(); | ||
} | ||
|
||
long expSeconds = expNode.asLong(); | ||
return Optional.of(Instant.ofEpochSecond(expSeconds)); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.