Skip to content

Commit 577b32a

Browse files
authored
Implement OpaPolarisAuthorizer (#2680)
1 parent eaa0445 commit 577b32a

File tree

32 files changed

+3487
-5
lines changed

32 files changed

+3487
-5
lines changed

bom/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ dependencies {
5656
api(project(":polaris-eclipselink"))
5757
api(project(":polaris-relational-jdbc"))
5858

59+
api(project(":polaris-extensions-auth-opa"))
60+
5961
api(project(":polaris-admin"))
6062
api(project(":polaris-runtime-common"))
6163
api(project(":polaris-runtime-test-common"))
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
20+
plugins {
21+
id("polaris-server")
22+
id("org.kordamp.gradle.jandex")
23+
}
24+
25+
dependencies {
26+
implementation(project(":polaris-core"))
27+
implementation(libs.apache.httpclient5)
28+
implementation(platform(libs.jackson.bom))
29+
implementation("com.fasterxml.jackson.core:jackson-core")
30+
implementation("com.fasterxml.jackson.core:jackson-databind")
31+
implementation(libs.guava)
32+
implementation(libs.slf4j.api)
33+
implementation(libs.auth0.jwt)
34+
implementation(project(":polaris-async-api"))
35+
36+
// Iceberg dependency for ForbiddenException
37+
implementation(platform(libs.iceberg.bom))
38+
implementation("org.apache.iceberg:iceberg-api")
39+
40+
compileOnly(project(":polaris-immutables"))
41+
annotationProcessor(project(":polaris-immutables", configuration = "processor"))
42+
43+
compileOnly(libs.jakarta.annotation.api)
44+
compileOnly(libs.jakarta.enterprise.cdi.api)
45+
compileOnly(libs.jakarta.inject.api)
46+
compileOnly(libs.smallrye.config.core)
47+
48+
testCompileOnly(project(":polaris-immutables"))
49+
testAnnotationProcessor(project(":polaris-immutables", configuration = "processor"))
50+
51+
testImplementation(testFixtures(project(":polaris-core")))
52+
testImplementation(platform(libs.junit.bom))
53+
testImplementation("org.junit.jupiter:junit-jupiter")
54+
testImplementation(libs.assertj.core)
55+
testImplementation(libs.mockito.core)
56+
testImplementation(libs.threeten.extra)
57+
testImplementation(testFixtures(project(":polaris-async-api")))
58+
testImplementation(project(":polaris-async-java"))
59+
testImplementation(project(":polaris-idgen-mocks"))
60+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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.extension.auth.opa;
20+
21+
import static com.google.common.base.Preconditions.checkArgument;
22+
23+
import com.google.common.base.Strings;
24+
import io.smallrye.config.ConfigMapping;
25+
import io.smallrye.config.WithDefault;
26+
import java.net.URI;
27+
import java.nio.file.Path;
28+
import java.time.Duration;
29+
import java.util.Optional;
30+
import org.apache.polaris.immutables.PolarisImmutable;
31+
32+
/**
33+
* Configuration for OPA (Open Policy Agent) authorization.
34+
*
35+
* <p><strong>Beta Feature:</strong> OPA authorization is currently in Beta and is not a stable
36+
* release. It may undergo breaking changes in future versions. Use with caution in production
37+
* environments.
38+
*/
39+
@PolarisImmutable
40+
@ConfigMapping(prefix = "polaris.authorization.opa")
41+
public interface OpaAuthorizationConfig {
42+
43+
/** Authentication types supported by OPA authorization */
44+
enum AuthenticationType {
45+
NONE("none"),
46+
BEARER("bearer");
47+
48+
private final String value;
49+
50+
AuthenticationType(String value) {
51+
this.value = value;
52+
}
53+
54+
public String getValue() {
55+
return value;
56+
}
57+
}
58+
59+
Optional<URI> policyUri();
60+
61+
AuthenticationConfig auth();
62+
63+
HttpConfig http();
64+
65+
/** Validates the complete OPA configuration */
66+
default void validate() {
67+
checkArgument(
68+
policyUri().isPresent(), "polaris.authorization.opa.policy-uri must be configured");
69+
70+
URI uri = policyUri().get();
71+
String scheme = uri.getScheme();
72+
checkArgument(
73+
"http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme),
74+
"polaris.authorization.opa.policy-uri must use http or https scheme, but got: " + scheme);
75+
76+
auth().validate();
77+
}
78+
79+
/** HTTP client configuration for OPA communication. */
80+
@PolarisImmutable
81+
interface HttpConfig {
82+
@WithDefault("PT2S")
83+
Duration timeout();
84+
85+
@WithDefault("true")
86+
boolean verifySsl();
87+
88+
Optional<Path> trustStorePath();
89+
90+
Optional<String> trustStorePassword();
91+
}
92+
93+
/** Authentication configuration for OPA communication. */
94+
@PolarisImmutable
95+
interface AuthenticationConfig {
96+
/** Type of authentication */
97+
@WithDefault("none")
98+
AuthenticationType type();
99+
100+
/** Bearer token authentication configuration */
101+
Optional<BearerTokenConfig> bearer();
102+
103+
default void validate() {
104+
switch (type()) {
105+
case BEARER:
106+
checkArgument(
107+
bearer().isPresent(), "Bearer configuration is required when type is 'bearer'");
108+
bearer().get().validate();
109+
break;
110+
case NONE:
111+
// No authentication - nothing to validate
112+
break;
113+
default:
114+
throw new IllegalArgumentException(
115+
"Invalid authentication type: " + type() + ". Supported types: 'bearer', 'none'");
116+
}
117+
}
118+
}
119+
120+
@PolarisImmutable
121+
interface BearerTokenConfig {
122+
/** Static bearer token configuration */
123+
Optional<StaticTokenConfig> staticToken();
124+
125+
/** File-based bearer token configuration */
126+
Optional<FileBasedConfig> fileBased();
127+
128+
default void validate() {
129+
// Ensure exactly one bearer token configuration is present (mutually exclusive)
130+
checkArgument(
131+
staticToken().isPresent() ^ fileBased().isPresent(),
132+
"Exactly one of 'static-token' or 'file-based' bearer token configuration must be specified");
133+
134+
// Validate the present configuration
135+
if (staticToken().isPresent()) {
136+
staticToken().get().validate();
137+
} else {
138+
fileBased().get().validate();
139+
}
140+
}
141+
142+
/** Configuration for static bearer tokens */
143+
@PolarisImmutable
144+
interface StaticTokenConfig {
145+
/** Static bearer token value */
146+
String value();
147+
148+
default void validate() {
149+
checkArgument(
150+
!Strings.isNullOrEmpty(value()), "Static bearer token value cannot be null or empty");
151+
}
152+
}
153+
154+
/** Configuration for file-based bearer tokens */
155+
@PolarisImmutable
156+
interface FileBasedConfig {
157+
/** Path to file containing bearer token */
158+
Path path();
159+
160+
/** How often to refresh file-based bearer tokens (defaults to 5 minutes if not specified) */
161+
Optional<Duration> refreshInterval();
162+
163+
/**
164+
* Whether to automatically detect JWT tokens and use their 'exp' field for refresh timing. If
165+
* true and the token is a valid JWT with an 'exp' claim, the token will be refreshed based on
166+
* the expiration time minus the buffer, rather than the fixed refresh interval. Defaults to
167+
* true if not specified.
168+
*/
169+
Optional<Boolean> jwtExpirationRefresh();
170+
171+
/**
172+
* Buffer time before JWT expiration to refresh the token. Only used when jwtExpirationRefresh
173+
* is true and the token is a valid JWT. Defaults to 1 minute if not specified.
174+
*/
175+
Optional<Duration> jwtExpirationBuffer();
176+
177+
default void validate() {
178+
checkArgument(
179+
refreshInterval().isEmpty() || refreshInterval().get().isPositive(),
180+
"refreshInterval must be positive");
181+
checkArgument(
182+
jwtExpirationBuffer().isEmpty() || jwtExpirationBuffer().get().isPositive(),
183+
"jwtExpirationBuffer must be positive");
184+
}
185+
}
186+
}
187+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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.extension.auth.opa;
20+
21+
import java.io.FileInputStream;
22+
import java.nio.file.Path;
23+
import java.security.KeyStore;
24+
import java.security.cert.X509Certificate;
25+
import javax.net.ssl.SSLContext;
26+
import org.apache.hc.client5.http.config.RequestConfig;
27+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
28+
import org.apache.hc.client5.http.impl.classic.HttpClients;
29+
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
30+
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
31+
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
32+
import org.apache.hc.core5.ssl.SSLContexts;
33+
import org.apache.hc.core5.util.Timeout;
34+
import org.slf4j.Logger;
35+
import org.slf4j.LoggerFactory;
36+
37+
/**
38+
* Factory for creating HTTP clients configured for OPA communication with SSL support.
39+
*
40+
* <p>This factory handles the creation of Apache HttpClient instances with proper SSL
41+
* configuration, timeout settings, and connection pooling for communicating with Open Policy Agent
42+
* (OPA) servers.
43+
*/
44+
class OpaHttpClientFactory {
45+
private static final Logger logger = LoggerFactory.getLogger(OpaHttpClientFactory.class);
46+
47+
/**
48+
* Creates a configured HTTP client for OPA communication.
49+
*
50+
* @param config HTTP configuration for timeouts and SSL settings
51+
* @return configured CloseableHttpClient
52+
*/
53+
public static CloseableHttpClient createHttpClient(OpaAuthorizationConfig.HttpConfig config) {
54+
RequestConfig requestConfig =
55+
RequestConfig.custom()
56+
.setResponseTimeout(Timeout.ofMilliseconds(config.timeout().toMillis()))
57+
.build();
58+
59+
try {
60+
// Create TLS strategy based on configuration
61+
DefaultClientTlsStrategy tlsStrategy = createTlsStrategy(config);
62+
63+
// Create connection manager with the TLS strategy
64+
var connectionManager =
65+
PoolingHttpClientConnectionManagerBuilder.create()
66+
.setTlsSocketStrategy(tlsStrategy)
67+
.build();
68+
69+
return HttpClients.custom()
70+
.setConnectionManager(connectionManager)
71+
.setDefaultRequestConfig(requestConfig)
72+
.build();
73+
} catch (Exception e) {
74+
throw new RuntimeException("Failed to create HTTP client for OPA communication", e);
75+
}
76+
}
77+
78+
/**
79+
* Creates a TLS strategy based on the configuration.
80+
*
81+
* @param config HTTP configuration containing SSL settings
82+
* @return DefaultClientTlsStrategy for HTTPS connections
83+
*/
84+
private static DefaultClientTlsStrategy createTlsStrategy(
85+
OpaAuthorizationConfig.HttpConfig config) throws Exception {
86+
SSLContext sslContext = createSslContext(config);
87+
88+
if (!config.verifySsl()) {
89+
// Disable hostname verification when SSL verification is disabled
90+
return new DefaultClientTlsStrategy(sslContext, NoopHostnameVerifier.INSTANCE);
91+
} else {
92+
// Use default hostname verification when SSL verification is enabled
93+
return new DefaultClientTlsStrategy(sslContext);
94+
}
95+
}
96+
97+
/**
98+
* Creates an SSL context based on the configuration.
99+
*
100+
* @param config HTTP configuration containing SSL settings
101+
* @return SSLContext for HTTPS connections
102+
*/
103+
private static SSLContext createSslContext(OpaAuthorizationConfig.HttpConfig config)
104+
throws Exception {
105+
if (!config.verifySsl()) {
106+
// Disable SSL verification (for development/testing)
107+
logger.warn(
108+
"SSL verification is disabled for OPA server. This should only be used in development/testing environments.");
109+
return SSLContexts.custom()
110+
.loadTrustMaterial(
111+
null, (X509Certificate[] chain, String authType) -> true) // trust all certificates
112+
.build();
113+
} else if (config.trustStorePath().isPresent()) {
114+
// Load custom trust store for SSL verification
115+
Path trustStorePath = config.trustStorePath().get();
116+
logger.info("Loading custom trust store for OPA SSL verification: {}", trustStorePath);
117+
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
118+
try (FileInputStream trustStoreStream = new FileInputStream(trustStorePath.toFile())) {
119+
String trustStorePassword = config.trustStorePassword().orElse(null);
120+
trustStore.load(
121+
trustStoreStream, trustStorePassword != null ? trustStorePassword.toCharArray() : null);
122+
}
123+
return SSLContexts.custom().loadTrustMaterial(trustStore, null).build();
124+
} else {
125+
// Use default system trust store for SSL verification
126+
logger.debug("Using default system trust store for OPA SSL verification");
127+
return SSLContexts.createDefault();
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)