You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by di...@apache.org on 2026/07/14 18:28:10 UTC
(polaris) branch main updated: fix(authz): include realm identifier in OPA authorization input for tenant isolation (#4992)
This is an automated email from the ASF dual-hosted git repository.
dimas-b pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push:
new d0a8dff401 fix(authz): include realm identifier in OPA authorization input for tenant isolation (#4992)
d0a8dff401 is described below
commit d0a8dff401e30cab1df3ca6d0e133816e80e9c10
Author: vignesh_A <16...@users.noreply.github.com>
AuthorDate: Tue Jul 14 23:58:06 2026 +0530
fix(authz): include realm identifier in OPA authorization input for tenant isolation (#4992)
* fix(authz): include realm identifier in OPA authorization input for tenant isolation
The OPA authorizer was not receiving any realm/tenant information.
This meant that for the same principal/role/resource names, the
authorization input sent to OPA was identical across realms (except
for the random request_id).
- Add optional 'realm' field to the Context model
- Thread realm from RealmContext in OpaPolarisAuthorizerFactory
(same pattern as RangerPolarisAuthorizerFactory)
- Update authorizer to include it in buildContext()
- Regenerate schema + update test/docs/CHANGELOG
This fixes cross-realm authorization confusion when using OPA
with multi-realm + shared policy.
* fix(authz): make OPA realm required and inject via constructor
---
CHANGELOG.md | 2 +-
extensions/auth/opa/opa-input-schema.json | 4 +
.../extension/auth/opa/OpaPolarisAuthorizer.java | 10 +-
.../auth/opa/OpaPolarisAuthorizerFactory.java | 15 +-
.../polaris/extension/auth/opa/model/Context.java | 8 ++
.../polaris/extension/auth/opa/model/README.md | 1 +
.../auth/opa/OpaPolarisAuthorizerFactoryTest.java | 157 ++++++++++++++++++++-
.../auth/opa/OpaPolarisAuthorizerTest.java | 154 +++++++++++++++++---
8 files changed, 326 insertions(+), 25 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19c2c3163c..221d952843 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -61,6 +61,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
- Async task execution (table cleanup, manifest and batch file cleanup) now retries when a handler returns false on transient errors (e.g. IO or delete failures). Previously `false` was swallowed with only a warning log and the task was never retried via the existing retry mechanism.
- `TableCleanupTaskHandler` now clamps `TABLE_METADATA_CLEANUP_BATCH_SIZE` to at least 1. Previously a non-positive realm override caused an infinite loop (0) or `IllegalArgumentException` (<0) when splitting metadata files for cleanup.
- `ManifestFileCleanupTaskHandler` now handles Iceberg v2 delete manifests in addition to data manifests. Previously, `DROP TABLE PURGE` on a v2 table that had been updated via merge-on-read DML left position-delete files and their manifests as orphans in object storage; the cleanup task failed silently because `ManifestFiles.read()` rejects delete manifests.
+- OPA authorizer now includes the realm identifier in the authorization context sent to OPA (`input.context.realm`). This ensures OPA policies can enforce tenant isolation across realms, preventing potential collisions if identical principal or resource names exist in different realms.
## [1.6.0]
@@ -99,7 +100,6 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
- The token broker now builds the JWT `Algorithm` and `JWTVerifier` once per realm in the `TokenBrokerFactory` and reuses them across requests, instead of rebuilding them on every `verify()`/`sign()` call on the request-scoped broker. For deployments using file-based symmetric secrets, the secret is now read once per realm (at first use) rather than on every JWT operation; rotating the on-disk secret requires a restart.
### Fixes
-
- Fixed a boundary condition in GCS downscoped credential generation (`GcpCredentialsStorageIntegration`). Locations without a trailing slash could previously grant access to sibling object prefixes via the generated CEL conditions for `resource.name` and list prefixes. Granted paths are now normalized to a directory prefix (with a trailing slash) before the CEL conditions are built, so sibling prefixes can no longer satisfy the `startsWith` checks.
- Fixed `NullPointerException` during `dropEntity` when an entity referenced by a grant had been concurrently removed (or purged). `lookupEntities` can return null entries for dropped entities; these are now skipped safely.
- `RateLimiterFilter` now returns an Iceberg-compatible `ErrorResponse` JSON body on HTTP 429, with `Content-Type: application/json`. Previously the body was empty, causing Iceberg REST clients to surface an opaque error.
diff --git a/extensions/auth/opa/opa-input-schema.json b/extensions/auth/opa/opa-input-schema.json
index a5fd615ac2..0a5b22d18f 100644
--- a/extensions/auth/opa/opa-input-schema.json
+++ b/extensions/auth/opa/opa-input-schema.json
@@ -69,6 +69,10 @@
"request_id" : {
"type" : "string",
"required" : true
+ },
+ "realm" : {
+ "type" : "string",
+ "required" : true
}
}
}
diff --git a/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java b/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java
index 7799bb22a5..a2664f5e9a 100644
--- a/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java
+++ b/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java
@@ -85,9 +85,10 @@ class OpaPolarisAuthorizer implements PolarisAuthorizer {
private final BearerTokenProvider tokenProvider;
private final CloseableHttpClient httpClient;
private final ObjectMapper objectMapper;
+ private final String realm;
/**
- * Public constructor that accepts a complete policy URI.
+ * Public constructor that accepts a complete policy URI and the current realm identifier.
*
* @param policyUri The required URI for the OPA endpoint. For example, {@code
* https://opa.example.com/v1/polaris/allow}.
@@ -96,17 +97,20 @@ class OpaPolarisAuthorizer implements PolarisAuthorizer {
* @param objectMapper Jackson ObjectMapper for JSON serialization (required). Shared across
* authorizer instances to avoid initialization overhead.
* @param tokenProvider Token provider for authentication (optional)
+ * @param realm The realm identifier (from RealmContext) for isolation in OPA policies.
*/
public OpaPolarisAuthorizer(
@NonNull URI policyUri,
@NonNull CloseableHttpClient httpClient,
@NonNull ObjectMapper objectMapper,
- @Nullable BearerTokenProvider tokenProvider) {
+ @Nullable BearerTokenProvider tokenProvider,
+ @NonNull String realm) {
this.policyUri = policyUri;
this.tokenProvider = tokenProvider;
this.httpClient = httpClient;
this.objectMapper = objectMapper;
+ this.realm = realm;
}
/**
@@ -334,7 +338,7 @@ class OpaPolarisAuthorizer implements PolarisAuthorizer {
}
private ImmutableContext buildContext() {
- return ImmutableContext.builder().requestId(UUID.randomUUID().toString()).build();
+ return ImmutableContext.builder().requestId(UUID.randomUUID().toString()).realm(realm).build();
}
private ImmutableResource buildResource(
diff --git a/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java b/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java
index e7e3810cb4..a0c3bef247 100644
--- a/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java
+++ b/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java
@@ -34,6 +34,7 @@ import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.polaris.core.auth.PolarisAuthorizer;
import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.core.context.RealmContext;
import org.apache.polaris.extension.auth.opa.token.BearerTokenProvider;
import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
@@ -52,15 +53,20 @@ class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
private final Clock clock;
private final ObjectMapper objectMapper;
private final AsyncExec asyncExec;
+ private final RealmContext realmContext;
private CloseableHttpClient httpClient;
private BearerTokenProvider bearerTokenProvider;
@Inject
public OpaPolarisAuthorizerFactory(
- OpaAuthorizationConfig opaConfig, Clock clock, AsyncExec asyncExec) {
+ OpaAuthorizationConfig opaConfig,
+ Clock clock,
+ AsyncExec asyncExec,
+ RealmContext realmContext) {
this.opaConfig = opaConfig;
this.clock = clock;
this.asyncExec = asyncExec;
+ this.realmContext = realmContext;
this.objectMapper = JsonMapper.builder().build();
}
@@ -96,7 +102,12 @@ class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
new IllegalStateException(
"OPA policy URI must be configured via polaris.authorization.opa.policy-uri"));
- return new OpaPolarisAuthorizer(policyUri, httpClient, objectMapper, bearerTokenProvider);
+ return new OpaPolarisAuthorizer(
+ policyUri,
+ httpClient,
+ objectMapper,
+ bearerTokenProvider,
+ realmContext.getRealmIdentifier());
}
@PreDestroy
diff --git a/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/model/Context.java b/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/model/Context.java
index ad3ded12ff..c87c36b630 100644
--- a/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/model/Context.java
+++ b/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/model/Context.java
@@ -36,4 +36,12 @@ import org.apache.polaris.immutables.PolarisImmutable;
public interface Context {
/** A unique identifier for correlating this request with OPA server logs. */
String requestId();
+
+ /**
+ * The realm identifier for this authorization request.
+ *
+ * <p>This enables OPA policies to enforce isolation when the same principal names or resource
+ * names exist across different realms (multi-realm deployments using a shared OPA policy).
+ */
+ String realm();
}
diff --git a/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/model/README.md b/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/model/README.md
index 8c3bd5ee9c..5df7637253 100644
--- a/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/model/README.md
+++ b/extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/model/README.md
@@ -66,6 +66,7 @@ Individual resource with hierarchical context:
### Context
Request metadata:
- `request_id`: Unique correlation ID for logging
+- `realm`: Realm identifier (allows OPA policies to scope decisions by realm)
## Schema Evolution
diff --git a/extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactoryTest.java b/extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactoryTest.java
index 59e4420b7d..16175fa156 100644
--- a/extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactoryTest.java
+++ b/extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactoryTest.java
@@ -19,15 +19,31 @@
package org.apache.polaris.extension.auth.opa;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.Mockito.mock;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
import java.time.Duration;
+import java.util.Map;
+import java.util.Set;
+import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
+import org.apache.polaris.core.auth.PolarisPrincipal;
import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.core.context.RealmContext;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
import org.apache.polaris.nosql.async.java.JavaPoolAsyncExec;
import org.junit.jupiter.api.Test;
@@ -62,8 +78,9 @@ public class OpaPolarisAuthorizerFactoryTest {
.build();
try (JavaPoolAsyncExec asyncExec = new JavaPoolAsyncExec()) {
+ RealmContext realmContext = () -> "test-realm";
OpaPolarisAuthorizerFactory factory =
- new OpaPolarisAuthorizerFactory(opaConfig, Clock.systemUTC(), asyncExec);
+ new OpaPolarisAuthorizerFactory(opaConfig, Clock.systemUTC(), asyncExec, realmContext);
// Create authorizer
RealmConfig realmConfig = mock(RealmConfig.class);
@@ -106,8 +123,9 @@ public class OpaPolarisAuthorizerFactoryTest {
.build();
try (JavaPoolAsyncExec asyncExec = new JavaPoolAsyncExec()) {
+ RealmContext realmContext = () -> "test-realm";
OpaPolarisAuthorizerFactory factory =
- new OpaPolarisAuthorizerFactory(opaConfig, Clock.systemUTC(), asyncExec);
+ new OpaPolarisAuthorizerFactory(opaConfig, Clock.systemUTC(), asyncExec, realmContext);
// Create authorizer
RealmConfig realmConfig = mock(RealmConfig.class);
@@ -151,8 +169,9 @@ public class OpaPolarisAuthorizerFactoryTest {
.build();
try (JavaPoolAsyncExec asyncExec = new JavaPoolAsyncExec()) {
+ RealmContext realmContext = () -> "test-realm";
OpaPolarisAuthorizerFactory factory =
- new OpaPolarisAuthorizerFactory(opaConfig, Clock.systemUTC(), asyncExec);
+ new OpaPolarisAuthorizerFactory(opaConfig, Clock.systemUTC(), asyncExec, realmContext);
// Create authorizer
RealmConfig realmConfig = mock(RealmConfig.class);
@@ -161,4 +180,136 @@ public class OpaPolarisAuthorizerFactoryTest {
assertThat(authorizer).isNotNull();
}
}
+
+ @Test
+ public void testFactoryPassesRealmToAuthorizerContext() throws Exception {
+ final String[] capturedRequestBody = new String[1];
+
+ HttpServer server = createServerWithRequestCapture(capturedRequestBody);
+ try {
+ OpaAuthorizationConfig opaConfig =
+ ImmutableOpaAuthorizationConfig.builder()
+ .policyUri(
+ URI.create(
+ "http://localhost:"
+ + server.getAddress().getPort()
+ + "/v1/data/polaris/allow"))
+ .auth(
+ ImmutableAuthenticationConfig.builder()
+ .type(OpaAuthorizationConfig.AuthenticationType.NONE)
+ .build())
+ .http(
+ ImmutableHttpConfig.builder()
+ .timeout(Duration.ofSeconds(2))
+ .verifySsl(true)
+ .build())
+ .build();
+
+ try (JavaPoolAsyncExec asyncExec = new JavaPoolAsyncExec()) {
+ RealmContext realmContext = () -> "factory-realm";
+ OpaPolarisAuthorizerFactory factory =
+ new OpaPolarisAuthorizerFactory(opaConfig, Clock.systemUTC(), asyncExec, realmContext);
+
+ factory.initialize();
+ OpaPolarisAuthorizer authorizer =
+ (OpaPolarisAuthorizer) factory.create(mock(RealmConfig.class));
+
+ PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("admin"));
+ assertThatNoException()
+ .isThrownBy(
+ () ->
+ authorizer.authorizeOrThrow(
+ principal,
+ Set.of(),
+ PolarisAuthorizableOperation.GET_CATALOG,
+ (PolarisResolvedPathWrapper) null,
+ (PolarisResolvedPathWrapper) null));
+
+ ObjectMapper mapper = JsonMapper.builder().build();
+ JsonNode root = mapper.readTree(capturedRequestBody[0]);
+ assertThat(root.path("input").path("context").get("realm").asText())
+ .isEqualTo("factory-realm");
+ factory.cleanup();
+ }
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ public void testFactoryUsesDistinctRealmValues() throws Exception {
+ final String[] capturedRequestBody = new String[1];
+
+ HttpServer server = createServerWithRequestCapture(capturedRequestBody);
+ try {
+ OpaAuthorizationConfig opaConfig =
+ ImmutableOpaAuthorizationConfig.builder()
+ .policyUri(
+ URI.create(
+ "http://localhost:"
+ + server.getAddress().getPort()
+ + "/v1/data/polaris/allow"))
+ .auth(
+ ImmutableAuthenticationConfig.builder()
+ .type(OpaAuthorizationConfig.AuthenticationType.NONE)
+ .build())
+ .http(
+ ImmutableHttpConfig.builder()
+ .timeout(Duration.ofSeconds(2))
+ .verifySsl(true)
+ .build())
+ .build();
+
+ try (JavaPoolAsyncExec asyncExec = new JavaPoolAsyncExec()) {
+ RealmContext realmContext = () -> "realm-b";
+ OpaPolarisAuthorizerFactory factory =
+ new OpaPolarisAuthorizerFactory(opaConfig, Clock.systemUTC(), asyncExec, realmContext);
+
+ factory.initialize();
+ OpaPolarisAuthorizer authorizer =
+ (OpaPolarisAuthorizer) factory.create(mock(RealmConfig.class));
+
+ PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("admin"));
+ assertThatNoException()
+ .isThrownBy(
+ () ->
+ authorizer.authorizeOrThrow(
+ principal,
+ Set.of(),
+ PolarisAuthorizableOperation.GET_CATALOG,
+ (PolarisResolvedPathWrapper) null,
+ (PolarisResolvedPathWrapper) null));
+
+ ObjectMapper mapper = JsonMapper.builder().build();
+ JsonNode root = mapper.readTree(capturedRequestBody[0]);
+ assertThat(root.path("input").path("context").get("realm").asText()).isEqualTo("realm-b");
+ factory.cleanup();
+ }
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ private HttpServer createServerWithRequestCapture(String[] capturedRequestBody)
+ throws IOException {
+ HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
+ server.createContext(
+ "/v1/data/polaris/allow",
+ new HttpHandler() {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ byte[] requestBytes = exchange.getRequestBody().readAllBytes();
+ capturedRequestBody[0] = new String(requestBytes, StandardCharsets.UTF_8);
+
+ String response = "{\"result\":{\"allow\":true}}";
+ exchange.getResponseHeaders().add("Content-Type", "application/json");
+ exchange.sendResponseHeaders(200, response.length());
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(response.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+ });
+ server.start();
+ return server;
+ }
}
diff --git a/extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java b/extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java
index bfae693671..8e236d8393 100644
--- a/extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java
+++ b/extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java
@@ -87,7 +87,11 @@ public class OpaPolarisAuthorizerTest {
"http://localhost:" + server.getAddress().getPort() + "/v1/data/polaris/allow");
OpaPolarisAuthorizer authorizer =
new OpaPolarisAuthorizer(
- policyUri, HttpClients.createDefault(), JsonMapper.builder().build(), null);
+ policyUri,
+ HttpClients.createDefault(),
+ JsonMapper.builder().build(),
+ null,
+ "test-realm");
PolarisPrincipal principal =
PolarisPrincipal.of("eve", Map.of("department", "finance"), Set.of("auditor"));
@@ -115,6 +119,10 @@ public class OpaPolarisAuthorizerTest {
assertThat(input.has("action")).as("Input should have 'action' field").isTrue();
assertThat(input.has("resource")).as("Input should have 'resource' field").isTrue();
assertThat(input.has("context")).as("Input should have 'context' field").isTrue();
+
+ // Verify realm is included for tenant isolation
+ assertThat(input.get("context").has("realm")).as("context should contain realm").isTrue();
+ assertThat(input.get("context").get("realm").asText()).isEqualTo("test-realm");
} finally {
server.stop(0);
}
@@ -132,7 +140,11 @@ public class OpaPolarisAuthorizerTest {
"http://localhost:" + server.getAddress().getPort() + "/v1/data/polaris/allow");
OpaPolarisAuthorizer authorizer =
new OpaPolarisAuthorizer(
- policyUri, HttpClients.createDefault(), JsonMapper.builder().build(), null);
+ policyUri,
+ HttpClients.createDefault(),
+ JsonMapper.builder().build(),
+ null,
+ "prod-realm");
// Set up a realistic principal
PolarisPrincipal principal =
@@ -208,6 +220,10 @@ public class OpaPolarisAuthorizerTest {
assertThat(input.has("resource")).as("Input should have 'resource' field").isTrue();
assertThat(input.has("context")).as("Input should have 'context' field").isTrue();
+ // Verify realm for tenant isolation
+ assertThat(input.get("context").has("realm")).isTrue();
+ assertThat(input.get("context").get("realm").asText()).isEqualTo("prod-realm");
+
// Verify actor details
var actor = input.get("actor");
assertThat(actor.has("principal")).as("Actor should have 'principal' field").isTrue();
@@ -263,7 +279,11 @@ public class OpaPolarisAuthorizerTest {
"http://localhost:" + server.getAddress().getPort() + "/v1/data/polaris/allow");
OpaPolarisAuthorizer authorizer =
new OpaPolarisAuthorizer(
- policyUri, HttpClients.createDefault(), JsonMapper.builder().build(), null);
+ policyUri,
+ HttpClients.createDefault(),
+ JsonMapper.builder().build(),
+ null,
+ "analytics-realm");
// Set up a realistic principal
PolarisPrincipal principal =
@@ -351,6 +371,10 @@ public class OpaPolarisAuthorizerTest {
assertThat(input.has("resource")).as("Input should have 'resource' field").isTrue();
assertThat(input.has("context")).as("Input should have 'context' field").isTrue();
+ // Verify realm for tenant isolation
+ assertThat(input.get("context").has("realm")).isTrue();
+ assertThat(input.get("context").get("realm").asText()).isEqualTo("analytics-realm");
+
// Verify actor details
var actor = input.get("actor");
assertThat(actor.get("principal").asText()).isEqualTo("bob");
@@ -427,7 +451,11 @@ public class OpaPolarisAuthorizerTest {
"http://localhost:" + server.getAddress().getPort() + "/v1/data/polaris/allow");
OpaPolarisAuthorizer authorizer =
new OpaPolarisAuthorizer(
- policyUri, HttpClients.createDefault(), JsonMapper.builder().build(), null);
+ policyUri,
+ HttpClients.createDefault(),
+ JsonMapper.builder().build(),
+ null,
+ "test-realm");
PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("admin"));
@@ -473,7 +501,11 @@ public class OpaPolarisAuthorizerTest {
URI policyUri = URI.create("http://opa.example.com:8181/v1/data/polaris/allow");
OpaPolarisAuthorizer authorizer =
new OpaPolarisAuthorizer(
- policyUri, HttpClients.createDefault(), JsonMapper.builder().build(), tokenProvider);
+ policyUri,
+ HttpClients.createDefault(),
+ JsonMapper.builder().build(),
+ tokenProvider,
+ "test-realm");
assertThat(authorizer).isNotNull();
}
@@ -492,7 +524,8 @@ public class OpaPolarisAuthorizerTest {
policyUri,
mock(CloseableHttpClient.class),
JsonMapper.builder().build(),
- tokenProvider) {
+ tokenProvider,
+ "test-realm") {
@Override
<T> T httpClientExecute(
ClassicHttpRequest request, HttpClientResponseHandler<? extends T> responseHandler)
@@ -535,7 +568,8 @@ public class OpaPolarisAuthorizerTest {
policyUri,
mock(CloseableHttpClient.class),
JsonMapper.builder().build(),
- tokenProvider) {
+ tokenProvider,
+ "test-realm") {
@Override
<T> T httpClientExecute(
ClassicHttpRequest request, HttpClientResponseHandler<? extends T> responseHandler)
@@ -573,7 +607,8 @@ public class OpaPolarisAuthorizerTest {
URI.create("http://opa.example.com:8181/v1/data/polaris/allow"),
mock(CloseableHttpClient.class),
JsonMapper.builder().build(),
- null);
+ null,
+ "test-realm");
PolarisResolutionManifest resolutionManifest = mock(PolarisResolutionManifest.class);
AuthorizationState authzState = new AuthorizationState(resolutionManifest);
PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1"));
@@ -600,7 +635,8 @@ public class OpaPolarisAuthorizerTest {
URI.create("http://opa.example.com:8181/v1/data/polaris/allow"),
mock(CloseableHttpClient.class),
JsonMapper.builder().build(),
- null) {
+ null,
+ "test-realm") {
@Override
<T> T httpClientExecute(
ClassicHttpRequest request, HttpClientResponseHandler<? extends T> responseHandler)
@@ -648,7 +684,8 @@ public class OpaPolarisAuthorizerTest {
URI.create("http://opa.example.com:8181/v1/data/polaris/allow"),
mock(CloseableHttpClient.class),
JsonMapper.builder().build(),
- null) {
+ null,
+ "test-realm") {
@Override
<T> T httpClientExecute(
ClassicHttpRequest request, HttpClientResponseHandler<? extends T> responseHandler)
@@ -695,7 +732,8 @@ public class OpaPolarisAuthorizerTest {
URI.create("http://opa.example.com:8181/v1/data/polaris/allow"),
mock(CloseableHttpClient.class),
JsonMapper.builder().build(),
- null) {
+ null,
+ "catalog-realm") {
@Override
<T> T httpClientExecute(
ClassicHttpRequest request, HttpClientResponseHandler<? extends T> responseHandler)
@@ -711,6 +749,7 @@ public class OpaPolarisAuthorizerTest {
JsonNode root = mapper.readTree(capturedRequestBody[0]);
assertThat(decision.isAllowed()).isTrue();
+ assertThat(root.path("input").path("context").get("realm").asText()).isEqualTo("catalog-realm");
JsonNode expectedTarget =
mapper.readTree(
"""
@@ -739,7 +778,11 @@ public class OpaPolarisAuthorizerTest {
"http://localhost:" + server.getAddress().getPort() + "/v1/data/polaris/allow");
OpaPolarisAuthorizer authorizer =
new OpaPolarisAuthorizer(
- policyUri, HttpClients.createDefault(), JsonMapper.builder().build(), null);
+ policyUri,
+ HttpClients.createDefault(),
+ JsonMapper.builder().build(),
+ null,
+ "test-realm");
PolarisEntity rootEntity =
new PolarisEntity.Builder()
@@ -808,7 +851,11 @@ public class OpaPolarisAuthorizerTest {
"http://localhost:" + server.getAddress().getPort() + "/v1/data/polaris/allow");
OpaPolarisAuthorizer authorizer =
new OpaPolarisAuthorizer(
- policyUri, HttpClients.createDefault(), JsonMapper.builder().build(), null);
+ policyUri,
+ HttpClients.createDefault(),
+ JsonMapper.builder().build(),
+ null,
+ "test-realm");
PolarisEntity rootEntity =
new PolarisEntity.Builder()
@@ -879,7 +926,8 @@ public class OpaPolarisAuthorizerTest {
URI.create("http://opa.example.com:8181/v1/data/polaris/allow"),
mock(CloseableHttpClient.class),
JsonMapper.builder().build(),
- null) {
+ null,
+ "test-realm") {
@Override
<T> T httpClientExecute(
ClassicHttpRequest request, HttpClientResponseHandler<? extends T> responseHandler)
@@ -944,7 +992,8 @@ public class OpaPolarisAuthorizerTest {
URI.create("http://opa.example.com:8181/v1/data/polaris/allow"),
mock(CloseableHttpClient.class),
JsonMapper.builder().build(),
- null) {
+ null,
+ "test-realm") {
@Override
<T> T httpClientExecute(
ClassicHttpRequest request, HttpClientResponseHandler<? extends T> responseHandler)
@@ -1030,7 +1079,8 @@ public class OpaPolarisAuthorizerTest {
URI.create("http://opa.example.com:8181/v1/data/polaris/allow"),
mock(CloseableHttpClient.class),
JsonMapper.builder().build(),
- null) {
+ null,
+ "test-realm") {
@Override
<T> T httpClientExecute(
ClassicHttpRequest request, HttpClientResponseHandler<? extends T> responseHandler)
@@ -1055,6 +1105,78 @@ public class OpaPolarisAuthorizerTest {
assertThat(requestCount[0]).isEqualTo(2);
}
+ @Test
+ void serializesInputWithRealm() throws Exception {
+ final String[] capturedRequestBody = new String[1];
+
+ HttpServer server = createServerWithRequestCapture(capturedRequestBody);
+ try {
+ URI policyUri =
+ URI.create(
+ "http://localhost:" + server.getAddress().getPort() + "/v1/data/polaris/allow");
+ OpaPolarisAuthorizer authorizer =
+ new OpaPolarisAuthorizer(
+ policyUri,
+ HttpClients.createDefault(),
+ JsonMapper.builder().build(),
+ null,
+ "explicit-realm");
+
+ PolarisPrincipal principal = PolarisPrincipal.of("eve", Map.of(), Set.of("auditor"));
+
+ assertThatNoException()
+ .isThrownBy(
+ () ->
+ authorizer.authorizeOrThrow(
+ principal,
+ Set.of(),
+ PolarisAuthorizableOperation.GET_CATALOG,
+ (PolarisResolvedPathWrapper) null,
+ (PolarisResolvedPathWrapper) null));
+
+ ObjectMapper mapper = JsonMapper.builder().build();
+ JsonNode root = mapper.readTree(capturedRequestBody[0]);
+ var context = root.path("input").path("context");
+ assertThat(context.has("request_id")).isTrue();
+ assertThat(context.path("realm").asText()).isEqualTo("explicit-realm");
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void serializesRealmInAuthorizePath() throws Exception {
+ final String[] capturedRequestBody = new String[1];
+
+ HttpServer server = createServerWithRequestCapture(capturedRequestBody);
+ try {
+ URI policyUri =
+ URI.create(
+ "http://localhost:" + server.getAddress().getPort() + "/v1/data/polaris/allow");
+ OpaPolarisAuthorizer authorizer =
+ new OpaPolarisAuthorizer(
+ policyUri,
+ HttpClients.createDefault(),
+ JsonMapper.builder().build(),
+ null,
+ "tenant-xyz");
+
+ PolarisResolutionManifest resolutionManifest = mock(PolarisResolutionManifest.class);
+ AuthorizationState authzState = new AuthorizationState(resolutionManifest);
+
+ PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("admin"));
+ AuthorizationRequest request = requestWithCatalogTarget(principal);
+
+ assertThatNoException().isThrownBy(() -> authorizer.authorize(authzState, request));
+
+ ObjectMapper mapper = JsonMapper.builder().build();
+ JsonNode root = mapper.readTree(capturedRequestBody[0]);
+ assertThat(root.path("input").path("context").get("realm").asText()).isEqualTo("tenant-xyz");
+ } finally {
+ server.stop(0);
+ }
+ }
+
private AuthorizationRequest requestWithCatalogTarget(PolarisPrincipal principal) {
return new AuthorizationRequest(
principal,