Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
be48131
OpaPolarisAuthorizer
sungwy Sep 20, 2025
317cdc4
add CDI AuthorizerProducer
sungwy Sep 20, 2025
0739855
inject polarisAuthorizer in ServiceProducers CDI
sungwy Sep 22, 2025
7be0482
add integration test
sungwy Sep 23, 2025
c18d4d2
Merge branch 'main' into opa-authorizer
sungwy Sep 23, 2025
c7701cb
license
sungwy Sep 23, 2025
ec3c142
add integration tests
sungwy Sep 24, 2025
eec60c2
Merge branch 'main' into opa-authorizer
sungwy Sep 25, 2025
ed6f265
minor fixes
sungwy Sep 25, 2025
5caf1f4
adopt review feedback
sungwy Sep 26, 2025
3935c6a
remove comment
sungwy Sep 26, 2025
5ad1030
support https and bearer token authz
sungwy Sep 30, 2025
0785bdb
file token provider and token refresh
sungwy Sep 30, 2025
4b950e3
Merge branch 'main' into opa-authorizer
sungwy Sep 30, 2025
85baedc
fix
sungwy Sep 30, 2025
6421275
refactoring
sungwy Oct 1, 2025
36f687c
refactor tests, disable ssl verification in integration tests
sungwy Oct 1, 2025
c1ae608
use http in integration tests
sungwy Oct 1, 2025
6516726
remove properties from initial implementation
sungwy Oct 8, 2025
edfe61a
remove unused ssl dependencies
sungwy Oct 8, 2025
723dec1
adopt review feedback
sungwy Oct 9, 2025
479ac60
Notes about Beta
sungwy Oct 9, 2025
c946d0d
Merge branch 'main' into opa-authorizer
sungwy Oct 9, 2025
f46f97b
adopt more feedback
sungwy Oct 9, 2025
81de61e
remove JwtDecoder in favor of auth0 java-jwt
sungwy Oct 9, 2025
d014a97
use httpclient 5
sungwy Oct 9, 2025
944d005
opa http client factory refactoring
sungwy Oct 9, 2025
477839a
extensions/auth/opa refactoring
sungwy Oct 10, 2025
4252a44
fix opa tests
sungwy Oct 10, 2025
c0053f9
lint
sungwy Oct 10, 2025
0eb0a97
refactoring and cleaning up dependencies
sungwy Oct 11, 2025
7b61eee
remove old integration test files
sungwy Oct 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions extensions/auth/opa/build.gradle.kts
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")
}
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");
}
}
}
}
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();
}
}
}
Loading