-
Notifications
You must be signed in to change notification settings - Fork 314
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
32
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.
Open
Changes from all commits
Commits
Show all changes
32 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 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
plugins { | ||
id("polaris-server") | ||
id("org.kordamp.gradle.jandex") | ||
} | ||
|
||
dependencies { | ||
implementation(project(":polaris-core")) | ||
implementation(libs.apache.httpclient5) | ||
implementation(platform(libs.jackson.bom)) | ||
implementation("com.fasterxml.jackson.core:jackson-core") | ||
implementation("com.fasterxml.jackson.core:jackson-databind") | ||
implementation(libs.guava) | ||
implementation(libs.slf4j.api) | ||
implementation(libs.auth0.jwt) | ||
|
||
// Iceberg dependency for ForbiddenException | ||
implementation(platform(libs.iceberg.bom)) | ||
implementation("org.apache.iceberg:iceberg-api") | ||
|
||
compileOnly(libs.jakarta.annotation.api) | ||
compileOnly(libs.jakarta.enterprise.cdi.api) | ||
compileOnly(libs.jakarta.inject.api) | ||
compileOnly(libs.smallrye.config.core) | ||
|
||
testImplementation(testFixtures(project(":polaris-core"))) | ||
testImplementation(project(":polaris-runtime-test-common")) | ||
testImplementation(platform(libs.junit.bom)) | ||
testImplementation("org.junit.jupiter:junit-jupiter") | ||
testImplementation(libs.assertj.core) | ||
testImplementation(libs.mockito.core) | ||
testImplementation(platform(libs.quarkus.bom)) | ||
testImplementation("io.quarkus:quarkus-junit5") | ||
testImplementation("io.rest-assured:rest-assured") | ||
testImplementation("com.github.tomakehurst:wiremock:3.0.1") | ||
testImplementation(platform(libs.testcontainers.bom)) | ||
testImplementation("org.testcontainers:junit-jupiter") | ||
} |
170 changes: 170 additions & 0 deletions
170
.../auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.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,170 @@ | ||
/* | ||
* 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.extension.auth.opa; | ||
|
||
import static com.google.common.base.Preconditions.checkArgument; | ||
|
||
import io.smallrye.config.ConfigMapping; | ||
import io.smallrye.config.WithDefault; | ||
import java.util.Optional; | ||
|
||
/** | ||
* Configuration for OPA (Open Policy Agent) authorization. | ||
* | ||
* <p><strong>Beta Feature:</strong> OPA authorization is currently in Beta and is not a stable | ||
* release. It may undergo breaking changes in future versions. Use with caution in production | ||
* environments. | ||
*/ | ||
@ConfigMapping(prefix = "polaris.authorization.opa") | ||
public interface OpaAuthorizationConfig { | ||
Optional<String> url(); | ||
|
||
Optional<String> policyPath(); | ||
|
||
Optional<AuthenticationConfig> auth(); | ||
|
||
Optional<HttpConfig> http(); | ||
|
||
/** Validates the complete OPA configuration */ | ||
default void validate() { | ||
checkArgument(url().isPresent() && !url().get().isBlank(), "OPA URL cannot be null or empty"); | ||
checkArgument( | ||
policyPath().isPresent() && !policyPath().get().isBlank(), | ||
"OPA policy path cannot be null or empty"); | ||
checkArgument(auth().isPresent(), "Authentication configuration is required"); | ||
|
||
auth().get().validate(); | ||
} | ||
|
||
/** HTTP client configuration for OPA communication. */ | ||
interface HttpConfig { | ||
@WithDefault("2000") | ||
int timeoutMs(); | ||
|
||
@WithDefault("true") | ||
boolean verifySsl(); | ||
|
||
Optional<String> trustStorePath(); | ||
|
||
Optional<String> trustStorePassword(); | ||
} | ||
|
||
/** Authentication configuration for OPA communication. */ | ||
interface AuthenticationConfig { | ||
/** Type of authentication */ | ||
@WithDefault("none") | ||
String type(); | ||
|
||
/** Bearer token authentication configuration */ | ||
Optional<BearerTokenConfig> bearer(); | ||
|
||
default void validate() { | ||
switch (type()) { | ||
case "bearer": | ||
checkArgument( | ||
bearer().isPresent(), "Bearer configuration is required when type is 'bearer'"); | ||
bearer().get().validate(); | ||
break; | ||
case "none": | ||
// No authentication - nothing to validate | ||
break; | ||
default: | ||
throw new IllegalArgumentException( | ||
"Invalid authentication type: " + type() + ". Supported types: 'bearer', 'none'"); | ||
} | ||
} | ||
} | ||
|
||
interface BearerTokenConfig { | ||
/** Type of bearer token configuration */ | ||
@WithDefault("static-token") | ||
String type(); | ||
|
||
/** Static bearer token configuration */ | ||
Optional<StaticTokenConfig> staticToken(); | ||
|
||
/** File-based bearer token configuration */ | ||
Optional<FileBasedConfig> fileBased(); | ||
|
||
default void validate() { | ||
switch (type()) { | ||
case "static-token": | ||
checkArgument( | ||
staticToken().isPresent(), | ||
"Static token configuration is required when type is 'static-token'"); | ||
staticToken().get().validate(); | ||
break; | ||
case "file-based": | ||
checkArgument( | ||
fileBased().isPresent(), | ||
"File-based configuration is required when type is 'file-based'"); | ||
fileBased().get().validate(); | ||
break; | ||
default: | ||
throw new IllegalArgumentException( | ||
"Invalid bearer token type: " + type() + ". Must be 'static-token' or 'file-based'"); | ||
} | ||
} | ||
|
||
/** Configuration for static bearer tokens */ | ||
interface StaticTokenConfig { | ||
/** Static bearer token value */ | ||
Optional<String> value(); | ||
|
||
default void validate() { | ||
checkArgument( | ||
value().isPresent() && !value().get().isBlank(), | ||
"Static bearer token value cannot be null or empty"); | ||
} | ||
} | ||
|
||
/** Configuration for file-based bearer tokens */ | ||
interface FileBasedConfig { | ||
/** Path to file containing bearer token */ | ||
Optional<String> path(); | ||
|
||
/** How often to refresh file-based bearer tokens (in seconds) */ | ||
@WithDefault("300") | ||
int refreshInterval(); | ||
|
||
/** | ||
* Whether to automatically detect JWT tokens and use their 'exp' field for refresh timing. If | ||
* true and the token is a valid JWT with an 'exp' claim, the token will be refreshed based on | ||
* the expiration time minus the buffer, rather than the fixed refresh interval. | ||
*/ | ||
@WithDefault("true") | ||
boolean jwtExpirationRefresh(); | ||
|
||
/** | ||
* Buffer time in seconds before JWT expiration to refresh the token. Only used when | ||
* jwtExpirationRefresh is true and the token is a valid JWT. Default is 60 seconds. | ||
*/ | ||
@WithDefault("60") | ||
int jwtExpirationBuffer(); | ||
|
||
default void validate() { | ||
checkArgument( | ||
path().isPresent() && !path().get().isBlank(), | ||
"Bearer token file path cannot be null or empty"); | ||
checkArgument(refreshInterval() > 0, "refreshInterval must be greater than 0"); | ||
checkArgument(jwtExpirationBuffer() > 0, "jwtExpirationBuffer must be greater than 0"); | ||
} | ||
} | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
...ns/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaHttpClientFactory.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,131 @@ | ||
/* | ||
* 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.extension.auth.opa; | ||
|
||
import com.google.common.base.Strings; | ||
import java.io.FileInputStream; | ||
import java.security.KeyStore; | ||
import java.security.cert.X509Certificate; | ||
import javax.net.ssl.SSLContext; | ||
import org.apache.hc.client5.http.config.RequestConfig; | ||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; | ||
import org.apache.hc.client5.http.impl.classic.HttpClients; | ||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; | ||
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; | ||
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; | ||
import org.apache.hc.core5.ssl.SSLContexts; | ||
import org.apache.hc.core5.util.Timeout; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* Factory for creating HTTP clients configured for OPA communication with SSL support. | ||
* | ||
* <p>This factory handles the creation of Apache HttpClient instances with proper SSL | ||
* configuration, timeout settings, and connection pooling for communicating with Open Policy Agent | ||
* (OPA) servers. | ||
*/ | ||
public class OpaHttpClientFactory { | ||
private static final Logger LOGGER = LoggerFactory.getLogger(OpaHttpClientFactory.class); | ||
|
||
/** | ||
* Creates a configured HTTP client for OPA communication. | ||
* | ||
* @param config HTTP configuration for timeouts and SSL settings | ||
* @return configured CloseableHttpClient | ||
*/ | ||
public static CloseableHttpClient createHttpClient(OpaAuthorizationConfig.HttpConfig config) { | ||
RequestConfig requestConfig = | ||
RequestConfig.custom() | ||
.setResponseTimeout(Timeout.ofMilliseconds(config.timeoutMs())) | ||
.build(); | ||
|
||
try { | ||
// Create TLS strategy based on configuration | ||
DefaultClientTlsStrategy tlsStrategy = createTlsStrategy(config); | ||
|
||
// Create connection manager with the TLS strategy | ||
var connectionManager = | ||
PoolingHttpClientConnectionManagerBuilder.create() | ||
.setTlsSocketStrategy(tlsStrategy) | ||
.build(); | ||
|
||
return HttpClients.custom() | ||
.setConnectionManager(connectionManager) | ||
.setDefaultRequestConfig(requestConfig) | ||
.build(); | ||
} catch (Exception e) { | ||
throw new RuntimeException("Failed to create HTTP client for OPA communication", e); | ||
} | ||
} | ||
|
||
/** | ||
* Creates a TLS strategy based on the configuration. | ||
* | ||
* @param config HTTP configuration containing SSL settings | ||
* @return DefaultClientTlsStrategy for HTTPS connections | ||
*/ | ||
private static DefaultClientTlsStrategy createTlsStrategy( | ||
OpaAuthorizationConfig.HttpConfig config) throws Exception { | ||
SSLContext sslContext = createSslContext(config); | ||
|
||
if (!config.verifySsl()) { | ||
// Disable hostname verification when SSL verification is disabled | ||
return new DefaultClientTlsStrategy(sslContext, NoopHostnameVerifier.INSTANCE); | ||
} else { | ||
// Use default hostname verification when SSL verification is enabled | ||
return new DefaultClientTlsStrategy(sslContext); | ||
} | ||
} | ||
|
||
/** | ||
* Creates an SSL context based on the configuration. | ||
* | ||
* @param config HTTP configuration containing SSL settings | ||
* @return SSLContext for HTTPS connections | ||
*/ | ||
private static SSLContext createSslContext(OpaAuthorizationConfig.HttpConfig config) | ||
throws Exception { | ||
if (!config.verifySsl()) { | ||
// Disable SSL verification (for development/testing) | ||
LOGGER.warn( | ||
"SSL verification is disabled for OPA server. This should only be used in development/testing environments."); | ||
return SSLContexts.custom() | ||
.loadTrustMaterial( | ||
null, (X509Certificate[] chain, String authType) -> true) // trust all certificates | ||
.build(); | ||
} else if (config.trustStorePath().isPresent() | ||
&& !Strings.isNullOrEmpty(config.trustStorePath().get())) { | ||
// Load custom trust store for SSL verification | ||
String trustStorePath = config.trustStorePath().get(); | ||
LOGGER.info("Loading custom trust store for OPA SSL verification: {}", trustStorePath); | ||
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); | ||
try (FileInputStream trustStoreStream = new FileInputStream(trustStorePath)) { | ||
String trustStorePassword = config.trustStorePassword().orElse(null); | ||
trustStore.load( | ||
trustStoreStream, trustStorePassword != null ? trustStorePassword.toCharArray() : null); | ||
} | ||
return SSLContexts.custom().loadTrustMaterial(trustStore, null).build(); | ||
} else { | ||
// Use default system trust store for SSL verification | ||
LOGGER.debug("Using default system trust store for OPA SSL verification"); | ||
return SSLContexts.createDefault(); | ||
} | ||
} | ||
} |
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.