You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@polaris.apache.org by "sungwy (via GitHub)" <gi...@apache.org> on 2025/09/25 03:03:19 UTC

[PR] Implement OpaPolarisAuthorizer [polaris]

sungwy opened a new pull request, #2680:
URL: https://github.com/apache/polaris/pull/2680

   <!--
       Possible security vulnerabilities: STOP here and contact security@apache.org instead!
   
       Please update the title of the PR with a meaningful message - do not leave it "empty" or "generated"
       Please update this summary field:
   
       The summary should cover these topics, if applicable:
       * the motivation for the change
       * a description of the status quo, for example the current behavior
       * the desired behavior
       * etc
   
       PR checklist:
       - Do a self-review of your code before opening a pull request
       - Make sure that there's good test coverage for the changes included in this PR
       - Run tests locally before pushing a PR (./gradlew check)
       - Code should have comments where applicable. Particularly hard-to-understand
         areas deserve good in-line documentation.
       - Include changes and enhancements to the documentation (in site/content/in-dev/unreleased)
       - For Work In Progress Pull Requests, please use the Draft PR feature.
   
       Make sure to add the information BELOW this comment.
       Everything in this comment will NOT be added to the PR description.
   -->
   
   Resolves https://github.com/apache/polaris/issues/138
   
   This pull request is a work in progress. It will be preceded by an RFC and further community discussions before moving forward.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3336133134

   Thanks for working on it @sungwy ! Looking forward to the RFC/design doc!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3457953983

   @dimas-b - I've addressed the concern regarding the `polaris-core` dependency. Appreciate the review!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2441505986


##########
runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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.service.auth;
+
+import io.smallrye.common.annotation.Identifier;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
+import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
+import org.apache.polaris.core.config.RealmConfig;
+
+/** Factory for creating the default Polaris authorizer implementation. */
+@ApplicationScoped
+@Identifier("internal")
+public class DefaultPolarisAuthorizerFactory implements PolarisAuthorizerFactory {

Review Comment:
   I just gave this a bit more thought - this will require us to add the following `compileOnly` dependencies into `polaris-core` and I'd like to get your opinion on whether that makes sense given our current separation of concerns with CDI related classes belonging in `polaris-runtime-service`.
   
   ```
     compileOnly(libs.jakarta.enterprise.cdi.api)
     compileOnly(libs.smallrye.common.annotation)
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2450002154


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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")

Review Comment:
   I've thought a bit more about this @snazy and I think using HTTP (and hence without ssl verification) is a legitimate production deployment strategy when we are running the OPA server as a side car to the Polaris server.
   
   Setting `verifySsl=true` and using a `http` url for `policyUrl` will work fine, and it'll just ignore the SSL verification settings. But I'm wondering if we need to be pedantic about what's production ready or not especially when it comes to deducing the security risk for a specific deployment strategy. Let me know what you think!
   
   I've added it for now, along with a warning that OpaPolarisAuthorizer is in beta release, but I'm happy to remove the severe check for `verifySsl=true` if you agree with this.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2452065281


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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")

Review Comment:
   Actually, after thinking about this a bit more I'm inclined to leave the `OpaProductionReadinessCheck` as it is. There's a good chance that `policyUri` won't be the only OPA Server Uri we'll be using in the future. Reference: https://trino.io/docs/current/security/opa-access-control.html#batch-mode



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2452872429


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  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 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
+   * @param clock clock instance for time operations
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer);
+  }
+
+  @Override
+  @Nullable
+  public String getToken() {
+    checkState(!closed, "Token provider is closed");
+
+    // Check if we need to refresh
+    if (shouldRefresh()) {
+      refreshToken();
+    }
+
+    return cachedToken;
+  }
+
+  @Override
+  public void close() {
+    closed = true;
+    cachedToken = null;
+  }
+
+  private boolean shouldRefresh() {
+    return clock.instant().isAfter(nextRefresh);
+  }
+
+  private void refreshToken() {
+    if (!refreshLock.compareAndSet(false, true)) {
+      return;
+    }
+    try {
+      String newToken = loadTokenFromFile();
+
+      // If we couldn't load a token and have no cached token, this is a fatal error
+      if (newToken == null && cachedToken == null) {
+        throw new RuntimeException(
+            "Unable to load bearer token from file: "
+                + tokenFilePath
+                + ". This is required for OPA authorization.");
+      }
+
+      // Only update cached token if we successfully loaded a new one
+      if (newToken != null) {
+        cachedToken = newToken;
+      }

Review Comment:
   noted :)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422369512


##########
runtime/service/src/intTest/java/org/apache/polaris/service/auth/opa/OpaFileTokenIntegrationTest.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * 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.service.auth.opa;
+
+import static io.restassured.RestAssured.given;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import io.quarkus.test.junit.QuarkusTestProfile.TestResourceEntry;
+import io.quarkus.test.junit.TestProfile;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.polaris.test.commons.OpaTestResource;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+@TestProfile(OpaFileTokenIntegrationTest.FileTokenOpaProfile.class)
+public class OpaFileTokenIntegrationTest {

Review Comment:
   Yeah I did try this out earlier and I found it quite challenging.
   
   These are @QuarkusTest integration tests that need to start the full Polaris runtime service application and configure the complete CDI container with all service dependencies to run an end to end test of the authorization workflow. The OPA extension module is a pure library without the Quarkus application context needed for @QuarkusTest, and I think there's value in leaving it at just that.
   
   IMHO I don't see too much of a problem in adding the extensions into `runtime/service` as a test dependency.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2417984284


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Strings;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+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.CloseableHttpResponse;
+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.NoopHostnameVerifier;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.TrustAllStrategy;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.ssl.SSLContextBuilder;
+import org.apache.hc.core5.util.Timeout;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ *
+ * <p><strong>Beta Feature:</strong> This implementation is currently in Beta and is not a stable
+ * release. It may undergo breaking changes in future versions. Use with caution in production
+ * environments.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final BearerTokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      BearerTokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a BearerTokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      BearerTokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client) {
+
+    if (Strings.isNullOrEmpty(opaServerUrl)) {
+      throw new IllegalArgumentException("opaServerUrl cannot be null or empty");
+    }
+    if (Strings.isNullOrEmpty(opaPolicyPath)) {
+      throw new IllegalArgumentException("opaPolicyPath cannot be null or empty");
+    }
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .setResponseTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .setConnectionRequestTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);

Review Comment:
   Yes, I'm already refactoring this as per @flyrain 's comment. It looks like I'll need to introduce an `OpaHttpClientFactory` anyways within this PR to clean this up.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2418034610


##########
polaris-core/build.gradle.kts:
##########
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(project(":polaris-api-management-model"))
+  implementation(libs.apache.httpclient5)

Review Comment:
   > I believe it would be valuable to isolate OPA from core and from runtime/service and only include it into runtime/server.
   > the new authorizer is pluggable, but not all downstream projects may want to have it by default.
   
   I'm OK to move OPA related classes from core to `runtime/service`, but I don't think we should put it in `runtime/server`. Here are the reason:
   1. I think it's perfect fine to have another implementation of authorizer, we did that in multiple places.
   2. The native RBAC is still the default.
   3. There is no single extra lib dependencies introduced when it landed in the `service` module. `http` and `jwt` libs are there already.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453007537


##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,8 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa/impl
+polaris-extensions-auth-opa-tests=extensions/auth/opa/tests

Review Comment:
   To clarify:
   The tests are for the runtime-service module, not by downstream projects consuming the runtime-service module.
   @flyrain you can assume a similar overhead for every single Apache Polaris CI run that's caused by the spark plugin. That's a lot for simple integration tests for every Polaris CI run.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2444914377


##########
extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/token/StaticBearerTokenProviderTest.java:
##########
@@ -0,0 +1,48 @@
+/*
+ * 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.token;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class StaticBearerTokenProviderTest {
+
+  @Test
+  public void testStaticBearerTokenProvider() {
+    String expectedToken = "static-bearer-token";
+    StaticBearerTokenProvider provider = new StaticBearerTokenProvider(expectedToken);

Review Comment:
   will take a look



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3443308613

   > For integration-tests, I'm thinking of having non-Quarkus integration tests against an OPA testcontainer in the "main" OPA module.
   > 
   > I've prepared some stuff for tests in [this commit](https://github.com/snazy/polaris/commit/0e535440eb9fa5cab6da4c7e25610f1b63505989) ([here](https://github.com/sungwy/polaris/pull/2) as well):
   > 
   > * Add a starter for an integration test in the "main" module
   > * Updated the existing int tests
   > 
   > Idea is to have the majority of integration-tests in the "main" module and only a smoke-test in runtime-service.
   > 
   > I think we can then get rid of the opa-tests module and can move the "main" module one directory level up.
   
   Thanks again @snazy :) Let me take a look at your draft PR and see if I can get rid of the `test` module and bring `impl` back up to `main`. I had some trouble achieving this because of the dependency on `:polaris-runtime-service`.
   
   Just so I'm clear: is your suggestion to move the QuarkusIntegrationTest annotated tests back into `polaris-runtime-service`?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453387664


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,176 @@
+/*
+ * 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.fasterxml.jackson.databind.ObjectMapper;
+import io.smallrye.common.annotation.Identifier;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.time.Clock;
+import java.time.Duration;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+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.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@ApplicationScoped
+@Identifier("opa")
+class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private static final Logger logger = LoggerFactory.getLogger(OpaPolarisAuthorizerFactory.class);
+
+  private final OpaAuthorizationConfig opaConfig;
+  private final Clock clock;
+  private CloseableHttpClient httpClient;
+  private BearerTokenProvider bearerTokenProvider;
+  private ObjectMapper objectMapper;
+
+  @Inject
+  public OpaPolarisAuthorizerFactory(OpaAuthorizationConfig opaConfig, Clock clock) {
+    this.opaConfig = opaConfig;
+    this.clock = clock;
+    this.objectMapper = new ObjectMapper();
+  }
+
+  /**
+   * Gets the OPA authorization configuration. Used by OpaProductionReadinessCheck
+   *
+   * @return the OPA configuration
+   */
+  OpaAuthorizationConfig getConfig() {
+    return opaConfig;
+  }
+
+  @PostConstruct
+  public void initialize() {
+    // Validate configuration once during startup
+    opaConfig.validate();
+
+    // Create HTTP client once during startup
+    httpClient = createHttpClient();
+
+    // Setup authentication once during startup
+    setupAuthentication(opaConfig.auth());

Review Comment:
   fair point :+1: 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422226282


##########
polaris-core/build.gradle.kts:
##########
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(project(":polaris-api-management-model"))
+  implementation(libs.apache.httpclient5)

Review Comment:
   @flyrain : if OPA is not added to `runtime/server` users of the OSS distributions will not be able to use it. I personally think it's a valuable feature for OSS users... Let's sync up on this aspect on the `dev` ML thread for visibility.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2416911569


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility
+      ObjectMapper mapper) {
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(timeoutMs)
+              .setSocketTimeout(timeoutMs)
+              .setConnectionRequestTimeout(timeoutMs)
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);
+
+      // Create HTTP client with SSL configuration
+      CloseableHttpClient httpClient;
+      if (client instanceof CloseableHttpClient) {
+        httpClient = (CloseableHttpClient) client;
+      } else {
+        if (sslSocketFactory != null) {
+          httpClient =
+              HttpClients.custom()
+                  .setDefaultRequestConfig(requestConfig)
+                  .setSSLSocketFactory(sslSocketFactory)

Review Comment:
   Agreed, @flyrain — I’d prefer not to use `org.apache.iceberg.rest.HTTPClient`, since it assumes rest-prefixed configurations in the properties (as it’s tied to the REST catalog in Iceberg).
   
   If we plan to introduce server-specific configurations such as proxy settings into the HTTPClient (which I think we should), we’ll likely need to create a distinct HTTPClient per HTTPServer, each initialized with its own server-specific HTTP configuration properties (e.g., `polaris.authorization.opa.http.proxy-url`, etc.).
   
   I’ll start working on this change, but feel free to merge this PR if the current version looks good as an initial step. I’ll follow up soon with the enhanced HTTPClient integration and its corresponding configuration support.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "adutra (via GitHub)" <gi...@apache.org>.
adutra commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2399636426


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      int timeoutMs,
+      OkHttpClient client,
+      ObjectMapper mapper) {
+    OkHttpClient clientWithTimeout =
+        (client != null)
+            ? client.newBuilder().callTimeout(java.time.Duration.ofMillis(timeoutMs)).build()
+            : new OkHttpClient.Builder()
+                .callTimeout(java.time.Duration.ofMillis(timeoutMs))
+                .build();
+    ObjectMapper objectMapper = (mapper != null) ? mapper : new ObjectMapper();
+    return new OpaPolarisAuthorizer(opaServerUrl, opaPolicyPath, clientWithTimeout, objectMapper);
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+      RequestBody body = RequestBody.create(inputJson, MediaType.parse("application/json"));
+      Request request = new Request.Builder().url(opaServerUrl + opaPolicyPath).post(body).build();
+      try (Response response = httpClient.newCall(request).execute()) {
+        if (!response.isSuccessful()) return false;
+        // Parse response JSON for 'result.allow'
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(response.body().string());
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();

Review Comment:
   https://github.com/apache/polaris/pull/2752



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2395293929


##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -163,10 +162,23 @@ public RealmConfig realmConfig(CallContext callContext) {
     return callContext.getRealmConfig();
   }
 
+  @jakarta.inject.Inject AuthorizationConfiguration authorizationConfig;

Review Comment:
   noted :)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422239291


##########
polaris-core/build.gradle.kts:
##########
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(project(":polaris-api-management-model"))
+  implementation(libs.apache.httpclient5)

Review Comment:
   Let me review this dependency again.
   
   I've moved most of the OPA factory and configuration classes into `extensions/auth/opa` as suggested, but now I'm seeing a test failure at `PolarisEventListenerTest` that's related to the Jandex index. I'll look into this a bit more and get this resolved. 
   
   ```
   PolarisEventListenerTest > testAllEventTypesHandled() FAILED
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422248298


##########
polaris-core/build.gradle.kts:
##########
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(project(":polaris-api-management-model"))
+  implementation(libs.apache.httpclient5)

Review Comment:
   Moreover, `OpaFileTokenIntegrationTest` and `OpaIntegrationTest` are still be part of the `polaris-runtime-service` module. What's the point of having the tested classes(like `OpaPolarisAuthorizer`) in an extension module?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2470543229


##########
polaris-core/build.gradle.kts:
##########
@@ -112,6 +112,7 @@ dependencies {
   testFixturesApi(libs.jakarta.ws.rs.api)
 
   compileOnly(libs.jakarta.annotation.api)
+  compileOnly(libs.jakarta.enterprise.cdi.api)

Review Comment:
   Thanks @dimas-b  these are great suggestions. Thank you for giving me the background context on the package discussions.
   
   I've added a Producer method in `ServiceProducers` and removed the cdi dependency from `polaris-core`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453088579


##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,8 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa/impl
+polaris-extensions-auth-opa-tests=extensions/auth/opa/tests

Review Comment:
   In fact, if we want to reduce the number of Quarkus builds in CI, why not run all integration tests inside the runtime-server module?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2451804447


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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")

Review Comment:
   I think those are different scenarios ;)
   
   1. HTTP (no TLS) - no certificate that could be validated.
   2. HTTPS (with TLS) - intentionally turn off certificate validation ("not production-ready")
   3. HTTPS (with TLS) - with certificate validation, potentially with a custom trust store, recommended for production
   
   With "plaintext" HTTP, which might be, as you mentioned a legit prod deployment strategy, with the known general security drawbacks. It's (hopefully) a well-educated user decision. I'd maybe emit a warning in this case, but would not fight for it.
   
   Scenario 2 is the one that should not work out of the box, as the usage of TLS implies some guarantees. But TLS without certificate & host validation is a pretty bad choice. That's the one that I'm thinking of.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2444913487


##########
extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/OpaTestResource.java:
##########
@@ -0,0 +1,137 @@
+/*
+ * 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 io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.polaris.containerspec.ContainerSpecHelper;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+public class OpaTestResource implements QuarkusTestResourceLifecycleManager {
+  private static GenericContainer<?> opa;
+  private int mappedPort;
+  private Map<String, String> resourceConfig;

Review Comment:
   stale artifact from refactoring, let me remove this 👍 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3448480594

   > BTW: Please add an entry for the "main" module to `:polaris-bom`.
   
   I've added the `polaris-extensions-auth-opa` module to `:polaris-bom`. I also noticed that the other extensions `polaris-extensions-federation-hive` and `polaris-extensions-federation-hadoop` are missing. Should these be added as well? (maybe in a separate PR)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "adutra (via GitHub)" <gi...@apache.org>.
adutra commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2379557063


##########
runtime/service/build.gradle.kts:
##########
@@ -25,6 +25,7 @@ plugins {
 }
 
 dependencies {
+  implementation("com.squareup.okhttp3:okhttp:4.12.0")

Review Comment:
   Is this a required dependency, or should it be optional? In any case, let's move it down to keep this dependencies block tidy.



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      int timeoutMs,
+      OkHttpClient client,
+      ObjectMapper mapper) {
+    OkHttpClient clientWithTimeout =
+        (client != null)
+            ? client.newBuilder().callTimeout(java.time.Duration.ofMillis(timeoutMs)).build()
+            : new OkHttpClient.Builder()
+                .callTimeout(java.time.Duration.ofMillis(timeoutMs))
+                .build();
+    ObjectMapper objectMapper = (mapper != null) ? mapper : new ObjectMapper();
+    return new OpaPolarisAuthorizer(opaServerUrl, opaPolicyPath, clientWithTimeout, objectMapper);
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+      RequestBody body = RequestBody.create(inputJson, MediaType.parse("application/json"));
+      Request request = new Request.Builder().url(opaServerUrl + opaPolicyPath).post(body).build();
+      try (Response response = httpClient.newCall(request).execute()) {
+        if (!response.isSuccessful()) return false;
+        // Parse response JSON for 'result.allow'
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(response.body().string());
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();

Review Comment:
   I noticed that `entities` is unused, which means that this authorizer effectively ignores whatever catalog roles or principal roles the `Resolver` has resolved for the current authenticated principal.
   
   I suppose that this is by design? We are actually slowly moving to a situation where Polaris RBAC model is being bypassed. I actually think that this is a good thing, I've been thinking about this myself in connection with federated principals and how to provide a better support for them. Your authorizer may be a good solution for that 😄 



##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -163,10 +162,23 @@ public RealmConfig realmConfig(CallContext callContext) {
     return callContext.getRealmConfig();
   }
 
+  @jakarta.inject.Inject AuthorizationConfiguration authorizationConfig;
+
+  @jakarta.inject.Inject RealmConfig realmConfig;
+
   @Produces
   @RequestScoped
-  public PolarisAuthorizer polarisAuthorizer(RealmConfig realmConfig) {
-    return new PolarisAuthorizerImpl(realmConfig);
+  public PolarisAuthorizer polarisAuthorizer() {
+    if ("opa".equalsIgnoreCase(authorizationConfig.implementation())) {

Review Comment:
   Reiterating my previous comment: I'd suggest using `@Identifier`.



##########
runtime/service/src/main/java/org/apache/polaris/service/config/AuthorizationConfiguration.java:
##########
@@ -0,0 +1,42 @@
+/*
+ * 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.service.config;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import io.smallrye.config.WithName;
+import java.util.Optional;
+
+@ConfigMapping(prefix = "polaris.authorization")
+public interface AuthorizationConfiguration {
+  @WithDefault("default")
+  String implementation();
+
+  OpaConfig opa();
+
+  interface OpaConfig {
+    Optional<String> url();
+
+    @WithName("policy-path")

Review Comment:
   I think both `@WithName` annotations aren't necessary.



##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -163,10 +162,23 @@ public RealmConfig realmConfig(CallContext callContext) {
     return callContext.getRealmConfig();
   }
 
+  @jakarta.inject.Inject AuthorizationConfiguration authorizationConfig;

Review Comment:
   nit: use imports.



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(

Review Comment:
   This factory method looks odd, why not use the Polaris usual way of selecting CDI implementations via the `@Identifier` annotation?



##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -193,6 +193,15 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization implementation settings
+# Which authorizer to use: "default" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+polaris.authorization.implementation=default

Review Comment:
   Should be renamed to `polaris.authorization.type` for consistency with other similar properties.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain merged PR #2680:
URL: https://github.com/apache/polaris/pull/2680


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2479039242


##########
runtime/service/build.gradle.kts:
##########
@@ -106,6 +106,8 @@ dependencies {
 
   implementation(libs.jakarta.servlet.api)
 
+  runtimeOnly(project(":polaris-async-vertx"))

Review Comment:
   Why do we need this dependency? Is this only for test?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453086104


##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,8 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa/impl
+polaris-extensions-auth-opa-tests=extensions/auth/opa/tests

Review Comment:
   My suggestion was to run integration tests inside the OPA modules (by including runtime-service and making a dedicated Quarkus server) to improve code isolation. If that adds too much CI time, I'm ok with including OPA as a test-only dependency into runtime-service.
   
   Even better, why not create a dedicated integration tests module? This will avoid any semi-related deps in runtime-service and still allow for one Quarkus server build for running all integration tests?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453419834


##########
runtime/service/build.gradle.kts:
##########
@@ -75,6 +75,7 @@ dependencies {
 
   implementation(libs.auth0.jwt)
 
+  implementation(libs.apache.httpclient5)

Review Comment:
   I think this was from when OPA related code was still in runtime/service module - let me remove it



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3382655756

   Will post my complete review soon. Adding one thing first, currently all grant and revoke privilege operation (e.g. `grantPrivilegeOnCatalogToRole`) won’t work as expected, since these mapping has to happen in the OPA side. How do we position these APIs? If we still want to support them within Polaris, we need new interfaces between the OPA service and Polaris. I think the least thing we could do now is blocking these operation for OPA authorizer. Given the current PR is large, I’m OK to leave it in another PR. Thanks @singhpk234 for bring it up for discussion.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453556374


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,222 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.google.common.base.Strings;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  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 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
+   * @param clock clock instance for time operations
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer);
+  }
+
+  @Override
+  public String getToken() {
+    checkState(!closed, "Token provider is closed");
+
+    // Check if we need to refresh
+    if (shouldRefresh()) {
+      refreshToken();
+    }
+
+    // If we couldn't load a token and have no cached token, this is a fatal error
+    if (Strings.isNullOrEmpty(cachedToken)) {
+      throw new RuntimeException(
+          "Unable to load bearer token from file: "
+              + tokenFilePath
+              + ". This is required for OPA authorization.");
+    }
+    return cachedToken;
+  }
+
+  @Override
+  public void close() {
+    closed = true;
+    cachedToken = null;
+  }
+
+  private boolean shouldRefresh() {
+    return clock.instant().isAfter(nextRefresh);
+  }
+
+  private void refreshToken() {
+    if (!refreshLock.compareAndSet(false, true)) {
+      return;

Review Comment:
   I agree. I liked @snazy 's suggestion to use AtomicBoolean, so I added eager token initialization in the constructor to address this specific edge case. Let me know if you still share this concern



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453103819


##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,8 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa/impl
+polaris-extensions-auth-opa-tests=extensions/auth/opa/tests

Review Comment:
   > why not run all integration tests inside the runtime-server module?
   
   +1 on @dimas-b 's idea. We could do that in a separated PR.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3458847454

   Let's give some more time to @flyrain and @snazy for final thoughts / comments. I suppose we could also follow up with test adjustments relocations after merging this PR (it's been in review for a long time :sweat_smile: )


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422912058


##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -244,6 +254,59 @@ public StsClientsPool stsClientsPool(
     return new StsClientsPool(config.effectiveClientsCacheMaxSize(), httpClient, meterRegistry);
   }
 
+  @Produces
+  @Singleton
+  @Identifier("opa-http-client")
+  public CloseableHttpClient opaHttpClient() {
+    return HttpClients.custom().build();
+  }
+
+  public void closeOpaHttpClient(
+      @Disposes @Identifier("opa-http-client") CloseableHttpClient client) {
+    try {
+      client.close();
+    } catch (Exception e) {
+      LOGGER.warn("Error closing OPA HTTP client", e);
+    }
+  }
+
+  @Produces
+  @Singleton
+  @Identifier("opa-bearer-token-provider")
+  public BearerTokenProvider opaBearerTokenProvider(

Review Comment:
   I took a stab at this. Let me know if this aligns with what you were suggesting



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3452765253

   > I've added the `polaris-extensions-auth-opa` module to `:polaris-bom`. I also noticed that the other extensions `polaris-extensions-federation-hive` and `polaris-extensions-federation-hadoop` are missing. Should these be added as well? (maybe in a separate PR)
   
   Yes, we will need them in BOM as well. A separate PR sounds good to me. 
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2480218307


##########
runtime/service/build.gradle.kts:
##########
@@ -106,6 +106,8 @@ dependencies {
 
   implementation(libs.jakarta.servlet.api)
 
+  runtimeOnly(project(":polaris-async-vertx"))

Review Comment:
   I’m good with using the Vert.x async executor since we’re already on Quarkus. The `polaris-async-vertx` module currently lives under `/persistence/nosql`, but it’s really part of the async infrastructure rather than a NoSQL implementation detail. We could consider moving it out of that directory, though it doesn’t need to be done in this PR.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2451945013


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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")

Review Comment:
   Yeah the flag will just be ignored if it's an http url, but it may still be confusing to the users to be forced to set it as `verifySsl=true` to avoid the severity warning. I'll also edit it to raise the ProductionReadinessCheck error when the url scheme is also `https`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422357032


##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,7 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa

Review Comment:
   I do like the consistency across the extension modules. We already have `polaris-extensions-federation-hadoop` and `polaris-extensions-federation-hive` above



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2417915432


##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -244,6 +254,59 @@ public StsClientsPool stsClientsPool(
     return new StsClientsPool(config.effectiveClientsCacheMaxSize(), httpClient, meterRegistry);
   }
 
+  @Produces
+  @Singleton
+  @Identifier("opa-http-client")
+  public CloseableHttpClient opaHttpClient() {
+    return HttpClients.custom().build();
+  }
+
+  public void closeOpaHttpClient(
+      @Disposes @Identifier("opa-http-client") CloseableHttpClient client) {
+    try {
+      client.close();
+    } catch (Exception e) {
+      LOGGER.warn("Error closing OPA HTTP client", e);
+    }
+  }
+
+  @Produces
+  @Singleton
+  @Identifier("opa-bearer-token-provider")
+  public BearerTokenProvider opaBearerTokenProvider(

Review Comment:
   I wonder it this producer could be moved into `OpaPolarisAuthorizerFactory`. It should only be required when that factory is active.



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Strings;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+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.CloseableHttpResponse;
+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.NoopHostnameVerifier;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.TrustAllStrategy;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.ssl.SSLContextBuilder;
+import org.apache.hc.core5.util.Timeout;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ *
+ * <p><strong>Beta Feature:</strong> This implementation is currently in Beta and is not a stable
+ * release. It may undergo breaking changes in future versions. Use with caution in production
+ * environments.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final BearerTokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      BearerTokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a BearerTokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      BearerTokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client) {
+
+    if (Strings.isNullOrEmpty(opaServerUrl)) {
+      throw new IllegalArgumentException("opaServerUrl cannot be null or empty");
+    }
+    if (Strings.isNullOrEmpty(opaPolicyPath)) {
+      throw new IllegalArgumentException("opaPolicyPath cannot be null or empty");
+    }
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .setResponseTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .setConnectionRequestTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);

Review Comment:
   This will be called for every request (REST API). I'm not sure how heavy is the socket factory, but would it make sense to keep it in `OpaPolarisAuthorizerFactory` (to be shared across `OpaPolarisAuthorizer` instances)?



##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -244,6 +254,59 @@ public StsClientsPool stsClientsPool(
     return new StsClientsPool(config.effectiveClientsCacheMaxSize(), httpClient, meterRegistry);
   }
 
+  @Produces
+  @Singleton
+  @Identifier("opa-http-client")
+  public CloseableHttpClient opaHttpClient() {
+    return HttpClients.custom().build();
+  }
+
+  public void closeOpaHttpClient(
+      @Disposes @Identifier("opa-http-client") CloseableHttpClient client) {
+    try {
+      client.close();
+    } catch (Exception e) {
+      LOGGER.warn("Error closing OPA HTTP client", e);
+    }
+  }
+
+  @Produces
+  @Singleton
+  @Identifier("opa-bearer-token-provider")
+  public BearerTokenProvider opaBearerTokenProvider(

Review Comment:
   Side note: if we take OPA out or core, it would be nice to avoid direct references to it here as well and rely only on CDI to find the right classes via annotations.



##########
runtime/service/src/main/java/org/apache/polaris/service/config/AuthorizationConfiguration.java:
##########
@@ -0,0 +1,120 @@
+/*
+ * 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.service.config;
+
+import com.google.common.base.Strings;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import java.util.Optional;
+
+@ConfigMapping(prefix = "polaris.authorization")
+public interface AuthorizationConfiguration {
+  @WithDefault("default")
+  String type();
+
+  OpaConfig opa();

Review Comment:
   If we move OPA out of core, let's create a separate config interface for it (with prefix `polaris.authorization.opa`). This class will only declare `type()` (which is common for all authorizers).



##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -244,6 +254,59 @@ public StsClientsPool stsClientsPool(
     return new StsClientsPool(config.effectiveClientsCacheMaxSize(), httpClient, meterRegistry);
   }
 
+  @Produces
+  @Singleton
+  @Identifier("opa-http-client")
+  public CloseableHttpClient opaHttpClient() {
+    return HttpClients.custom().build();
+  }
+
+  public void closeOpaHttpClient(
+      @Disposes @Identifier("opa-http-client") CloseableHttpClient client) {
+    try {
+      client.close();
+    } catch (Exception e) {
+      LOGGER.warn("Error closing OPA HTTP client", e);
+    }
+  }
+
+  @Produces
+  @Singleton
+  @Identifier("opa-bearer-token-provider")
+  public BearerTokenProvider opaBearerTokenProvider(
+      AuthorizationConfiguration authorizationConfig) {
+    AuthorizationConfiguration.OpaConfig opa = authorizationConfig.opa();
+    AuthorizationConfiguration.BearerTokenConfig bearerToken = opa.bearerToken();
+
+    // Validate configuration before using it
+    bearerToken.validate();
+
+    // Check if bearer token authentication is enabled
+    if (!bearerToken.enabled()) {
+      return new StaticBearerTokenProvider("");
+    }
+
+    // Static token takes precedence
+    if (bearerToken.staticValue().isPresent()) {
+      return new StaticBearerTokenProvider(bearerToken.staticValue().get());
+    }
+
+    // File-based token as fallback
+    if (bearerToken.filePath().isPresent()) {
+      java.time.Duration refreshInterval =
+          java.time.Duration.ofSeconds(bearerToken.refreshInterval());
+      boolean jwtExpirationRefresh = bearerToken.jwtExpirationRefresh();
+      java.time.Duration jwtExpirationBuffer =

Review Comment:
   nit: import?



##########
polaris-core/build.gradle.kts:
##########
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(project(":polaris-api-management-model"))
+  implementation(libs.apache.httpclient5)

Review Comment:
   I think it might be preferable to move OPA stuff out of "core". I propose `extensions/opa` (include into `runtime/server`).
   
   Rationale: 
   1) the new authorizer is pluggable, but not all downstream projects may want to have it by default.
   2) reducing core dependencies (IIRC, Quarkus runtime already has `httpclient5`, but core does not have to depend on it).



##########
runtime/service/src/test/java/org/apache/polaris/service/auth/OpaFileTokenIntegrationTest.java:
##########
@@ -0,0 +1,307 @@
+/*
+ * 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.service.auth;
+
+import static io.restassured.RestAssured.given;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import io.quarkus.test.junit.QuarkusTestProfile.TestResourceEntry;
+import io.quarkus.test.junit.TestProfile;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.polaris.test.commons.OpaTestResource;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+@TestProfile(OpaFileTokenIntegrationTest.FileTokenOpaProfile.class)
+public class OpaFileTokenIntegrationTest {
+
+  public static class FileTokenOpaProfile implements QuarkusTestProfile {
+    private static volatile Path tokenFile;
+
+    @Override
+    public Map<String, String> getConfigOverrides() {
+      Map<String, String> config = new HashMap<>();
+      config.put("polaris.authorization.type", "opa");
+      config.put("polaris.authorization.opa.policy-path", "/v1/data/polaris/authz");
+      config.put("polaris.authorization.opa.timeout-ms", "2000");
+
+      // Create temporary token file for testing
+      try {
+        tokenFile = Files.createTempFile("opa-test-token", ".txt");
+        Files.writeString(tokenFile, "test-opa-bearer-token-from-file-67890");
+        tokenFile.toFile().deleteOnExit();

Review Comment:
   nit: this is not guaranteed to delete the file, AFAIK. Plus the "JVM" may be long running, e.g. a Gradle process.
   
   It might be best to manage this as a `QuarkusTestResourceLifecycleManager`, which can produce extra server config entries.



##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -244,6 +254,59 @@ public StsClientsPool stsClientsPool(
     return new StsClientsPool(config.effectiveClientsCacheMaxSize(), httpClient, meterRegistry);
   }
 
+  @Produces
+  @Singleton
+  @Identifier("opa-http-client")
+  public CloseableHttpClient opaHttpClient() {

Review Comment:
   `@Singleton` might be an overkill. It is only needed when OPA is active. It should be a dependent bean, I think.
   
   Also, `OpaPolarisAuthorizerFactory` might be able to produce / dispose of this bean. Beans can produce other beans, AFAIK.
   
   If dependent beans prove to be too hard to manage, perhaps we can keep the client as a field in the factory and dispose of it together with the factory.



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Strings;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+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.CloseableHttpResponse;
+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.NoopHostnameVerifier;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.TrustAllStrategy;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.ssl.SSLContextBuilder;
+import org.apache.hc.core5.util.Timeout;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ *
+ * <p><strong>Beta Feature:</strong> This implementation is currently in Beta and is not a stable
+ * release. It may undergo breaking changes in future versions. Use with caution in production
+ * environments.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final BearerTokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      BearerTokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a BearerTokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      BearerTokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client) {
+
+    if (Strings.isNullOrEmpty(opaServerUrl)) {
+      throw new IllegalArgumentException("opaServerUrl cannot be null or empty");
+    }
+    if (Strings.isNullOrEmpty(opaPolicyPath)) {
+      throw new IllegalArgumentException("opaPolicyPath cannot be null or empty");
+    }
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .setResponseTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .setConnectionRequestTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);
+
+      // Create HTTP client with SSL configuration
+      CloseableHttpClient httpClient;
+      if (client instanceof CloseableHttpClient) {
+        httpClient = (CloseableHttpClient) client;
+      } else {
+        if (sslSocketFactory != null) {
+          httpClient =
+              HttpClients.custom()
+                  .setDefaultRequestConfig(requestConfig)
+                  .setConnectionManager(
+                      PoolingHttpClientConnectionManagerBuilder.create()
+                          .setSSLSocketFactory(sslSocketFactory)
+                          .build())
+                  .build();
+        } else {
+          httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
+        }
+      }
+
+      ObjectMapper objectMapperWithDefaults = new ObjectMapper();
+      return new OpaPolarisAuthorizer(
+          opaServerUrl, opaPolicyPath, tokenProvider, httpClient, objectMapperWithDefaults);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to create OpaPolarisAuthorizer with SSL configuration", e);
+    }
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response. The request format follows the OPA REST API specification for data queries.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   * @see <a href="https://www.openpolicyagent.org/docs/rest-api">OPA REST API Documentation</a>
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+
+      // Create HTTP POST request using Apache HttpComponents
+      HttpPost httpPost = new HttpPost(opaServerUrl + opaPolicyPath);
+      httpPost.setHeader("Content-Type", "application/json");
+
+      // Add bearer token authentication if provided
+      if (tokenProvider != null) {
+        String token = tokenProvider.getToken();
+        if (token != null && !token.isEmpty()) {
+          httpPost.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
+        }
+      }
+
+      httpPost.setEntity(new StringEntity(inputJson, ContentType.APPLICATION_JSON));
+
+      // Execute request
+      try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+        int statusCode = response.getCode();
+        if (statusCode != 200) {
+          return false;
+        }
+
+        // Read and parse response
+        String responseBody;
+        try {
+          responseBody = EntityUtils.toString(response.getEntity());
+        } catch (ParseException e) {
+          throw new RuntimeException("Failed to parse OPA response", e);
+        }
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(responseBody);
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * <p><strong>Note:</strong> OpaPolarisAuthorizer bypasses Polaris's built-in role-based
+   * authorization system. This includes both principal roles and catalog roles that would normally
+   * be processed by Polaris. Instead, authorization decisions are delegated entirely to the
+   * configured OPA policies, which receive the raw principal information and must implement their
+   * own role/permission logic.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();
+    input.set("actor", buildActorNode(principal));
+    input.put("action", op.name());
+    input.set("resource", buildResourceNode(targets, secondaries));
+    input.set("context", buildContextNode());
+    ObjectNode root = objectMapper.createObjectNode();
+    root.set("input", input);
+    return objectMapper.writeValueAsString(root);
+  }
+
+  /**
+   * Builds the actor section of the OPA input JSON.
+   *
+   * <p>Includes principal name, and roles as a generic field.
+   *
+   * @param principal the principal requesting authorization
+   * @return the actor node for OPA input
+   */
+  private ObjectNode buildActorNode(PolarisPrincipal principal) {
+    ObjectNode actor = objectMapper.createObjectNode();
+    actor.put("principal", principal.getName());
+    ArrayNode roles = objectMapper.createArrayNode();
+    for (String role : principal.getRoles()) roles.add(role);
+    actor.set("roles", roles);
+    return actor;
+  }
+
+  /**
+   * Builds the resource section of the OPA input JSON.
+   *
+   * <p>Includes the main target entity under 'primary' and secondary entities under 'secondaries'.
+   *
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities
+   * @return the resource node for OPA input
+   */
+  private ObjectNode buildResourceNode(
+      List<PolarisResolvedPathWrapper> targets, List<PolarisResolvedPathWrapper> secondaries) {
+    ObjectNode resource = objectMapper.createObjectNode();
+    // Main targets as 'targets' array
+    ArrayNode targetsArray = objectMapper.createArrayNode();
+    if (targets != null && !targets.isEmpty()) {
+      for (PolarisResolvedPathWrapper targetWrapper : targets) {
+        targetsArray.add(buildSingleResourceNode(targetWrapper));
+      }
+    }
+    resource.set("targets", targetsArray);
+    // Secondaries as array
+    ArrayNode secondariesArray = objectMapper.createArrayNode();
+    if (secondaries != null && !secondaries.isEmpty()) {
+      for (PolarisResolvedPathWrapper secondaryWrapper : secondaries) {
+        secondariesArray.add(buildSingleResourceNode(secondaryWrapper));
+      }
+    }
+    resource.set("secondaries", secondariesArray);
+    return resource;
+  }
+
+  /** Helper to build a resource node for a single PolarisResolvedPathWrapper. */
+  private ObjectNode buildSingleResourceNode(PolarisResolvedPathWrapper wrapper) {
+    ObjectNode node = objectMapper.createObjectNode();
+    if (wrapper == null) return node;
+    var resolvedEntity = wrapper.getResolvedLeafEntity();
+    if (resolvedEntity != null) {
+      var entity = resolvedEntity.getEntity();
+      node.put("type", entity.getType().name());
+      node.put("name", entity.getName());
+      var parentPath = wrapper.getResolvedParentPath();
+      if (parentPath != null && !parentPath.isEmpty()) {
+        ArrayNode parentsArray = objectMapper.createArrayNode();
+        for (var parent : parentPath) {
+          ObjectNode parentNode = objectMapper.createObjectNode();
+          parentNode.put("type", parent.getEntity().getType().name());
+          parentNode.put("name", parent.getEntity().getName());
+          parentsArray.add(parentNode);
+        }
+        node.set("parents", parentsArray);
+      }
+    }
+    return node;
+  }
+
+  /**
+   * Builds the context section of the OPA input JSON.
+   *
+   * <p>Includes only timestamp and request ID.
+   *
+   * @return the context node for OPA input
+   */
+  private ObjectNode buildContextNode() {
+    ObjectNode context = objectMapper.createObjectNode();
+    context.put("time", java.time.ZonedDateTime.now().toString());
+    context.put("request_id", java.util.UUID.randomUUID().toString());
+    return context;
+  }
+
+  /**
+   * Creates an SSL socket factory for HTTPS connections based on the configuration.
+   *
+   * @param opaServerUrl the OPA server URL
+   * @param verifySsl whether to verify SSL certificates
+   * @param trustStorePath custom trust store path (optional)
+   * @param trustStorePassword custom trust store password (optional)
+   * @return SSLConnectionSocketFactory for HTTPS connections, or null for HTTP
+   * @throws Exception if SSL configuration fails
+   */
+  private static SSLConnectionSocketFactory createSslSocketFactory(

Review Comment:
   `SSLConnectionSocketFactory` appears to be deprecated 🤔 Should we use `DefaultClientTlsStrategy` as suggested by its javadoc?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453421281


##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -139,10 +139,23 @@ public RealmConfig realmConfig(CallContext callContext) {
     return callContext.getRealmConfig();
   }
 
+  @Produces
+  @ApplicationScoped
+  public PolarisAuthorizerFactory polarisAuthorizerFactory(
+      AuthorizationConfiguration authorizationConfig,
+      @Any Instance<PolarisAuthorizerFactory> authorizerFactories) {
+    PolarisAuthorizerFactory factory =
+        authorizerFactories
+            .select(Identifier.Literal.of(authorizationConfig.type().getValue()))

Review Comment:
   That's an interesting point. I can remove this



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2455100562


##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -139,10 +139,23 @@ public RealmConfig realmConfig(CallContext callContext) {
     return callContext.getRealmConfig();
   }
 
+  @Produces
+  @ApplicationScoped
+  public PolarisAuthorizerFactory polarisAuthorizerFactory(
+      AuthorizationConfiguration authorizationConfig,
+      @Any Instance<PolarisAuthorizerFactory> authorizerFactories) {
+    PolarisAuthorizerFactory factory =
+        authorizerFactories
+            .select(Identifier.Literal.of(authorizationConfig.type().getValue()))

Review Comment:
   The general approach so far is that anything discoverable via CDI has to be assumed pluggable / alterable in downstream builds.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2455097739


##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -139,10 +139,23 @@ public RealmConfig realmConfig(CallContext callContext) {
     return callContext.getRealmConfig();
   }
 
+  @Produces
+  @ApplicationScoped
+  public PolarisAuthorizerFactory polarisAuthorizerFactory(
+      AuthorizationConfiguration authorizationConfig,
+      @Any Instance<PolarisAuthorizerFactory> authorizerFactories) {
+    PolarisAuthorizerFactory factory =
+        authorizerFactories
+            .select(Identifier.Literal.of(authorizationConfig.type().getValue()))

Review Comment:
   Please do.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2455294052


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,222 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.google.common.base.Strings;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  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 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
+   * @param clock clock instance for time operations
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer);
+  }
+
+  @Override
+  public String getToken() {
+    checkState(!closed, "Token provider is closed");
+
+    // Check if we need to refresh
+    if (shouldRefresh()) {
+      refreshToken();
+    }
+
+    // If we couldn't load a token and have no cached token, this is a fatal error
+    if (Strings.isNullOrEmpty(cachedToken)) {
+      throw new RuntimeException(
+          "Unable to load bearer token from file: "
+              + tokenFilePath
+              + ". This is required for OPA authorization.");
+    }
+    return cachedToken;
+  }
+
+  @Override
+  public void close() {
+    closed = true;
+    cachedToken = null;
+  }
+
+  private boolean shouldRefresh() {
+    return clock.instant().isAfter(nextRefresh);
+  }
+
+  private void refreshToken() {
+    if (!refreshLock.compareAndSet(false, true)) {
+      return;

Review Comment:
   NP, it's a bit more than what could fit into a GH review comment suggestion ;)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2455275044


##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -139,10 +139,23 @@ public RealmConfig realmConfig(CallContext callContext) {
     return callContext.getRealmConfig();
   }
 
+  @Produces
+  @ApplicationScoped
+  public PolarisAuthorizerFactory polarisAuthorizerFactory(
+      AuthorizationConfiguration authorizationConfig,
+      @Any Instance<PolarisAuthorizerFactory> authorizerFactories) {
+    PolarisAuthorizerFactory factory =
+        authorizerFactories
+            .select(Identifier.Literal.of(authorizationConfig.type().getValue()))

Review Comment:
   yeah makes sense - I've already made this change @dimas-b 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3445204930

   > I suspect we don't need that many ITs in runtime-service, probably more or less just a "smoke test".
   > I'm not really familiar with what's worth being integration tested in the non-Quarkus ITs.
   
   I actually found it very helpful to have the Quarkus integration tests because it helped verify the `smallrye` Configs. Maybe that's a matter of me being new to CDIs but I think it's still helpful to have that coverage.
   
   @dimas-b - I would love to get your thoughts on this suggestion as well


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2395294643


##########
runtime/service/src/main/java/org/apache/polaris/service/config/AuthorizationConfiguration.java:
##########
@@ -0,0 +1,42 @@
+/*
+ * 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.service.config;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import io.smallrye.config.WithName;
+import java.util.Optional;
+
+@ConfigMapping(prefix = "polaris.authorization")
+public interface AuthorizationConfiguration {
+  @WithDefault("default")
+  String implementation();
+
+  OpaConfig opa();
+
+  interface OpaConfig {
+    Optional<String> url();
+
+    @WithName("policy-path")

Review Comment:
   yep - this was a mess I created while debugging an issue



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2434070576


##########
runtime/test-common/src/main/java/org/apache/polaris/test/commons/OpaTestResource.java:
##########
@@ -0,0 +1,123 @@
+/*
+ * 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.test.commons;
+
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+public class OpaTestResource implements QuarkusTestResourceLifecycleManager {

Review Comment:
   Do we need this class in a separate module? Should it be with its callers?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2440084581


##########
runtime/test-common/src/main/java/org/apache/polaris/test/commons/OpaTestResource.java:
##########
@@ -0,0 +1,123 @@
+/*
+ * 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.test.commons;
+
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+public class OpaTestResource implements QuarkusTestResourceLifecycleManager {
+  private static GenericContainer<?> opa;
+  private int mappedPort;
+  private Map<String, String> resourceConfig;
+
+  @Override
+  public void init(Map<String, String> initArgs) {
+    this.resourceConfig = initArgs;
+  }
+
+  @Override
+  public Map<String, String> start() {
+    try {
+      // Reuse container across tests to speed up execution
+      if (opa == null || !opa.isRunning()) {
+        opa =
+            new GenericContainer<>(DockerImageName.parse("openpolicyagent/opa:0.63.0"))

Review Comment:
   Let me look into this



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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")

Review Comment:
   This is a great idea - I'll add this



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   Yeah I agree.
   
   Let me take a look at this again, I'm new to the ConfigMapping library and I had trouble getting it to work without having the nested interfaces being type checked as well.



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   What do you mean by unifying both properties? Do you mean to just use one property that encapsulates the policy URL?



##########
runtime/service/src/intTest/java/org/apache/polaris/service/auth/opa/OpaFileTokenIntegrationTest.java:
##########
@@ -0,0 +1,293 @@
+/*
+ * 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.service.auth.opa;
+
+import static io.restassured.RestAssured.given;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import io.quarkus.test.junit.QuarkusTestProfile.TestResourceEntry;
+import io.quarkus.test.junit.TestProfile;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.polaris.test.commons.OpaTestResource;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+@TestProfile(OpaFileTokenIntegrationTest.FileTokenOpaProfile.class)
+public class OpaFileTokenIntegrationTest {
+
+  @ConfigProperty(name = "polaris.authorization.opa.auth.bearer.file-based.path")
+  String tokenFilePath;
+
+  public static class FileTokenOpaProfile implements QuarkusTestProfile {
+
+    @Override
+    public Map<String, String> getConfigOverrides() {
+      Map<String, String> config = new HashMap<>();
+      config.put("polaris.authorization.type", "opa");
+      config.put("polaris.authorization.opa.policy-path", "/v1/data/polaris/authz");
+      config.put("polaris.authorization.opa.http.timeout-ms", "2000");
+
+      // Configure OPA server authentication with file-based bearer token
+      config.put("polaris.authorization.opa.auth.type", "bearer");
+      config.put("polaris.authorization.opa.auth.bearer.type", "file-based");
+      // Token file path will be provided by OpaFileTokenTestResource
+      config.put(
+          "polaris.authorization.opa.http.verify-ssl",
+          "false"); // Disable SSL verification for tests
+
+      // TODO: Add tests for OIDC and federated principal
+      config.put("polaris.authentication.type", "internal");
+
+      return config;
+    }
+
+    @Override
+    public List<TestResourceEntry> testResources() {
+      String customRegoPolicy =
+          """
+        package polaris.authz
+
+        default allow := false
+
+        # Allow root user for all operations
+        allow {
+          input.actor.principal == "root"
+        }
+
+        # Allow admin user for all operations
+        allow {
+          input.actor.principal == "admin"
+        }
+
+        # Deny stranger user explicitly (though default is false)
+        allow {
+          input.actor.principal == "stranger"
+          false
+        }
+        """;
+
+      return List.of(
+          new TestResourceEntry(
+              OpaTestResource.class,
+              Map.of("policy-name", "polaris-authz", "rego-policy", customRegoPolicy)),
+          new TestResourceEntry(OpaFileTokenTestResource.class));
+    }
+  }
+
+  /**
+   * Test demonstrates OPA integration with file-based bearer token authentication. This test
+   * verifies that the FileBearerTokenProvider correctly reads tokens from a file and that the full
+   * integration works with file-based configuration.
+   */
+  @Test
+  void testOpaAllowsRootUserWithFileToken() {
+    // Test demonstrates the complete integration flow with file-based tokens:
+    // 1. OAuth token acquisition with internal authentication
+    // 2. OPA policy allowing root users
+    // 3. Bearer token read from file by FileBearerTokenProvider
+
+    // Get a token using the catalog service OAuth endpoint
+    String response =
+        given()
+            .contentType("application/x-www-form-urlencoded")
+            .formParam("grant_type", "client_credentials")
+            .formParam("client_id", "test-admin")
+            .formParam("client_secret", "test-secret")
+            .formParam("scope", "PRINCIPAL_ROLE:ALL")
+            .when()
+            .post("/api/catalog/v1/oauth/tokens")
+            .then()
+            .statusCode(200)
+            .extract()
+            .body()
+            .asString();
+
+    // Parse JSON response to get access_token
+    String accessToken = extractJsonValue(response, "access_token");
+
+    if (accessToken == null) {
+      fail("Failed to parse access_token from OAuth response: " + response);
+    }
+
+    // Use the Bearer token to test OPA authorization
+    // The JWT token has principal "root" which our policy allows
+    given()
+        .header("Authorization", "Bearer " + accessToken)
+        .when()
+        .get("/api/management/v1/principals")
+        .then()
+        .statusCode(200); // Should succeed - "root" user is allowed by policy
+  }
+
+  @Test
+  void testFileTokenRefresh() throws IOException, InterruptedException {
+    // This test verifies that the FileBearerTokenProvider refreshes tokens from the file
+
+    // First verify the system works with the initial token
+    String rootToken = getRootToken();
+
+    given()
+        .header("Authorization", "Bearer " + rootToken)
+        .when()
+        .get("/api/management/v1/principals")
+        .then()
+        .statusCode(200);
+
+    // Get the token file path from injected configuration
+    Path tokenFile = Path.of(tokenFilePath);
+    if (Files.exists(tokenFile)) {
+      String originalContent = Files.readString(tokenFile);
+
+      // Update the file content
+      Files.writeString(tokenFile, "test-opa-bearer-token-updated-12345");
+
+      // Wait for refresh interval (1 second as configured) plus some buffer
+      Thread.sleep(1500); // 1.5 seconds to ensure refresh happens

Review Comment:
   Noted - let me make this change



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   Imho it's better to be explicit by using a `type` here to trigger validation checks



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.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 FileBearerTokenProvider(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 FileBearerTokenProvider(
+      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);
+
+      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 = getJwtExpirationTime(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)) {

Review Comment:
   Taking notes - thank you!



##########
runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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.service.auth;
+
+import io.smallrye.common.annotation.Identifier;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
+import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
+import org.apache.polaris.core.config.RealmConfig;
+
+/** Factory for creating the default Polaris authorizer implementation. */
+@ApplicationScoped
+@Identifier("internal")
+public class DefaultPolarisAuthorizerFactory implements PolarisAuthorizerFactory {

Review Comment:
   I think that makes sense? @flyrain @dimas-b - do you have opinion on this as well?



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.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 FileBearerTokenProvider(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 FileBearerTokenProvider(
+      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) {

Review Comment:
   I agree with this logic - the provider would only close when the Authorizer is brought down as well



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2459205566


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,252 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+import org.apache.polaris.nosql.async.AsyncExec;
+import org.apache.polaris.nosql.async.Cancelable;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Supplier<Instant> clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+  private final AsyncExec asyncExec;
+  private final CompletableFuture<String> initialTokenFuture = new CompletableFuture<>();
+  private final long initialTokenWaitMillis;
+
+  private volatile String cachedToken;
+  private volatile Instant lastRefresh;
+  private volatile Instant nextRefresh;
+  private volatile Cancelable<?> refreshTask;
+
+  /**
+   * 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
+   * @param clock clock instance for time operations
+   * @throws IllegalStateException if the initial token cannot be loaded from the file
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Duration initialTokenWait,
+      AsyncExec asyncExec,
+      Supplier<Instant> clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.initialTokenWaitMillis = initialTokenWait.toMillis();
+    this.clock = clock;
+    this.asyncExec = asyncExec;
+
+    this.nextRefresh = Instant.MIN;
+    this.lastRefresh = Instant.MIN;
+    // start refreshing the token (immediately)
+    scheduleRefreshAttempt(Duration.ZERO);
+
+    checkState(Files.isReadable(tokenFilePath), "OPA token file does not exist or is not readable");
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}, next refresh: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer,
+        nextRefresh);
+  }
+
+  @Override
+  public String getToken() {
+    ;
+    if (cachedToken != null) {
+      // Regular case, we have a cached token
+      return cachedToken;
+    }

Review Comment:
   Nit: saves one volatile read.
   ```suggestion
       String token = cachedToken;
       if (token != null) {
         // Regular case, we have a cached token
         return token;
       }
   ```
   



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.google.common.base.Strings;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  private volatile String cachedToken;
+  private volatile Instant lastRefresh;
+  private volatile Instant nextRefresh;
+
+  /**
+   * 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
+   * @param clock clock instance for time operations
+   * @throws IllegalStateException if the initial token cannot be loaded from the file
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+
+    // Load initial token eagerly to avoid race conditions during first getToken() calls
+    this.cachedToken = loadTokenFromFile();
+    if (Strings.isNullOrEmpty(this.cachedToken)) {
+      throw new IllegalStateException(
+          "Failed to load initial bearer token from file: "
+              + tokenFilePath
+              + ". This is required for OPA authorization.");
+    }
+
+    this.lastRefresh = clock.instant();
+    this.nextRefresh = calculateNextRefresh(this.cachedToken);
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}, next refresh: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer,
+        nextRefresh);
+  }
+
+  @Override
+  public String getToken() {
+    // Check if we need to refresh
+    if (shouldRefresh()) {
+      refreshToken();
+    }
+
+    // Token is guaranteed to be present after construction, but check anyway for safety
+    if (Strings.isNullOrEmpty(cachedToken)) {
+      throw new RuntimeException(
+          "Bearer token is unexpectedly empty. This should not happen after successful construction.");
+    }
+    return cachedToken;

Review Comment:
   I guess this is outdated now with the new commits.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2454142500


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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")

Review Comment:
   As long as users can leave the default verify-ssl value for http, that's fine.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2455661653


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.google.common.base.Strings;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  private volatile String cachedToken;
+  private volatile Instant lastRefresh;
+  private volatile Instant nextRefresh;
+
+  /**
+   * 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
+   * @param clock clock instance for time operations
+   * @throws IllegalStateException if the initial token cannot be loaded from the file
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+
+    // Load initial token eagerly to avoid race conditions during first getToken() calls
+    this.cachedToken = loadTokenFromFile();
+    if (Strings.isNullOrEmpty(this.cachedToken)) {
+      throw new IllegalStateException(
+          "Failed to load initial bearer token from file: "
+              + tokenFilePath
+              + ". This is required for OPA authorization.");
+    }
+
+    this.lastRefresh = clock.instant();
+    this.nextRefresh = calculateNextRefresh(this.cachedToken);
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}, next refresh: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer,
+        nextRefresh);
+  }
+
+  @Override
+  public String getToken() {
+    // Check if we need to refresh
+    if (shouldRefresh()) {
+      refreshToken();
+    }
+
+    // Token is guaranteed to be present after construction, but check anyway for safety
+    if (Strings.isNullOrEmpty(cachedToken)) {
+      throw new RuntimeException(
+          "Bearer token is unexpectedly empty. This should not happen after successful construction.");
+    }
+    return cachedToken;

Review Comment:
   technically this is still racy since `cachedToken` is `volatile`... it is not likely to materialize as a bug, but the code pattern looks a bit worrisome 😅 why not load the value into a local var, check and return from local?
   
   e.g. `return Preconditions.checkNotNull(cachedToken, "Bearer token is unexpectedly empty ...")`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2451948832


##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   I updated the config to adopt this strategy by checking for mutual exclusion on the two bearer token prefixes within the `validate` block



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453119284


##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -197,6 +197,40 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization type settings
+# Which authorizer to use: "internal" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+# polaris.authorization.type=internal
+
+# OPA Authorizer Configuration: effective only if polaris.authorization.type=opa
+# NOTE: The OPA Authorizer is currently in Beta and is not a stable release.
+# It may undergo breaking changes in future versions.
+# polaris.authorization.opa.url=http://localhost:8181
+# polaris.authorization.opa.policy-path=/v1/data/polaris/authz/allow
+
+# OPA HTTP configuration
+# polaris.authorization.opa.http.timeout-ms=5000
+# polaris.authorization.opa.http.verify-ssl=false

Review Comment:
   Just added it as well @snazy - thank you!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453338636


##########
extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactoryTest.java:
##########
@@ -0,0 +1,163 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.Optional;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class OpaPolarisAuthorizerFactoryTest {
+
+  @TempDir Path tempDir;
+
+  @Test
+  public void testFactoryWithStaticTokenConfiguration() {
+    // Mock configuration for static token
+    OpaAuthorizationConfig.BearerTokenConfig.StaticTokenConfig staticTokenConfig =
+        mock(OpaAuthorizationConfig.BearerTokenConfig.StaticTokenConfig.class);

Review Comment:
   nit: `OpaAuthorizationConfig` and related interfaces could be annotated with `@PolarisImmutable` to get auto-generated default impl. IMHO, using immutable builders is nicer than mocking... but this is totally optional.



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,222 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.google.common.base.Strings;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  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 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
+   * @param clock clock instance for time operations
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer);
+  }
+
+  @Override
+  public String getToken() {
+    checkState(!closed, "Token provider is closed");
+
+    // Check if we need to refresh
+    if (shouldRefresh()) {
+      refreshToken();
+    }
+
+    // If we couldn't load a token and have no cached token, this is a fatal error
+    if (Strings.isNullOrEmpty(cachedToken)) {
+      throw new RuntimeException(
+          "Unable to load bearer token from file: "
+              + tokenFilePath
+              + ". This is required for OPA authorization.");
+    }
+    return cachedToken;
+  }
+
+  @Override
+  public void close() {
+    closed = true;
+    cachedToken = null;
+  }
+
+  private boolean shouldRefresh() {
+    return clock.instant().isAfter(nextRefresh);
+  }
+
+  private void refreshToken() {
+    if (!refreshLock.compareAndSet(false, true)) {
+      return;

Review Comment:
   If two threads come here from `getToken()` on a freshly created object, and the loading of the token takes some time, one the threads will get `null` as the token... right? If this is not a concern, could you add a comment?



##########
runtime/service/src/intTest/java/org/apache/polaris/service/it/ServiceProducersIT.java:
##########
@@ -0,0 +1,55 @@
+/*
+ * 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.service.it;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;

Review Comment:
   Most of the (new) tests use AssertJ assertions... Would you mind switching to it?



##########
runtime/service/src/intTest/java/org/apache/polaris/service/it/ServiceProducersIT.java:
##########
@@ -0,0 +1,55 @@
+/*
+ * 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.service.it;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import jakarta.inject.Inject;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.junit.jupiter.api.Test;
+
+public class ServiceProducersIT {
+
+  public static class InternalAuthorizationConfig implements QuarkusTestProfile {
+    @Override
+    public Map<String, String> getConfigOverrides() {
+      Map<String, String> config = new HashMap<>();
+      config.put("polaris.authorization.type", "internal");
+      return config;
+    }
+  }
+
+  @QuarkusTest
+  @io.quarkus.test.junit.TestProfile(ServiceProducersIT.InternalAuthorizationConfig.class)

Review Comment:
   nit: import?



##########
extensions/auth/opa/build.gradle.kts:
##########


Review Comment:
   `extensions/auth/opa/` seems to be an empty module... Does it need a build script?



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,222 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.google.common.base.Strings;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  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 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
+   * @param clock clock instance for time operations
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer);
+  }
+
+  @Override
+  public String getToken() {
+    checkState(!closed, "Token provider is closed");

Review Comment:
   Is it worth the trouble? It looks like we keep the `closed` flag only for this check.



##########
runtime/defaults/src/main/resources/application-it.properties:
##########
@@ -56,3 +56,4 @@ polaris.realm-context.realms=POLARIS,OTHER
 polaris.storage.gcp.token=token
 polaris.storage.gcp.lifespan=PT1H
 
+

Review Comment:
   spurious change?



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,176 @@
+/*
+ * 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.fasterxml.jackson.databind.ObjectMapper;
+import io.smallrye.common.annotation.Identifier;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.time.Clock;
+import java.time.Duration;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+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.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@ApplicationScoped
+@Identifier("opa")
+class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private static final Logger logger = LoggerFactory.getLogger(OpaPolarisAuthorizerFactory.class);
+
+  private final OpaAuthorizationConfig opaConfig;
+  private final Clock clock;
+  private CloseableHttpClient httpClient;
+  private BearerTokenProvider bearerTokenProvider;
+  private ObjectMapper objectMapper;
+
+  @Inject
+  public OpaPolarisAuthorizerFactory(OpaAuthorizationConfig opaConfig, Clock clock) {
+    this.opaConfig = opaConfig;
+    this.clock = clock;
+    this.objectMapper = new ObjectMapper();
+  }
+
+  /**
+   * Gets the OPA authorization configuration. Used by OpaProductionReadinessCheck
+   *
+   * @return the OPA configuration
+   */
+  OpaAuthorizationConfig getConfig() {
+    return opaConfig;
+  }
+
+  @PostConstruct
+  public void initialize() {
+    // Validate configuration once during startup
+    opaConfig.validate();
+
+    // Create HTTP client once during startup
+    httpClient = createHttpClient();
+
+    // Setup authentication once during startup
+    setupAuthentication(opaConfig.auth());

Review Comment:
   maybe `bearerTokenProvider = setupAuthentication(opaConfig.auth())`?.. to follow the pattern of the `createHttpClient()` call above.



##########
runtime/service/src/intTest/java/org/apache/polaris/service/it/ServiceProducersIT.java:
##########
@@ -0,0 +1,55 @@
+/*
+ * 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.service.it;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import jakarta.inject.Inject;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.junit.jupiter.api.Test;
+
+public class ServiceProducersIT {
+
+  public static class InternalAuthorizationConfig implements QuarkusTestProfile {
+    @Override
+    public Map<String, String> getConfigOverrides() {
+      Map<String, String> config = new HashMap<>();
+      config.put("polaris.authorization.type", "internal");
+      return config;
+    }
+  }
+
+  @QuarkusTest
+  @io.quarkus.test.junit.TestProfile(ServiceProducersIT.InternalAuthorizationConfig.class)
+  public static class InternalAuthorizationTest {
+
+    @Inject PolarisAuthorizer polarisAuthorizer;
+
+    @Test
+    void testInternalPolarisAuthorizerProduced() {
+      assertNotNull(polarisAuthorizer, "PolarisAuthorizer should be produced");
+      // Verify it's the correct implementation for internal config
+      assertNotNull(polarisAuthorizer, "Internal PolarisAuthorizer should not be null");

Review Comment:
   This assertion always holds if line 50 passes, right?



##########
runtime/service/build.gradle.kts:
##########
@@ -75,6 +75,7 @@ dependencies {
 
   implementation(libs.auth0.jwt)
 
+  implementation(libs.apache.httpclient5)

Review Comment:
   Do we need this dep declaration? Is is not available transitively?



##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -139,10 +139,23 @@ public RealmConfig realmConfig(CallContext callContext) {
     return callContext.getRealmConfig();
   }
 
+  @Produces
+  @ApplicationScoped
+  public PolarisAuthorizerFactory polarisAuthorizerFactory(
+      AuthorizationConfiguration authorizationConfig,
+      @Any Instance<PolarisAuthorizerFactory> authorizerFactories) {
+    PolarisAuthorizerFactory factory =
+        authorizerFactories
+            .select(Identifier.Literal.of(authorizationConfig.type().getValue()))

Review Comment:
   Is it worth having an enum for the type? `PolarisAuthorizerFactory` are pluggable at build time. There is no way for `runtime/service` code to be in sync will all possible downstream projects that want to customize `PolarisAuthorizerFactory` 🤔 



##########
build.gradle.kts:
##########
@@ -89,7 +89,7 @@ tasks.named<RatTask>("rat").configure {
   // Misc build artifacts
   excludes.add(".java-version")
   excludes.add("**/.keep")
-  excludes.add("logs/**")
+  excludes.add("extensions/auth/opa/tests/logs/**")

Review Comment:
   Why would test logs be outside `build` or `tmp` directories? 🤔 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2454430027


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,222 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.google.common.base.Strings;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  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 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
+   * @param clock clock instance for time operations
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer);
+  }
+
+  @Override
+  public String getToken() {
+    checkState(!closed, "Token provider is closed");
+
+    // Check if we need to refresh
+    if (shouldRefresh()) {
+      refreshToken();
+    }
+
+    // If we couldn't load a token and have no cached token, this is a fatal error
+    if (Strings.isNullOrEmpty(cachedToken)) {
+      throw new RuntimeException(
+          "Unable to load bearer token from file: "
+              + tokenFilePath
+              + ". This is required for OPA authorization.");
+    }
+    return cachedToken;
+  }
+
+  @Override
+  public void close() {
+    closed = true;
+    cachedToken = null;
+  }
+
+  private boolean shouldRefresh() {
+    return clock.instant().isAfter(nextRefresh);
+  }
+
+  private void refreshToken() {
+    if (!refreshLock.compareAndSet(false, true)) {
+      return;

Review Comment:
   Damn, sorry, my fault. @dimas-b is right, there are a few bad race conditions.
   I think the best way is to schedule token refreshes asynchronously.
   I took a stab at this in this PR against @sungwy 's branch https://github.com/sungwy/polaris/pull/1.
   It's using `AsyncExec` and its `MockAsyncExec` that allows validation of scheduled tasks.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2452873228


##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -197,6 +197,40 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization type settings
+# Which authorizer to use: "internal" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+# polaris.authorization.type=internal
+
+# OPA Authorizer Configuration: effective only if polaris.authorization.type=opa
+# NOTE: The OPA Authorizer is currently in Beta and is not a stable release.
+# It may undergo breaking changes in future versions.
+# polaris.authorization.opa.url=http://localhost:8181
+# polaris.authorization.opa.policy-path=/v1/data/polaris/authz/allow
+
+# OPA HTTP configuration
+# polaris.authorization.opa.http.timeout-ms=5000
+# polaris.authorization.opa.http.verify-ssl=false

Review Comment:
   added the trust-store settings



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422241130


##########
polaris-core/build.gradle.kts:
##########
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(project(":polaris-api-management-model"))
+  implementation(libs.apache.httpclient5)

Review Comment:
   > Do we still need this dep. in core?
   
   No. I think it’s perfectly fine to keep the OPA authorizer within the `polaris-runtime-service` module rather than moving it to a separate extension module.
   The rationale for separating components like `hive` or `hadoop` federation is that they introduce heavy and intrusive `Hadoop` dependencies that we don’t want to pull into the service module.
   The OPA authorizer, on the other hand, adds no additional dependencies, it integrates cleanly within the service module, so there’s no downside to keeping it there.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "adutra (via GitHub)" <gi...@apache.org>.
adutra commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2399624194


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      int timeoutMs,
+      OkHttpClient client,
+      ObjectMapper mapper) {
+    OkHttpClient clientWithTimeout =
+        (client != null)
+            ? client.newBuilder().callTimeout(java.time.Duration.ofMillis(timeoutMs)).build()
+            : new OkHttpClient.Builder()
+                .callTimeout(java.time.Duration.ofMillis(timeoutMs))
+                .build();
+    ObjectMapper objectMapper = (mapper != null) ? mapper : new ObjectMapper();
+    return new OpaPolarisAuthorizer(opaServerUrl, opaPolicyPath, clientWithTimeout, objectMapper);
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+      RequestBody body = RequestBody.create(inputJson, MediaType.parse("application/json"));
+      Request request = new Request.Builder().url(opaServerUrl + opaPolicyPath).post(body).build();
+      try (Response response = httpClient.newCall(request).execute()) {
+        if (!response.isSuccessful()) return false;
+        // Parse response JSON for 'result.allow'
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(response.body().string());
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();

Review Comment:
   @collado-mike I'm afraid this isn't true anymore, have a look at `DefaultAuthenicator` and especially to the `resolvePrincipalRoles` method. `PolarisPrincipal.getPrincipalRoles()` now returns the _resolved_ roles.
   
   I notice though, that the javadocs of `PolarisPrincipal.getPrincipalRoles()` isn't accurate. I will fix that.
   
   But in any case, when the authorizer is invoked, `PolarisPrincipal.getPrincipalRoles()` will basically return the same principal roles that we have in `activatedEntities`. Which is not ideal, I think we all agree.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "collado-mike (via GitHub)" <gi...@apache.org>.
collado-mike commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2399482275


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      int timeoutMs,
+      OkHttpClient client,
+      ObjectMapper mapper) {
+    OkHttpClient clientWithTimeout =
+        (client != null)
+            ? client.newBuilder().callTimeout(java.time.Duration.ofMillis(timeoutMs)).build()
+            : new OkHttpClient.Builder()
+                .callTimeout(java.time.Duration.ofMillis(timeoutMs))
+                .build();
+    ObjectMapper objectMapper = (mapper != null) ? mapper : new ObjectMapper();
+    return new OpaPolarisAuthorizer(opaServerUrl, opaPolicyPath, clientWithTimeout, objectMapper);
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+      RequestBody body = RequestBody.create(inputJson, MediaType.parse("application/json"));
+      Request request = new Request.Builder().url(opaServerUrl + opaPolicyPath).post(body).build();
+      try (Response response = httpClient.newCall(request).execute()) {
+        if (!response.isSuccessful()) return false;
+        // Parse response JSON for 'result.allow'
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(response.body().string());
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();

Review Comment:
   Chiming in - per the community discussion, the `activatedEntities` set has been validated by the Resolver to ensure the principal has activated the roles in the token and that the principal actually has been granted those roles. If the user specifies `PRINCIPAL_ROLE:ALL` in the token, the `PolarisPrincipal`'s list of roles will be empty (this is not ideal, but this is what it is). While 3P identity provider tokens may not have this issue, (unless they support `PRINCIPAL_ROLE:ALL`), all regular Polaris Principals will have this problem - I think we should stick to using `activatedEntities` as the true set of activated roles



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "adutra (via GitHub)" <gi...@apache.org>.
adutra commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2401481490


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(

Review Comment:
   It's perfect 💯 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3386210285

   Thank you all for the reviews. I think this PR is ready for another round of feedback.
   
   I've made note of the next TODO items we could start working on soon after this initial PR is merged, and listed them in the PR description


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2415299897


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/TokenProvider.java:
##########
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+/**
+ * Interface for providing bearer tokens for authentication.
+ *
+ * <p>Implementations can provide tokens from various sources such as:
+ *
+ * <ul>
+ *   <li>Static string values
+ *   <li>Files (with automatic reloading)
+ *   <li>External token services
+ * </ul>
+ */
+public interface TokenProvider {

Review Comment:
   Sure!



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/FileTokenProvider.java:
##########
@@ -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()) {

Review Comment:
   Good catch! I'll remove this leftover from debugging. Thank you for catching it @flyrain



##########
runtime/service/src/main/java/org/apache/polaris/service/config/AuthorizationConfiguration.java:
##########
@@ -0,0 +1,75 @@
+/*
+ * 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.service.config;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import java.util.Optional;
+
+@ConfigMapping(prefix = "polaris.authorization")
+public interface AuthorizationConfiguration {
+  @WithDefault("default")
+  String type();
+
+  OpaConfig opa();
+
+  interface OpaConfig {
+    Optional<String> url();
+
+    Optional<String> policyPath();
+
+    Optional<Integer> timeoutMs();
+
+    BearerTokenConfig bearerToken();
+
+    @WithDefault("true")
+    boolean verifySsl();
+
+    Optional<String> trustStorePath();
+
+    Optional<String> trustStorePassword();
+  }
+
+  interface BearerTokenConfig {

Review Comment:
   Thanks for the suggestion Yufei. I've added the validation in the factory



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility

Review Comment:
   Thanks for catching this Yufei - I've correctly injected the client into the OpaPolarisAuthorizer so we have it set up at the application scope and injected into the class instead (since PolarisAuthorizer is RequestScoped).
   
   As for the mapper - I was being a bit forward thinking in having a catch all input param for future configs. I can remove this for now.



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility
+      ObjectMapper mapper) {
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(timeoutMs)
+              .setSocketTimeout(timeoutMs)
+              .setConnectionRequestTimeout(timeoutMs)
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);
+
+      // Create HTTP client with SSL configuration
+      CloseableHttpClient httpClient;
+      if (client instanceof CloseableHttpClient) {
+        httpClient = (CloseableHttpClient) client;
+      } else {
+        if (sslSocketFactory != null) {
+          httpClient =
+              HttpClients.custom()
+                  .setDefaultRequestConfig(requestConfig)
+                  .setSSLSocketFactory(sslSocketFactory)
+                  .build();
+        } else {
+          httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
+        }
+      }
+
+      ObjectMapper objectMapperWithDefaults = mapper != null ? mapper : new ObjectMapper();
+      return new OpaPolarisAuthorizer(
+          opaServerUrl, opaPolicyPath, tokenProvider, httpClient, objectMapperWithDefaults);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to create OpaPolarisAuthorizer with SSL configuration", e);
+    }
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+
+      // Create HTTP POST request using Apache HttpComponents
+      HttpPost httpPost = new HttpPost(opaServerUrl + opaPolicyPath);
+      httpPost.setHeader("Content-Type", "application/json");
+
+      // Add bearer token authentication if provided
+      if (tokenProvider != null) {
+        String token = tokenProvider.getToken();
+        if (token != null && !token.isEmpty()) {
+          httpPost.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
+        }
+      }
+
+      httpPost.setEntity(new StringEntity(inputJson, StandardCharsets.UTF_8));
+
+      // Execute request
+      try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+        int statusCode = response.getStatusLine().getStatusCode();
+        if (statusCode != 200) {
+          return false;
+        }
+
+        // Read and parse response
+        String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(responseBody);
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();
+    input.set("actor", buildActorNode(principal));
+    input.put("action", op.name());
+    input.set("resource", buildResourceNode(targets, secondaries));
+    input.set("context", buildContextNode());
+    ObjectNode root = objectMapper.createObjectNode();
+    root.set("input", input);

Review Comment:
   Yes, I was just thinking about this :)
   I think a follow up PR to introduce the JSON Schema would be nice for documentation, generating the pojo, and enabling schema validations of the OPA Server Rego https://www.openpolicyagent.org/docs/cli#check



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility
+      ObjectMapper mapper) {
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(timeoutMs)
+              .setSocketTimeout(timeoutMs)
+              .setConnectionRequestTimeout(timeoutMs)
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);
+
+      // Create HTTP client with SSL configuration
+      CloseableHttpClient httpClient;
+      if (client instanceof CloseableHttpClient) {
+        httpClient = (CloseableHttpClient) client;
+      } else {
+        if (sslSocketFactory != null) {
+          httpClient =
+              HttpClients.custom()
+                  .setDefaultRequestConfig(requestConfig)
+                  .setSSLSocketFactory(sslSocketFactory)
+                  .build();
+        } else {
+          httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
+        }
+      }
+
+      ObjectMapper objectMapperWithDefaults = mapper != null ? mapper : new ObjectMapper();
+      return new OpaPolarisAuthorizer(
+          opaServerUrl, opaPolicyPath, tokenProvider, httpClient, objectMapperWithDefaults);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to create OpaPolarisAuthorizer with SSL configuration", e);
+    }
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+
+      // Create HTTP POST request using Apache HttpComponents
+      HttpPost httpPost = new HttpPost(opaServerUrl + opaPolicyPath);

Review Comment:
   The header and response are somewhat standard, the request payload is only standardized to the top level attribute "input", and the path is entirely our decision. I'll add a Java doc to reference the most relevant documentation, and I think we can supplement this with user docs that explain our choices.
   
   > How does a OPA Service support multiple Polaris instance? Configuring different path?
   
   I think we could support it either at the same path through the same OPA Server and Policy Rego with different payloads that refer to a centrally governed policy bundle, or with multiple Polaris instances each configured to hit different OPA Servers running different Rego. Both work in my opinion, and depends strictly on how varied the policy Rego is across instances.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2444917057


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -0,0 +1,201 @@
+/*
+ * 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 com.google.common.base.Strings;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import java.net.URI;
+import java.nio.file.Path;
+import java.time.Duration;
+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 {
+
+  /** Authentication types supported by OPA authorization */
+  enum AuthenticationType {
+    NONE("none"),
+    BEARER("bearer");
+
+    private final String value;
+
+    AuthenticationType(String value) {
+      this.value = value;
+    }
+
+    public String getValue() {
+      return value;
+    }
+  }
+
+  /** Bearer token configuration types */
+  enum BearerTokenType {
+    STATIC_TOKEN("static-token"),
+    FILE_BASED("file-based");
+
+    private final String value;
+
+    BearerTokenType(String value) {
+      this.value = value;
+    }
+
+    public String getValue() {
+      return value;
+    }
+  }
+
+  Optional<URI> policyUri();
+
+  Optional<AuthenticationConfig> auth();
+
+  Optional<HttpConfig> http();

Review Comment:
   Thanks @snazy !
   
   I was just about to do another round of self-review today to address these. Thanks so much for jumping in and reviewing my changes so quickly — that’s a big help.
   
   I had run into some issues earlier with SmallRye Config when using required fields in an optional config (like FileBasedConfig) together with `@WithDefault`. I’ve learned a bit more about how the library behaves and resolved those problems, so I agree that removing these should be fine.



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -0,0 +1,201 @@
+/*
+ * 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 com.google.common.base.Strings;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import java.net.URI;
+import java.nio.file.Path;
+import java.time.Duration;
+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 {
+
+  /** Authentication types supported by OPA authorization */
+  enum AuthenticationType {
+    NONE("none"),
+    BEARER("bearer");
+
+    private final String value;
+
+    AuthenticationType(String value) {
+      this.value = value;
+    }
+
+    public String getValue() {
+      return value;
+    }
+  }
+
+  /** Bearer token configuration types */
+  enum BearerTokenType {
+    STATIC_TOKEN("static-token"),
+    FILE_BASED("file-based");
+
+    private final String value;
+
+    BearerTokenType(String value) {
+      this.value = value;
+    }
+
+    public String getValue() {
+      return value;
+    }
+  }
+
+  Optional<URI> policyUri();
+
+  Optional<AuthenticationConfig> auth();
+
+  Optional<HttpConfig> http();

Review Comment:
   Thanks @snazy !
   
   I was just about to do another round of self-review today to address these. Thanks so much for jumping in and reviewing my changes so quickly, that’s a big help.
   
   I had run into some issues earlier with SmallRye Config when using required fields in an optional config (like FileBasedConfig) together with `@WithDefault`. I’ve learned a bit more about how the library behaves and resolved those problems, so I agree that removing these should be fine.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2443985827


##########
runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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.service.auth;
+
+import io.smallrye.common.annotation.Identifier;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
+import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
+import org.apache.polaris.core.config.RealmConfig;
+
+/** Factory for creating the default Polaris authorizer implementation. */
+@ApplicationScoped
+@Identifier("internal")
+public class DefaultPolarisAuthorizerFactory implements PolarisAuthorizerFactory {

Review Comment:
   `compileOnly` is fine IMO.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453092210


##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,8 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa/impl
+polaris-extensions-auth-opa-tests=extensions/auth/opa/tests

Review Comment:
   Looking into the pipeline, I noticed that the OPA integration tests are still being executed within the `runtime-service` module, specifically via this command:
   
   ```
   ./gradlew \
     intTest \
     -x :polaris-runtime-service:intTest
   ```
   
   This behavior stems from the change in [`[runtime/server/build.gradle.kts](https://github.com/apache/polaris/pull/2680/files#diff-9895ab333561c6f07093e25cd9915f15caa3901efe46cdf0dffdf4c8b34cb5ad)`](https://github.com/apache/polaris/pull/2680/files#diff-9895ab333561c6f07093e25cd9915f15caa3901efe46cdf0dffdf4c8b34cb5ad).
   
   I’m not particularly a fan of this setup, but I’m OK with it, and I haven’t observed any performance issues.
   
   For reference:
   
   With OPA ~12 mins, https://github.com/apache/polaris/actions/runs/18717788753/job/53381604223?pr=2680
   Without OPA ~11mins, https://github.com/apache/polaris/actions/runs/18701076539/job/53329779421?pr=2852



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2379791838


##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -193,6 +193,15 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization implementation settings
+# Which authorizer to use: "default" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+polaris.authorization.implementation=default
+
+# OPA Authorizer Configuration: effective only if polaris.authorization.implementation=opa
+# polaris.authorization.opa.url=http://localhost:8181

Review Comment:
   Does it requite authentication?



##########
polaris-core/build.gradle.kts:
##########
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(project(":polaris-api-management-model"))
+  implementation("com.squareup.okhttp3:okhttp:4.12.0")

Review Comment:
   Polaris Servers already have the Apache Http client. Would it be good enough for this authorizer?
   
   Side note: please use `gradle/libs.versions.toml` for version management.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "adutra (via GitHub)" <gi...@apache.org>.
adutra commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2380132237


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      int timeoutMs,
+      OkHttpClient client,
+      ObjectMapper mapper) {
+    OkHttpClient clientWithTimeout =
+        (client != null)
+            ? client.newBuilder().callTimeout(java.time.Duration.ofMillis(timeoutMs)).build()
+            : new OkHttpClient.Builder()
+                .callTimeout(java.time.Duration.ofMillis(timeoutMs))
+                .build();
+    ObjectMapper objectMapper = (mapper != null) ? mapper : new ObjectMapper();
+    return new OpaPolarisAuthorizer(opaServerUrl, opaPolicyPath, clientWithTimeout, objectMapper);
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+      RequestBody body = RequestBody.create(inputJson, MediaType.parse("application/json"));
+      Request request = new Request.Builder().url(opaServerUrl + opaPolicyPath).post(body).build();
+      try (Response response = httpClient.newCall(request).execute()) {
+        if (!response.isSuccessful()) return false;
+        // Parse response JSON for 'result.allow'
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(response.body().string());
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();

Review Comment:
   Also, I'd suggest that you take the roles from `PolarisPrincipal`, not from `activatedEntities`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2380307067


##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -193,6 +193,15 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization implementation settings
+# Which authorizer to use: "default" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+polaris.authorization.implementation=default
+
+# OPA Authorizer Configuration: effective only if polaris.authorization.implementation=opa
+# polaris.authorization.opa.url=http://localhost:8181

Review Comment:
   I can add support for HTTPS. I think we can keep the support HTTP in case the OPA server is being run as a side case



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2479039242


##########
runtime/service/build.gradle.kts:
##########
@@ -106,6 +106,8 @@ dependencies {
 
   implementation(libs.jakarta.servlet.api)
 
+  runtimeOnly(project(":polaris-async-vertx"))

Review Comment:
   Is this only for test?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2423027338


##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaHttpClientFactory.java:
##########
@@ -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;

Review Comment:
   I simply used Apache HTTPc because it's also used in `apache-iceberg`.
   
   However, if we don't foresee ourselves needing the additional configurability of Apache HTTPc, I can make do with `java.net.http.HttpClient` for now.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2454824309


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,222 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.google.common.base.Strings;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  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 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
+   * @param clock clock instance for time operations
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer);
+  }
+
+  @Override
+  public String getToken() {
+    checkState(!closed, "Token provider is closed");
+
+    // Check if we need to refresh
+    if (shouldRefresh()) {
+      refreshToken();
+    }
+
+    // If we couldn't load a token and have no cached token, this is a fatal error
+    if (Strings.isNullOrEmpty(cachedToken)) {
+      throw new RuntimeException(
+          "Unable to load bearer token from file: "
+              + tokenFilePath
+              + ". This is required for OPA authorization.");
+    }
+    return cachedToken;
+  }
+
+  @Override
+  public void close() {
+    closed = true;
+    cachedToken = null;
+  }
+
+  private boolean shouldRefresh() {
+    return clock.instant().isAfter(nextRefresh);
+  }
+
+  private void refreshToken() {
+    if (!refreshLock.compareAndSet(false, true)) {
+      return;

Review Comment:
   That's very kind of you @snazy - let me take a look at this when I get some bandwidth this week



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2455065243


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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")

Review Comment:
   Sounds good



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2444039255


##########
extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/token/StaticBearerTokenProviderTest.java:
##########
@@ -0,0 +1,48 @@
+/*
+ * 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.token;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class StaticBearerTokenProviderTest {
+
+  @Test
+  public void testStaticBearerTokenProvider() {
+    String expectedToken = "static-bearer-token";
+    StaticBearerTokenProvider provider = new StaticBearerTokenProvider(expectedToken);

Review Comment:
   Mind add a try-with-resources in this test class as well?
   IntelliJ shows warnings because of unclosed resources here.



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -0,0 +1,201 @@
+/*
+ * 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 com.google.common.base.Strings;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import java.net.URI;
+import java.nio.file.Path;
+import java.time.Duration;
+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 {
+
+  /** Authentication types supported by OPA authorization */
+  enum AuthenticationType {
+    NONE("none"),
+    BEARER("bearer");
+
+    private final String value;
+
+    AuthenticationType(String value) {
+      this.value = value;
+    }
+
+    public String getValue() {
+      return value;
+    }
+  }
+
+  /** Bearer token configuration types */
+  enum BearerTokenType {
+    STATIC_TOKEN("static-token"),
+    FILE_BASED("file-based");
+
+    private final String value;
+
+    BearerTokenType(String value) {
+      this.value = value;
+    }
+
+    public String getValue() {
+      return value;
+    }
+  }
+
+  Optional<URI> policyUri();
+
+  Optional<AuthenticationConfig> auth();
+
+  Optional<HttpConfig> http();

Review Comment:
   These three properties don't need to be `Optional`s (simplifies the code quite a bit, smallrye-config/Quarkus would emit relevant startup errors for missing mandatory properties).



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,307 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  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 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 FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer) {
+    this(
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer,
+        Clock.systemUTC());
+  }
+
+  /**
+   * Create a new file-based token provider with JWT expiration support and custom clock.
+   * Package-private constructor for testing purposes.
+   *
+   * @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
+   * @param clock clock instance for time operations (useful for testing)
+   */
+  FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "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 clock.instant().isAfter(nextRefresh);
+  }
+
+  private void refreshToken() {
+    lock.writeLock().lock();

Review Comment:
   Nit, with all other `lock` usages removed, it can be simplified by changing
   ```java
     private final ReadWriteLock lock = new ReentrantReadWriteLock();
   ```
   to
   ```java
     private final AtomicBoolean refreshLock = new AtomicBoolean();
   ```
   
   and this in this function:
   ```java
   if (!refreshLock.compareAndSet(false, true)) {
     return;
   }
   try {
     // refresh code
   } finally {
     refreshLock.set(false);
   }
   ```



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,307 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  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 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 FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer) {
+    this(
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer,
+        Clock.systemUTC());
+  }
+
+  /**
+   * Create a new file-based token provider with JWT expiration support and custom clock.
+   * Package-private constructor for testing purposes.
+   *
+   * @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
+   * @param clock clock instance for time operations (useful for testing)
+   */
+  FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "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();
+    }

Review Comment:
   ```suggestion
       cachedToken = null;
   ```



##########
extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/OpaTestResource.java:
##########
@@ -0,0 +1,137 @@
+/*
+ * 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 io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.polaris.containerspec.ContainerSpecHelper;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+public class OpaTestResource implements QuarkusTestResourceLifecycleManager {
+  private static GenericContainer<?> opa;
+  private int mappedPort;
+  private Map<String, String> resourceConfig;

Review Comment:
   Nit: unused (assigned, but not read)



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,307 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  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 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 FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer) {
+    this(
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer,
+        Clock.systemUTC());
+  }
+
+  /**
+   * Create a new file-based token provider with JWT expiration support and custom clock.
+   * Package-private constructor for testing purposes.
+   *
+   * @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
+   * @param clock clock instance for time operations (useful for testing)
+   */
+  FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "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 clock.instant().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();
+
+      // If we couldn't load a token and have no cached token, this is a fatal error
+      if (newToken == null && cachedToken == null) {
+        throw new RuntimeException(
+            "Unable to load bearer token from file: "
+                + tokenFilePath
+                + ". This is required for OPA authorization.");
+      }
+
+      // Only update cached token if we successfully loaded a new one
+      if (newToken != null) {
+        cachedToken = newToken;
+      }
+      // If newToken is null but cachedToken exists, we keep using the cached token
+
+      lastRefresh = clock.instant();
+
+      // Calculate next refresh time based on current token (may be cached)
+      nextRefresh = calculateNextRefresh(cachedToken);
+
+      logger.debug(
+          "Token refreshed from file: {} (token present: {}), next refresh: {}",
+          tokenFilePath,
+          cachedToken != null && !cachedToken.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 = getJwtExpirationTime(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 = clock.instant().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() {
+    int attempts = 0;
+    long deadlineMs =
+        clock.millis()
+            + 3000; // 3 second deadline for retries (reasonable for CSI driver file mounts)
+    String currentCachedToken = cachedToken; // Snapshot of current cache
+
+    while (true) {
+      try {
+        // Check if file is readable first
+        if (!Files.isReadable(tokenFilePath)) {
+          // Treat as transient error and retry (could be CSI driver not ready yet)
+          throw new IOException("File is not readable: " + tokenFilePath);
+        }
+
+        String token = Files.readString(tokenFilePath, StandardCharsets.UTF_8).trim();
+        if (!token.isEmpty()) {
+          return token;
+        }
+
+        // Empty token - treat as transient issue (could be file being written)
+        throw new IOException("File contains only whitespace: " + tokenFilePath);
+      } catch (IOException e) {
+        // Treat file system exceptions as transient (file being rotated, temporary unavailability)
+        logger.debug(
+            "Transient error reading token file (attempt {}): {}", attempts + 1, e.getMessage());
+      }
+
+      // If we have a cached token and it's safe to use, fall back to it
+      if (currentCachedToken != null && !currentCachedToken.isEmpty()) {
+        logger.warn(
+            "Failed to read new token from file after {} attempts, using cached token",
+            attempts + 1);
+        return currentCachedToken;
+      }
+
+      // No cached token available and we can't read from file
+      attempts++;
+      if (attempts >= 5 || clock.millis() > deadlineMs) {
+        // Return null to let refreshToken() decide how to handle this based on cached token state
+        logger.debug("Token unavailable after {} attempts", attempts);
+        return null;
+      }
+
+      // Exponential backoff: 100ms, 200ms, 400ms, 800ms, 1000ms (reasonable for CSI driver
+      // scenarios)
+      long delayMs = Math.min(100L << (attempts - 1), 1000);
+      try {
+        Thread.sleep(delayMs);
+      } catch (InterruptedException ie) {
+        Thread.currentThread().interrupt();
+        logger.debug("Interrupted while reading token, returning null");
+        return null;
+      }
+    }
+  }

Review Comment:
   I think, the whole function can be simplified like this:
   ```suggestion
     private String loadTokenFromFile() {
       try {
         String token = Files.readString(tokenFilePath, StandardCharsets.UTF_8).trim();
         if (!token.isEmpty()) {
           return token;
         }
       } catch (IOException e) {
         logger.debug("Failed to read token from file", e);
       }
       return null;
     }
   ```



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   Hm, not really.
   
   Given
   ```
   polaris.authorization.opa.auth.bearer.file-based.path=/path/to/token/file
   ```
   `BearerTokenConfig.staticToken()` will be empty and `BearerTokenConfig.fileBased()` will not be empty. Feels a bit redundant to require a type property. WDYT?
   



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,307 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  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 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 FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer) {
+    this(
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer,
+        Clock.systemUTC());
+  }
+
+  /**
+   * Create a new file-based token provider with JWT expiration support and custom clock.
+   * Package-private constructor for testing purposes.
+   *
+   * @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
+   * @param clock clock instance for time operations (useful for testing)
+   */
+  FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "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();
+    }

Review Comment:
   The read-lock isn't necessary, the field is already `volatile`.
   
   ```suggestion
       return cachedToken;
   ```



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,307 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  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 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 FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer) {
+    this(
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer,
+        Clock.systemUTC());
+  }
+
+  /**
+   * Create a new file-based token provider with JWT expiration support and custom clock.
+   * Package-private constructor for testing purposes.
+   *
+   * @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
+   * @param clock clock instance for time operations (useful for testing)
+   */
+  FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "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 clock.instant().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();
+
+      // If we couldn't load a token and have no cached token, this is a fatal error
+      if (newToken == null && cachedToken == null) {
+        throw new RuntimeException(
+            "Unable to load bearer token from file: "
+                + tokenFilePath
+                + ". This is required for OPA authorization.");
+      }
+
+      // Only update cached token if we successfully loaded a new one
+      if (newToken != null) {
+        cachedToken = newToken;
+      }
+      // If newToken is null but cachedToken exists, we keep using the cached token
+
+      lastRefresh = clock.instant();
+
+      // Calculate next refresh time based on current token (may be cached)
+      nextRefresh = calculateNextRefresh(cachedToken);
+
+      logger.debug(
+          "Token refreshed from file: {} (token present: {}), next refresh: {}",
+          tokenFilePath,
+          cachedToken != null && !cachedToken.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 = getJwtExpirationTime(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 = clock.instant().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() {
+    int attempts = 0;
+    long deadlineMs =

Review Comment:
   Hm, this would block all requests.
   What I had in mind is to try only once (as before) and yield the previous token in case of an error.



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,307 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  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 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 FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer) {
+    this(
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer,
+        Clock.systemUTC());
+  }
+
+  /**
+   * Create a new file-based token provider with JWT expiration support and custom clock.
+   * Package-private constructor for testing purposes.
+   *
+   * @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
+   * @param clock clock instance for time operations (useful for testing)
+   */
+  FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "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;
+    }

Review Comment:
   ```suggestion
       checkState(!closed, "Token provider is closed");
   ```



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -0,0 +1,201 @@
+/*
+ * 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 com.google.common.base.Strings;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import java.net.URI;
+import java.nio.file.Path;
+import java.time.Duration;
+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 {
+
+  /** Authentication types supported by OPA authorization */
+  enum AuthenticationType {
+    NONE("none"),
+    BEARER("bearer");
+
+    private final String value;
+
+    AuthenticationType(String value) {
+      this.value = value;
+    }
+
+    public String getValue() {
+      return value;
+    }
+  }
+
+  /** Bearer token configuration types */
+  enum BearerTokenType {
+    STATIC_TOKEN("static-token"),
+    FILE_BASED("file-based");
+
+    private final String value;
+
+    BearerTokenType(String value) {
+      this.value = value;
+    }
+
+    public String getValue() {
+      return value;
+    }
+  }
+
+  Optional<URI> policyUri();
+
+  Optional<AuthenticationConfig> auth();
+
+  Optional<HttpConfig> http();
+
+  /** Validates the complete OPA configuration */
+  default void validate() {
+    checkArgument(policyUri().isPresent(), "OPA policy URI cannot be null");
+    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<Path> trustStorePath();
+
+    Optional<String> trustStorePassword();
+  }
+
+  /** Authentication configuration for OPA communication. */
+  interface AuthenticationConfig {
+    /** Type of authentication */
+    @WithDefault("none")
+    AuthenticationType 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")
+    BearerTokenType 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 */
+      String value();
+
+      default void validate() {
+        checkArgument(
+            !Strings.isNullOrEmpty(value()), "Static bearer token value cannot be null or empty");
+      }
+    }
+
+    /** Configuration for file-based bearer tokens */
+    interface FileBasedConfig {
+      /** Path to file containing bearer token */
+      Path path();
+
+      /** How often to refresh file-based bearer tokens (defaults to 5 minutes if not specified) */
+      Optional<Duration> 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. Defaults to
+       * true if not specified.
+       */
+      Optional<Boolean> jwtExpirationRefresh();
+
+      /**
+       * Buffer time before JWT expiration to refresh the token. Only used when jwtExpirationRefresh
+       * is true and the token is a valid JWT. Defaults to 1 minute if not specified.
+       */
+      Optional<Duration> jwtExpirationBuffer();
+
+      default void validate() {
+        checkArgument(path() != null, "Bearer token file path cannot be null");

Review Comment:
   `path` will always be present (it's mandatory now), so the check will always yield `true`.



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java:
##########
@@ -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 io.smallrye.common.annotation.Identifier;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.time.Duration;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+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.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@ApplicationScoped
+@Identifier("opa")
+class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private static final Logger logger = LoggerFactory.getLogger(OpaPolarisAuthorizerFactory.class);
+
+  private final OpaAuthorizationConfig opaConfig;
+  private CloseableHttpClient httpClient;
+  private BearerTokenProvider bearerTokenProvider;
+
+  @Inject
+  public OpaPolarisAuthorizerFactory(OpaAuthorizationConfig opaConfig) {
+    this.opaConfig = opaConfig;
+  }
+
+  @PostConstruct
+  public void initialize() {
+    // Validate configuration once during startup
+    opaConfig.validate();
+
+    // Create HTTP client once during startup
+    httpClient = createHttpClient();
+
+    // Setup authentication once during startup
+    setupAuthentication(opaConfig.auth().get());
+  }
+
+  @Override
+  public PolarisAuthorizer create(RealmConfig realmConfig) {
+    // All components are now pre-initialized, just create the authorizer
+    URI policyUri = opaConfig.policyUri().get();
+
+    return new OpaPolarisAuthorizer(policyUri, httpClient, bearerTokenProvider);
+  }
+
+  @PreDestroy
+  public void cleanup() {
+    // Clean up bearer token provider resources
+    if (bearerTokenProvider != null) {
+      try {
+        bearerTokenProvider.close();
+        logger.info("Bearer token provider closed successfully");
+      } catch (Exception e) {
+        // Log but don't throw - we're shutting down anyway
+        logger.warn("Error closing bearer token provider: {}", e.getMessage(), e);
+      }
+    }
+
+    // Clean up HTTP client resources
+    if (httpClient != null) {
+      try {
+        httpClient.close();
+        logger.info("HTTP client closed successfully");
+      } catch (IOException e) {
+        // Log but don't throw - we're shutting down anyway
+        logger.warn("Error closing HTTP client: {}", e.getMessage(), e);
+      }
+    }
+  }
+
+  private CloseableHttpClient createHttpClient() {
+    try {
+      if (opaConfig.http().isEmpty()) {
+        throw new IllegalStateException("HTTP configuration is required");
+      }
+      return OpaHttpClientFactory.createHttpClient(opaConfig.http().get());

Review Comment:
   Nit:
   ```suggestion
         if (opaConfig.http().isEmpty()) {
           throw new IllegalStateException("HTTP configuration is required");
         }
         return OpaHttpClientFactory.createHttpClient(opaConfig.http().orElseThrow(() -> new IllegalStateException("HTTP configuration is required")));
   ```
   (irrelevant if `http()` is not an `Optional, see above)



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java:
##########
@@ -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 io.smallrye.common.annotation.Identifier;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.time.Duration;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+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.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@ApplicationScoped
+@Identifier("opa")
+class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private static final Logger logger = LoggerFactory.getLogger(OpaPolarisAuthorizerFactory.class);
+
+  private final OpaAuthorizationConfig opaConfig;
+  private CloseableHttpClient httpClient;
+  private BearerTokenProvider bearerTokenProvider;
+
+  @Inject
+  public OpaPolarisAuthorizerFactory(OpaAuthorizationConfig opaConfig) {
+    this.opaConfig = opaConfig;
+  }
+
+  @PostConstruct
+  public void initialize() {
+    // Validate configuration once during startup
+    opaConfig.validate();
+
+    // Create HTTP client once during startup
+    httpClient = createHttpClient();
+
+    // Setup authentication once during startup
+    setupAuthentication(opaConfig.auth().get());
+  }
+
+  @Override
+  public PolarisAuthorizer create(RealmConfig realmConfig) {
+    // All components are now pre-initialized, just create the authorizer
+    URI policyUri = opaConfig.policyUri().get();
+
+    return new OpaPolarisAuthorizer(policyUri, httpClient, bearerTokenProvider);
+  }
+
+  @PreDestroy
+  public void cleanup() {
+    // Clean up bearer token provider resources
+    if (bearerTokenProvider != null) {
+      try {
+        bearerTokenProvider.close();
+        logger.info("Bearer token provider closed successfully");
+      } catch (Exception e) {
+        // Log but don't throw - we're shutting down anyway
+        logger.warn("Error closing bearer token provider: {}", e.getMessage(), e);
+      }
+    }
+
+    // Clean up HTTP client resources
+    if (httpClient != null) {
+      try {
+        httpClient.close();
+        logger.info("HTTP client closed successfully");

Review Comment:
   Nit
   ```suggestion
           logger.debug("HTTP client closed successfully");
   ```



##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -197,6 +197,40 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization type settings
+# Which authorizer to use: "internal" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+# polaris.authorization.type=internal
+
+# OPA Authorizer Configuration: effective only if polaris.authorization.type=opa
+# NOTE: The OPA Authorizer is currently in Beta and is not a stable release.
+# It may undergo breaking changes in future versions.
+# polaris.authorization.opa.url=http://localhost:8181
+# polaris.authorization.opa.policy-path=/v1/data/polaris/authz/allow
+
+# OPA HTTP configuration
+# polaris.authorization.opa.http.timeout-ms=5000
+# polaris.authorization.opa.http.verify-ssl=false

Review Comment:
   Mind mentioning the trust-store settings here as well and document that using verify-ssl=true breaks production readiness?



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java:
##########
@@ -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 io.smallrye.common.annotation.Identifier;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.time.Duration;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+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.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@ApplicationScoped
+@Identifier("opa")
+class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private static final Logger logger = LoggerFactory.getLogger(OpaPolarisAuthorizerFactory.class);
+
+  private final OpaAuthorizationConfig opaConfig;
+  private CloseableHttpClient httpClient;
+  private BearerTokenProvider bearerTokenProvider;
+
+  @Inject
+  public OpaPolarisAuthorizerFactory(OpaAuthorizationConfig opaConfig) {

Review Comment:
   Nit:
   ```suggestion
     public OpaPolarisAuthorizerFactory(OpaAuthorizationConfig opaConfig, Clock clock) {
   ```
   and pass that clock to `FileBearerTokenProvider`. (`Clock`'s produced via `ServiceProducers#clock`)



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java:
##########
@@ -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 io.smallrye.common.annotation.Identifier;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.time.Duration;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+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.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@ApplicationScoped
+@Identifier("opa")
+class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private static final Logger logger = LoggerFactory.getLogger(OpaPolarisAuthorizerFactory.class);
+
+  private final OpaAuthorizationConfig opaConfig;
+  private CloseableHttpClient httpClient;
+  private BearerTokenProvider bearerTokenProvider;
+
+  @Inject
+  public OpaPolarisAuthorizerFactory(OpaAuthorizationConfig opaConfig) {
+    this.opaConfig = opaConfig;
+  }
+
+  @PostConstruct
+  public void initialize() {
+    // Validate configuration once during startup
+    opaConfig.validate();
+
+    // Create HTTP client once during startup
+    httpClient = createHttpClient();
+
+    // Setup authentication once during startup
+    setupAuthentication(opaConfig.auth().get());
+  }
+
+  @Override
+  public PolarisAuthorizer create(RealmConfig realmConfig) {
+    // All components are now pre-initialized, just create the authorizer
+    URI policyUri = opaConfig.policyUri().get();
+
+    return new OpaPolarisAuthorizer(policyUri, httpClient, bearerTokenProvider);
+  }
+
+  @PreDestroy
+  public void cleanup() {
+    // Clean up bearer token provider resources
+    if (bearerTokenProvider != null) {
+      try {
+        bearerTokenProvider.close();
+        logger.info("Bearer token provider closed successfully");

Review Comment:
   Nit
   
   ```suggestion
           logger.debug("Bearer token provider closed successfully");
   ```



##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,8 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa/impl
+polaris-extensions-auth-opa-tests=extensions/auth/opa/tests

Review Comment:
   Hm, I'd prefer to leave the integration tests in the runtime module.
   Having this module adds another expensive and (somewhat) long-running Quarkus build to CI duration.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453384952


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,176 @@
+/*
+ * 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.fasterxml.jackson.databind.ObjectMapper;
+import io.smallrye.common.annotation.Identifier;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.time.Clock;
+import java.time.Duration;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+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.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@ApplicationScoped
+@Identifier("opa")
+class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private static final Logger logger = LoggerFactory.getLogger(OpaPolarisAuthorizerFactory.class);
+
+  private final OpaAuthorizationConfig opaConfig;
+  private final Clock clock;
+  private CloseableHttpClient httpClient;
+  private BearerTokenProvider bearerTokenProvider;
+  private ObjectMapper objectMapper;
+
+  @Inject
+  public OpaPolarisAuthorizerFactory(OpaAuthorizationConfig opaConfig, Clock clock) {
+    this.opaConfig = opaConfig;
+    this.clock = clock;
+    this.objectMapper = new ObjectMapper();
+  }
+
+  /**
+   * Gets the OPA authorization configuration. Used by OpaProductionReadinessCheck
+   *
+   * @return the OPA configuration
+   */
+  OpaAuthorizationConfig getConfig() {
+    return opaConfig;
+  }
+
+  @PostConstruct
+  public void initialize() {
+    // Validate configuration once during startup
+    opaConfig.validate();
+
+    // Create HTTP client once during startup
+    httpClient = createHttpClient();
+
+    // Setup authentication once during startup
+    setupAuthentication(opaConfig.auth());

Review Comment:
   I intentionally left this function in this format, because we may be setting up mTLS authentication in the future, in which case we won't be setting a bearerTokenProvider.
   
   I thought wrapping the setup of all authentication modes within a single function makes sense



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453381959


##########
extensions/auth/opa/build.gradle.kts:
##########


Review Comment:
   Thank you! This is from the previous iteration of project structure. I'll remove this



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453556374


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,222 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.google.common.base.Strings;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  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 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
+   * @param clock clock instance for time operations
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer);
+  }
+
+  @Override
+  public String getToken() {
+    checkState(!closed, "Token provider is closed");
+
+    // Check if we need to refresh
+    if (shouldRefresh()) {
+      refreshToken();
+    }
+
+    // If we couldn't load a token and have no cached token, this is a fatal error
+    if (Strings.isNullOrEmpty(cachedToken)) {
+      throw new RuntimeException(
+          "Unable to load bearer token from file: "
+              + tokenFilePath
+              + ". This is required for OPA authorization.");
+    }
+    return cachedToken;
+  }
+
+  @Override
+  public void close() {
+    closed = true;
+    cachedToken = null;
+  }
+
+  private boolean shouldRefresh() {
+    return clock.instant().isAfter(nextRefresh);
+  }
+
+  private void refreshToken() {
+    if (!refreshLock.compareAndSet(false, true)) {
+      return;

Review Comment:
   I agree. I like @snazy 's suggestion to use AtomicBoolean to simplify the code, so instead of replacing this with a lock, I added eager token initialization in the constructor to address this specific edge case. Let me know if you still share this concern



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3459050260

   Woohoo! 
   
   Thank you @dimas-b ! And @snazy @flyrain and @adutra for the reviews as well.
   
   This was a great way for me to get up to speed with the project, so thanks for your interest and patience in reviewing this PR as well.
   
   I'll be following this up with some more PRs (docs, schema, etc) and we'll start integrating this into our platform.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "adutra (via GitHub)" <gi...@apache.org>.
adutra commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2380127562


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      int timeoutMs,
+      OkHttpClient client,
+      ObjectMapper mapper) {
+    OkHttpClient clientWithTimeout =
+        (client != null)
+            ? client.newBuilder().callTimeout(java.time.Duration.ofMillis(timeoutMs)).build()
+            : new OkHttpClient.Builder()
+                .callTimeout(java.time.Duration.ofMillis(timeoutMs))
+                .build();
+    ObjectMapper objectMapper = (mapper != null) ? mapper : new ObjectMapper();
+    return new OpaPolarisAuthorizer(opaServerUrl, opaPolicyPath, clientWithTimeout, objectMapper);
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+      RequestBody body = RequestBody.create(inputJson, MediaType.parse("application/json"));
+      Request request = new Request.Builder().url(opaServerUrl + opaPolicyPath).post(body).build();
+      try (Response response = httpClient.newCall(request).execute()) {
+        if (!response.isSuccessful()) return false;
+        // Parse response JSON for 'result.allow'
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(response.body().string());
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();

Review Comment:
   > There's role in `PolarisPrincipal`, as well as the `activatedEntities` and I couldn't figure out easily which one I should pass over into OPA.
   
   I think I can provide some explanations. 
   
   The roles in `PolarisPrincipal` are principal roles resolved by the `Authenticator`. Today, any principal role in the access token will appear there, but it will be checked against the database. Therefore principal roles must have been created beforehand, using the Polaris management API, if you want to make use of them. Even if you are using an external IDP, you still need to create the principal roles beforehand, and that is one of the pain points today.
   
   Furthermore, the access token controls which principal roles should be "activated". "Activate a role" means that the role is considered for authorization decisions for the current request; if the role is not active (not present in the token), it won't be considered. This allows access tokens to contain a subset of the roles the principal was granted. 
   
   As for `activatedEntities`, it is a bit of a cryptic name, I confess. This parameter contains a mix of principal roles and catalog roles that the authenticated principal has been granted. This parameter is populated by the `Resolver`. The principal roles are the same ones that you get from the `PolarisPrincipal` (but the `Resolver` re-fetches them from the database unfortunately). The catalog roles, if any, are populated by the `Resolver` as well.
   
   Finally, the `Resolver` also validates all involved "paths", that is, references to resources such as tables and views that are being affected by the request.
   
   Then, once the `Resolver` has finished resolving roles and validating "paths", the `PolarisAuthorizer` is invoked with these fundamental elements:
   
   1. The principal
   2. The principal roles and catalog roles (in `activatedEntities`)
   3. The resources or "paths" affected by the operation
   4. The operation to authorize
   
   > I couldn't figure out easily which one I should pass over into OPA.
   
   I would say that:
   
   1. For principal roles, you probably need to send those to OPA, as they indicate group membership – unless OPA can figure this out by simply searching the principal name.
   2. For catalog roles however, it probably doesn't make sense to manage them in Polaris since you are already managing access control externally. I would expect OPA users to never create any catalog role at all. So it doesn't make sense to include those either in the request to OPA.
   
   Sorry for the wall of text 😅 
   
   
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2380314035


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      int timeoutMs,
+      OkHttpClient client,
+      ObjectMapper mapper) {
+    OkHttpClient clientWithTimeout =
+        (client != null)
+            ? client.newBuilder().callTimeout(java.time.Duration.ofMillis(timeoutMs)).build()
+            : new OkHttpClient.Builder()
+                .callTimeout(java.time.Duration.ofMillis(timeoutMs))
+                .build();
+    ObjectMapper objectMapper = (mapper != null) ? mapper : new ObjectMapper();
+    return new OpaPolarisAuthorizer(opaServerUrl, opaPolicyPath, clientWithTimeout, objectMapper);
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+      RequestBody body = RequestBody.create(inputJson, MediaType.parse("application/json"));
+      Request request = new Request.Builder().url(opaServerUrl + opaPolicyPath).post(body).build();
+      try (Response response = httpClient.newCall(request).execute()) {
+        if (!response.isSuccessful()) return false;
+        // Parse response JSON for 'result.allow'
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(response.body().string());
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();

Review Comment:
   Thank you so much for the detailed explanation! I'll keep this part of the implementation as-is.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2380307067


##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -193,6 +193,15 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization implementation settings
+# Which authorizer to use: "default" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+polaris.authorization.implementation=default
+
+# OPA Authorizer Configuration: effective only if polaris.authorization.implementation=opa
+# polaris.authorization.opa.url=http://localhost:8181

Review Comment:
   I can add support for HTTPS. I think we can keep the support HTTP in case the OPA server is being run as a side car



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2379578865


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      int timeoutMs,
+      OkHttpClient client,
+      ObjectMapper mapper) {
+    OkHttpClient clientWithTimeout =
+        (client != null)
+            ? client.newBuilder().callTimeout(java.time.Duration.ofMillis(timeoutMs)).build()
+            : new OkHttpClient.Builder()
+                .callTimeout(java.time.Duration.ofMillis(timeoutMs))
+                .build();
+    ObjectMapper objectMapper = (mapper != null) ? mapper : new ObjectMapper();
+    return new OpaPolarisAuthorizer(opaServerUrl, opaPolicyPath, clientWithTimeout, objectMapper);
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+      RequestBody body = RequestBody.create(inputJson, MediaType.parse("application/json"));
+      Request request = new Request.Builder().url(opaServerUrl + opaPolicyPath).post(body).build();
+      try (Response response = httpClient.newCall(request).execute()) {
+        if (!response.isSuccessful()) return false;
+        // Parse response JSON for 'result.allow'
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(response.body().string());
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();

Review Comment:
   I love that you pointed this out :)
   
   I already made note of this as an open question in the RFC I'm drafting up. There's role in `PolarisPrincipal`, as well as the `activatedEntities` and I couldn't figure out easily which one I should pass over into OPA



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2379806658


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      int timeoutMs,
+      OkHttpClient client,
+      ObjectMapper mapper) {
+    OkHttpClient clientWithTimeout =
+        (client != null)
+            ? client.newBuilder().callTimeout(java.time.Duration.ofMillis(timeoutMs)).build()
+            : new OkHttpClient.Builder()
+                .callTimeout(java.time.Duration.ofMillis(timeoutMs))
+                .build();
+    ObjectMapper objectMapper = (mapper != null) ? mapper : new ObjectMapper();
+    return new OpaPolarisAuthorizer(opaServerUrl, opaPolicyPath, clientWithTimeout, objectMapper);
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+      RequestBody body = RequestBody.create(inputJson, MediaType.parse("application/json"));
+      Request request = new Request.Builder().url(opaServerUrl + opaPolicyPath).post(body).build();
+      try (Response response = httpClient.newCall(request).execute()) {
+        if (!response.isSuccessful()) return false;
+        // Parse response JSON for 'result.allow'
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(response.body().string());
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();

Review Comment:
   `PolarisPrincipal` is constructed during request authentication and those mechanisms are pluggable. The principal can conceivably contain information that is provided by the IdP, but is not reflected in local Polaris roles.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422220845


##########
runtime/service/src/intTest/java/org/apache/polaris/service/auth/opa/OpaFileTokenIntegrationTest.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * 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.service.auth.opa;
+
+import static io.restassured.RestAssured.given;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import io.quarkus.test.junit.QuarkusTestProfile.TestResourceEntry;
+import io.quarkus.test.junit.TestProfile;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.polaris.test.commons.OpaTestResource;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+@TestProfile(OpaFileTokenIntegrationTest.FileTokenOpaProfile.class)
+public class OpaFileTokenIntegrationTest {

Review Comment:
   Would it be possible to keep this test under `extensions/auth/opa/...` somewhere? In light of making OPA a proper plugin, it would be nice to avoid hard dependencies from the main service module.



##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -141,8 +141,13 @@ public RealmConfig realmConfig(CallContext callContext) {
 
   @Produces
   @RequestScoped
-  public PolarisAuthorizer polarisAuthorizer(RealmConfig realmConfig) {
-    return new PolarisAuthorizerImpl(realmConfig);
+  public PolarisAuthorizer polarisAuthorizer(
+      AuthorizationConfiguration authorizationConfig,
+      RealmConfig realmConfig,
+      @Any Instance<PolarisAuthorizerFactory> authorizerFactories) {
+    PolarisAuthorizerFactory factory =
+        authorizerFactories.select(Identifier.Literal.of(authorizationConfig.type())).get();

Review Comment:
   Could you move the factory into a separate `@ApplicationScoped` producer to avoid `.select()` calls on every request?



##########
extensions/auth/opa/build.gradle.kts:
##########
@@ -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(project(":polaris-runtime-service"))

Review Comment:
   why does OPA need `polaris-runtime-service`?



##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,7 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa

Review Comment:
   nit: I'd personally be fine with `polaris-auth-opa` (for brevity). The module name does not have to repeat all sub-dir names of its location. I think the `polaris` prefix helps in case modules are imported into another project, but otherwise shorter name are easier to use, IMHO.



##########
polaris-core/build.gradle.kts:
##########
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(project(":polaris-api-management-model"))
+  implementation(libs.apache.httpclient5)

Review Comment:
   Do we still need this dep. in core?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "jbonofre (via GitHub)" <gi...@apache.org>.
jbonofre commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2423029215


##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaHttpClientFactory.java:
##########
@@ -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;

Review Comment:
   Thanks, it's totally fine. I was just wondering 😄 Generally speaking, when we can avoid extra deps, that's better. As Iceberg "provides" Apache HTTPc dep, that's OK.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "jbonofre (via GitHub)" <gi...@apache.org>.
jbonofre commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2423029586


##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaHttpClientFactory.java:
##########
@@ -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;

Review Comment:
   Let's keep Apache HTTPc for now.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422909606


##########
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java:
##########
@@ -141,8 +141,13 @@ public RealmConfig realmConfig(CallContext callContext) {
 
   @Produces
   @RequestScoped
-  public PolarisAuthorizer polarisAuthorizer(RealmConfig realmConfig) {
-    return new PolarisAuthorizerImpl(realmConfig);
+  public PolarisAuthorizer polarisAuthorizer(
+      AuthorizationConfiguration authorizationConfig,
+      RealmConfig realmConfig,
+      @Any Instance<PolarisAuthorizerFactory> authorizerFactories) {
+    PolarisAuthorizerFactory factory =
+        authorizerFactories.select(Identifier.Literal.of(authorizationConfig.type())).get();

Review Comment:
   Good point @dimas-b - I took care of this, and also made some adjustments into OpaPolarisAuthorizorFactory to validate the configurations and initialize the HttpClient at the application scope.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2416840211


##########
runtime/service/src/main/java/org/apache/polaris/service/auth/OpaPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,97 @@
+/*
+ * 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.service.auth;
+
+import io.smallrye.common.annotation.Identifier;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import java.time.Duration;
+import org.apache.polaris.core.auth.FileTokenProvider;
+import org.apache.polaris.core.auth.OpaPolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
+import org.apache.polaris.core.auth.StaticTokenProvider;
+import org.apache.polaris.core.auth.TokenProvider;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.service.config.AuthorizationConfiguration;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@RequestScoped
+@Identifier("opa")
+public class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private final AuthorizationConfiguration authorizationConfig;
+
+  @Inject
+  public OpaPolarisAuthorizerFactory(AuthorizationConfiguration authorizationConfig) {
+    this.authorizationConfig = authorizationConfig;
+  }
+
+  @Override
+  public PolarisAuthorizer create(RealmConfig realmConfig) {
+    AuthorizationConfiguration.OpaConfig opa = authorizationConfig.opa();
+
+    // Create appropriate token provider based on configuration
+    TokenProvider tokenProvider = createTokenProvider(opa);
+
+    return OpaPolarisAuthorizer.create(
+        opa.url().orElse(null),
+        opa.policyPath().orElse(null),
+        tokenProvider,
+        opa.timeoutMs().orElse(2000), // Default to 2000ms if not specified
+        opa.verifySsl(), // Default is true from @WithDefault annotation
+        opa.trustStorePath().orElse(null),
+        opa.trustStorePassword().orElse(null),
+        null,
+        null);
+  }
+
+  /**
+   * Creates a token provider based on the OPA configuration.
+   *
+   * <p>Prioritizes static token over file-based token:
+   *
+   * <ol>
+   *   <li>If bearerToken.staticValue is set, uses StaticTokenProvider
+   *   <li>If bearerToken.filePath is set, uses FileTokenProvider
+   *   <li>Otherwise, returns StaticTokenProvider with null token
+   * </ol>
+   */
+  private TokenProvider createTokenProvider(AuthorizationConfiguration.OpaConfig opa) {
+    AuthorizationConfiguration.BearerTokenConfig bearerToken = opa.bearerToken();
+
+    // Static token takes precedence
+    if (bearerToken.staticValue().isPresent()) {
+      return new StaticTokenProvider(bearerToken.staticValue().get());
+    }
+
+    // File-based token as fallback
+    if (bearerToken.filePath().isPresent()) {
+      Duration refreshInterval = Duration.ofSeconds(bearerToken.refreshInterval());
+      boolean jwtExpirationRefresh = bearerToken.jwtExpirationRefresh();
+      Duration jwtExpirationBuffer = Duration.ofSeconds(bearerToken.jwtExpirationBuffer());
+
+      return new FileTokenProvider(

Review Comment:
   Good catch @flyrain 
   
   I've updated the BearerTokenProvider to be injected as a Singleton for now, along with the `opa-http-client`. Would it make sense to add support for the per-realm configurations in as followup PR?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2418000190


##########
polaris-core/build.gradle.kts:
##########
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(project(":polaris-api-management-model"))
+  implementation(libs.apache.httpclient5)

Review Comment:
   I think that makes a lot of sense as well. I was curious to hear opinions on the new dependencies and OPA plugin being introduced as a part of `polaris-core`.
   
   Let me take a stab at separating out these changes as a separate build in `extensions/auth/opa`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2379578865


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      int timeoutMs,
+      OkHttpClient client,
+      ObjectMapper mapper) {
+    OkHttpClient clientWithTimeout =
+        (client != null)
+            ? client.newBuilder().callTimeout(java.time.Duration.ofMillis(timeoutMs)).build()
+            : new OkHttpClient.Builder()
+                .callTimeout(java.time.Duration.ofMillis(timeoutMs))
+                .build();
+    ObjectMapper objectMapper = (mapper != null) ? mapper : new ObjectMapper();
+    return new OpaPolarisAuthorizer(opaServerUrl, opaPolicyPath, clientWithTimeout, objectMapper);
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+      RequestBody body = RequestBody.create(inputJson, MediaType.parse("application/json"));
+      Request request = new Request.Builder().url(opaServerUrl + opaPolicyPath).post(body).build();
+      try (Response response = httpClient.newCall(request).execute()) {
+        if (!response.isSuccessful()) return false;
+        // Parse response JSON for 'result.allow'
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(response.body().string());
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();

Review Comment:
   I love that you pointed this out :)
   
   I already made note of this as an open question in the RFC I'm drafting up. There's role in `PolarisPrincipal`, as well as the `activatedEntities` and I couldn't figure out easily which one I should pass over into OPA.
   
   And yes, I started looking into your issue on federated principals as well. And I agree that we need both the OPA implementation and federated principal to integrate with an external IDP and external policy store without much friction.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2382438257


##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -193,6 +193,15 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization implementation settings
+# Which authorizer to use: "default" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+polaris.authorization.implementation=default
+
+# OPA Authorizer Configuration: effective only if polaris.authorization.implementation=opa
+# polaris.authorization.opa.url=http://localhost:8181

Review Comment:
   thanks for the pointer!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2395876067


##########
runtime/service/build.gradle.kts:
##########
@@ -25,6 +25,7 @@ plugins {
 }
 
 dependencies {
+  implementation("com.squareup.okhttp3:okhttp:4.12.0")

Review Comment:
   noted - I've made use of apache http instead to remove unneeded dependencies



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422259640


##########
polaris-core/build.gradle.kts:
##########
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(project(":polaris-api-management-model"))
+  implementation(libs.apache.httpclient5)

Review Comment:
   The pluggability aspect is not about "heavy" or "light" depenpencies per se, but about giving downstream projects control over what to enable/bundle or not. As this appears to be a point where a bit of misunderstanding developed, let's move the discussion about this particular aspect to the `dev` ML for visibility and reaching consensus within the broader community (not everyone watches PRs).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422909890


##########
extensions/auth/opa/build.gradle.kts:
##########
@@ -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(project(":polaris-runtime-service"))

Review Comment:
   Updated the dependencies 👍 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "jbonofre (via GitHub)" <gi...@apache.org>.
jbonofre commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2422934586


##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaHttpClientFactory.java:
##########
@@ -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;

Review Comment:
   Just curious: do you prefer to use Apache HTTPc instead of the HTTP client provided by the JDK ?



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility
+      ObjectMapper mapper) {
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(timeoutMs)
+              .setSocketTimeout(timeoutMs)
+              .setConnectionRequestTimeout(timeoutMs)
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);
+
+      // Create HTTP client with SSL configuration
+      CloseableHttpClient httpClient;
+      if (client instanceof CloseableHttpClient) {
+        httpClient = (CloseableHttpClient) client;
+      } else {
+        if (sslSocketFactory != null) {
+          httpClient =
+              HttpClients.custom()
+                  .setDefaultRequestConfig(requestConfig)
+                  .setSSLSocketFactory(sslSocketFactory)
+                  .build();
+        } else {
+          httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
+        }
+      }
+
+      ObjectMapper objectMapperWithDefaults = mapper != null ? mapper : new ObjectMapper();
+      return new OpaPolarisAuthorizer(
+          opaServerUrl, opaPolicyPath, tokenProvider, httpClient, objectMapperWithDefaults);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to create OpaPolarisAuthorizer with SSL configuration", e);
+    }
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+
+      // Create HTTP POST request using Apache HttpComponents
+      HttpPost httpPost = new HttpPost(opaServerUrl + opaPolicyPath);
+      httpPost.setHeader("Content-Type", "application/json");
+
+      // Add bearer token authentication if provided
+      if (tokenProvider != null) {
+        String token = tokenProvider.getToken();
+        if (token != null && !token.isEmpty()) {
+          httpPost.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
+        }
+      }
+
+      httpPost.setEntity(new StringEntity(inputJson, StandardCharsets.UTF_8));
+
+      // Execute request
+      try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+        int statusCode = response.getStatusLine().getStatusCode();
+        if (statusCode != 200) {
+          return false;
+        }
+
+        // Read and parse response
+        String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(responseBody);
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();
+    input.set("actor", buildActorNode(principal));
+    input.put("action", op.name());
+    input.set("resource", buildResourceNode(targets, secondaries));
+    input.set("context", buildContextNode());
+    ObjectNode root = objectMapper.createObjectNode();
+    root.set("input", input);

Review Comment:
   Yes, I agree that it would be "simpler/cleaner" to create beans (eventually usign Jackson annotations) to generate the JSON input, generating the associate schema and validating it. It's pretty straight forward as we already "bundle" Jackson.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2412297582


##########
runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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.service.auth;
+
+import io.smallrye.common.annotation.Identifier;
+import jakarta.enterprise.context.RequestScoped;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
+import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
+import org.apache.polaris.core.config.RealmConfig;
+
+/** Factory for creating the default Polaris authorizer implementation. */
+@RequestScoped

Review Comment:
   Same here



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility
+      ObjectMapper mapper) {
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(timeoutMs)
+              .setSocketTimeout(timeoutMs)
+              .setConnectionRequestTimeout(timeoutMs)
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);
+
+      // Create HTTP client with SSL configuration
+      CloseableHttpClient httpClient;
+      if (client instanceof CloseableHttpClient) {
+        httpClient = (CloseableHttpClient) client;
+      } else {
+        if (sslSocketFactory != null) {
+          httpClient =
+              HttpClients.custom()
+                  .setDefaultRequestConfig(requestConfig)
+                  .setSSLSocketFactory(sslSocketFactory)
+                  .build();
+        } else {
+          httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
+        }
+      }
+
+      ObjectMapper objectMapperWithDefaults = mapper != null ? mapper : new ObjectMapper();
+      return new OpaPolarisAuthorizer(
+          opaServerUrl, opaPolicyPath, tokenProvider, httpClient, objectMapperWithDefaults);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to create OpaPolarisAuthorizer with SSL configuration", e);
+    }
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+
+      // Create HTTP POST request using Apache HttpComponents
+      HttpPost httpPost = new HttpPost(opaServerUrl + opaPolicyPath);

Review Comment:
   I assume the OPA query REST spec are standardized in terms of path, request payload, header, response and http codes. Can you confirm? Would it be possible to add Java doc about which OPA spec we are using here?
   
   How does a OPA Service support multiple Polaris instance? Configuring different path?



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/FileTokenProvider.java:
##########
@@ -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()) {

Review Comment:
   Do we need this if clause? I think `logger.debug()` will check that by default.



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility
+      ObjectMapper mapper) {
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(timeoutMs)
+              .setSocketTimeout(timeoutMs)
+              .setConnectionRequestTimeout(timeoutMs)
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);
+
+      // Create HTTP client with SSL configuration
+      CloseableHttpClient httpClient;
+      if (client instanceof CloseableHttpClient) {
+        httpClient = (CloseableHttpClient) client;
+      } else {
+        if (sslSocketFactory != null) {
+          httpClient =
+              HttpClients.custom()
+                  .setDefaultRequestConfig(requestConfig)
+                  .setSSLSocketFactory(sslSocketFactory)

Review Comment:
   I guess we will need a connection manager (`HttpClientConnectionManager`) soon, as each operation triggers a opa http call. The connection manager could optimize the http client connections.  BTW, is it a crazy idea to use the `org.apache.iceberg.rest.HTTPClient` directly? It is well tested with a lot of things built in including connection manager, proxy, etc. The only concern is that it has a bit of extra Iceberg stuff. 



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/JwtDecoder.java:
##########
@@ -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 {

Review Comment:
   Could we use an existing lib, like we did in the class `JWTBroker`? Does something like `JWT.decode()` in the lib `java-jwt` work here?



##########
runtime/service/src/main/java/org/apache/polaris/service/auth/OpaPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,97 @@
+/*
+ * 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.service.auth;
+
+import io.smallrye.common.annotation.Identifier;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import java.time.Duration;
+import org.apache.polaris.core.auth.FileTokenProvider;
+import org.apache.polaris.core.auth.OpaPolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
+import org.apache.polaris.core.auth.StaticTokenProvider;
+import org.apache.polaris.core.auth.TokenProvider;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.service.config.AuthorizationConfiguration;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@RequestScoped

Review Comment:
   Would `applicationScope` fit better here? We don't need to create a new factory every time a http request comes in.



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/TokenProvider.java:
##########
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+/**
+ * Interface for providing bearer tokens for authentication.
+ *
+ * <p>Implementations can provide tokens from various sources such as:
+ *
+ * <ul>
+ *   <li>Static string values
+ *   <li>Files (with automatic reloading)
+ *   <li>External token services
+ * </ul>
+ */
+public interface TokenProvider {

Review Comment:
   Nit: Shall we call it `BearerTokenProvider` to be more descriptive, if it is only for bearer token? 



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility
+      ObjectMapper mapper) {
+

Review Comment:
   We may need to do a null check for url and path here. `createSslSocketFactory()` did check the server url, but it doesn't throw, in that case line 129 still creates a http client. I'm not sure how far line 129 can go when url or path is null. It'd be good to fail early in that case. 



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility
+      ObjectMapper mapper) {
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(timeoutMs)
+              .setSocketTimeout(timeoutMs)
+              .setConnectionRequestTimeout(timeoutMs)
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);
+
+      // Create HTTP client with SSL configuration
+      CloseableHttpClient httpClient;
+      if (client instanceof CloseableHttpClient) {
+        httpClient = (CloseableHttpClient) client;
+      } else {
+        if (sslSocketFactory != null) {
+          httpClient =
+              HttpClients.custom()
+                  .setDefaultRequestConfig(requestConfig)
+                  .setSSLSocketFactory(sslSocketFactory)
+                  .build();
+        } else {
+          httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
+        }
+      }
+
+      ObjectMapper objectMapperWithDefaults = mapper != null ? mapper : new ObjectMapper();
+      return new OpaPolarisAuthorizer(
+          opaServerUrl, opaPolicyPath, tokenProvider, httpClient, objectMapperWithDefaults);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to create OpaPolarisAuthorizer with SSL configuration", e);
+    }
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+
+      // Create HTTP POST request using Apache HttpComponents
+      HttpPost httpPost = new HttpPost(opaServerUrl + opaPolicyPath);
+      httpPost.setHeader("Content-Type", "application/json");
+
+      // Add bearer token authentication if provided
+      if (tokenProvider != null) {
+        String token = tokenProvider.getToken();
+        if (token != null && !token.isEmpty()) {
+          httpPost.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
+        }
+      }
+
+      httpPost.setEntity(new StringEntity(inputJson, StandardCharsets.UTF_8));
+
+      // Execute request
+      try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+        int statusCode = response.getStatusLine().getStatusCode();
+        if (statusCode != 200) {
+          return false;
+        }
+
+        // Read and parse response
+        String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(responseBody);
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();
+    input.set("actor", buildActorNode(principal));
+    input.put("action", op.name());
+    input.set("resource", buildResourceNode(targets, secondaries));
+    input.set("context", buildContextNode());
+    ObjectNode root = objectMapper.createObjectNode();
+    root.set("input", input);

Review Comment:
   Not a blocker: Not a fan of manually building the request payload. One option is to create Java classes(example, `DataCompactionPolicyContent`), another option is autogen based on the REST spec. Is there a REST spec we can at least refer to?



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability

Review Comment:
   Do we need this comment?



##########
runtime/service/src/main/java/org/apache/polaris/service/auth/OpaPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,97 @@
+/*
+ * 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.service.auth;
+
+import io.smallrye.common.annotation.Identifier;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import java.time.Duration;
+import org.apache.polaris.core.auth.FileTokenProvider;
+import org.apache.polaris.core.auth.OpaPolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
+import org.apache.polaris.core.auth.StaticTokenProvider;
+import org.apache.polaris.core.auth.TokenProvider;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.service.config.AuthorizationConfiguration;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@RequestScoped
+@Identifier("opa")
+public class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private final AuthorizationConfiguration authorizationConfig;
+
+  @Inject
+  public OpaPolarisAuthorizerFactory(AuthorizationConfiguration authorizationConfig) {
+    this.authorizationConfig = authorizationConfig;
+  }
+
+  @Override
+  public PolarisAuthorizer create(RealmConfig realmConfig) {
+    AuthorizationConfiguration.OpaConfig opa = authorizationConfig.opa();
+
+    // Create appropriate token provider based on configuration
+    TokenProvider tokenProvider = createTokenProvider(opa);
+
+    return OpaPolarisAuthorizer.create(
+        opa.url().orElse(null),
+        opa.policyPath().orElse(null),
+        tokenProvider,
+        opa.timeoutMs().orElse(2000), // Default to 2000ms if not specified
+        opa.verifySsl(), // Default is true from @WithDefault annotation
+        opa.trustStorePath().orElse(null),
+        opa.trustStorePassword().orElse(null),
+        null,
+        null);
+  }
+
+  /**
+   * Creates a token provider based on the OPA configuration.
+   *
+   * <p>Prioritizes static token over file-based token:
+   *
+   * <ol>
+   *   <li>If bearerToken.staticValue is set, uses StaticTokenProvider
+   *   <li>If bearerToken.filePath is set, uses FileTokenProvider
+   *   <li>Otherwise, returns StaticTokenProvider with null token
+   * </ol>
+   */
+  private TokenProvider createTokenProvider(AuthorizationConfiguration.OpaConfig opa) {
+    AuthorizationConfiguration.BearerTokenConfig bearerToken = opa.bearerToken();
+
+    // Static token takes precedence
+    if (bearerToken.staticValue().isPresent()) {
+      return new StaticTokenProvider(bearerToken.staticValue().get());
+    }
+
+    // File-based token as fallback
+    if (bearerToken.filePath().isPresent()) {
+      Duration refreshInterval = Duration.ofSeconds(bearerToken.refreshInterval());
+      boolean jwtExpirationRefresh = bearerToken.jwtExpirationRefresh();
+      Duration jwtExpirationBuffer = Duration.ofSeconds(bearerToken.jwtExpirationBuffer());
+
+      return new FileTokenProvider(

Review Comment:
   Looks like we will create `FileTokenProvider` for every http request. It may not be necessary. Image how much I/O we need as each `FileTokenProvider` refresh the token by itself. Wondering if we could make it per-realm object, given that we don't do per-request token now. 



##########
runtime/service/src/main/java/org/apache/polaris/service/config/AuthorizationConfiguration.java:
##########
@@ -0,0 +1,75 @@
+/*
+ * 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.service.config;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import java.util.Optional;
+
+@ConfigMapping(prefix = "polaris.authorization")
+public interface AuthorizationConfiguration {
+  @WithDefault("default")
+  String type();
+
+  OpaConfig opa();
+
+  interface OpaConfig {
+    Optional<String> url();
+
+    Optional<String> policyPath();
+
+    Optional<Integer> timeoutMs();
+
+    BearerTokenConfig bearerToken();
+
+    @WithDefault("true")
+    boolean verifySsl();
+
+    Optional<String> trustStorePath();
+
+    Optional<String> trustStorePassword();
+  }
+
+  interface BearerTokenConfig {

Review Comment:
   Nit: Adding a default method like this here, so that caller can validate it?
   ```
     default void validate() {
       if (refreshInterval() <= 0) {
         throw new IllegalArgumentException("refreshInterval must be greater than 0");
       }
       if (jwtExpirationBuffer() <= 0) {
         throw new IllegalArgumentException("jwtExpirationBuffer must be greater than 0");
       }
     }
   ```



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility

Review Comment:
   The a parameter `client` isn't used now, how do you envision it is used in the future?



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility

Review Comment:
   Same question for the `mapper`?



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      int timeoutMs,
+      OkHttpClient client,
+      ObjectMapper mapper) {
+    OkHttpClient clientWithTimeout =
+        (client != null)
+            ? client.newBuilder().callTimeout(java.time.Duration.ofMillis(timeoutMs)).build()
+            : new OkHttpClient.Builder()
+                .callTimeout(java.time.Duration.ofMillis(timeoutMs))
+                .build();
+    ObjectMapper objectMapper = (mapper != null) ? mapper : new ObjectMapper();
+    return new OpaPolarisAuthorizer(opaServerUrl, opaPolicyPath, clientWithTimeout, objectMapper);
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+      RequestBody body = RequestBody.create(inputJson, MediaType.parse("application/json"));
+      Request request = new Request.Builder().url(opaServerUrl + opaPolicyPath).post(body).build();
+      try (Response response = httpClient.newCall(request).execute()) {
+        if (!response.isSuccessful()) return false;
+        // Parse response JSON for 'result.allow'
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(response.body().string());
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();

Review Comment:
   Can we add a Java doc here or somewhere else to clarify that Polaris principal roles and catalog roles are bypassed? Since this literally does that.  



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility
+      ObjectMapper mapper) {
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(timeoutMs)
+              .setSocketTimeout(timeoutMs)
+              .setConnectionRequestTimeout(timeoutMs)
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);
+
+      // Create HTTP client with SSL configuration
+      CloseableHttpClient httpClient;
+      if (client instanceof CloseableHttpClient) {
+        httpClient = (CloseableHttpClient) client;
+      } else {
+        if (sslSocketFactory != null) {
+          httpClient =
+              HttpClients.custom()
+                  .setDefaultRequestConfig(requestConfig)
+                  .setSSLSocketFactory(sslSocketFactory)
+                  .build();
+        } else {
+          httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
+        }
+      }
+
+      ObjectMapper objectMapperWithDefaults = mapper != null ? mapper : new ObjectMapper();
+      return new OpaPolarisAuthorizer(
+          opaServerUrl, opaPolicyPath, tokenProvider, httpClient, objectMapperWithDefaults);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to create OpaPolarisAuthorizer with SSL configuration", e);
+    }
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+
+      // Create HTTP POST request using Apache HttpComponents
+      HttpPost httpPost = new HttpPost(opaServerUrl + opaPolicyPath);
+      httpPost.setHeader("Content-Type", "application/json");
+
+      // Add bearer token authentication if provided
+      if (tokenProvider != null) {
+        String token = tokenProvider.getToken();
+        if (token != null && !token.isEmpty()) {
+          httpPost.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
+        }
+      }
+
+      httpPost.setEntity(new StringEntity(inputJson, StandardCharsets.UTF_8));
+
+      // Execute request
+      try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+        int statusCode = response.getStatusLine().getStatusCode();
+        if (statusCode != 200) {
+          return false;
+        }
+
+        // Read and parse response
+        String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(responseBody);
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();
+    input.set("actor", buildActorNode(principal));
+    input.put("action", op.name());
+    input.set("resource", buildResourceNode(targets, secondaries));
+    input.set("context", buildContextNode());
+    ObjectNode root = objectMapper.createObjectNode();
+    root.set("input", input);
+    return objectMapper.writeValueAsString(root);
+  }
+
+  /**
+   * Builds the actor section of the OPA input JSON.
+   *
+   * <p>Includes principal name, and roles as a generic field.
+   *
+   * @param principal the principal requesting authorization
+   * @return the actor node for OPA input
+   */
+  private ObjectNode buildActorNode(PolarisPrincipal principal) {
+    ObjectNode actor = objectMapper.createObjectNode();
+    actor.put("principal", principal.getName());
+    ArrayNode roles = objectMapper.createArrayNode();
+    for (String role : principal.getRoles()) roles.add(role);
+    actor.set("roles", roles);
+    return actor;
+  }
+
+  /**
+   * Builds the resource section of the OPA input JSON.
+   *
+   * <p>Includes the main target entity under 'primary' and secondary entities under 'secondaries'.
+   *
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities
+   * @return the resource node for OPA input
+   */
+  private ObjectNode buildResourceNode(
+      List<PolarisResolvedPathWrapper> targets, List<PolarisResolvedPathWrapper> secondaries) {
+    ObjectNode resource = objectMapper.createObjectNode();
+    // Main targets as 'targets' array
+    ArrayNode targetsArray = objectMapper.createArrayNode();
+    if (targets != null && !targets.isEmpty()) {
+      for (PolarisResolvedPathWrapper targetWrapper : targets) {
+        targetsArray.add(buildSingleResourceNode(targetWrapper));
+      }
+    }
+    resource.set("targets", targetsArray);
+    // Secondaries as array
+    ArrayNode secondariesArray = objectMapper.createArrayNode();
+    if (secondaries != null && !secondaries.isEmpty()) {
+      for (PolarisResolvedPathWrapper secondaryWrapper : secondaries) {
+        secondariesArray.add(buildSingleResourceNode(secondaryWrapper));
+      }
+    }
+    resource.set("secondaries", secondariesArray);
+    return resource;
+  }
+
+  /** Helper to build a resource node for a single PolarisResolvedPathWrapper. */
+  private ObjectNode buildSingleResourceNode(PolarisResolvedPathWrapper wrapper) {
+    ObjectNode node = objectMapper.createObjectNode();
+    if (wrapper == null) return node;
+    var resolvedEntity = wrapper.getResolvedLeafEntity();
+    if (resolvedEntity != null) {
+      var entity = resolvedEntity.getEntity();
+      node.put("type", entity.getType().name());
+      node.put("name", entity.getName());
+      var parentPath = wrapper.getResolvedParentPath();
+      if (parentPath != null && !parentPath.isEmpty()) {
+        ArrayNode parentsArray = objectMapper.createArrayNode();
+        for (var parent : parentPath) {
+          ObjectNode parentNode = objectMapper.createObjectNode();
+          parentNode.put("type", parent.getEntity().getType().name());
+          parentNode.put("name", parent.getEntity().getName());
+          parentsArray.add(parentNode);
+        }
+        node.set("parents", parentsArray);
+      }
+    }
+    return node;
+  }
+
+  /**
+   * Builds the context section of the OPA input JSON.
+   *
+   * <p>Includes only timestamp and request ID.
+   *
+   * @return the context node for OPA input
+   */
+  private ObjectNode buildContextNode() {
+    ObjectNode context = objectMapper.createObjectNode();
+    context.put("time", java.time.ZonedDateTime.now().toString());
+    context.put("request_id", java.util.UUID.randomUUID().toString());
+    return context;
+  }
+
+  /**
+   * Creates an SSL socket factory for HTTPS connections based on the configuration.
+   *
+   * @param opaServerUrl the OPA server URL
+   * @param verifySsl whether to verify SSL certificates
+   * @param trustStorePath custom trust store path (optional)
+   * @param trustStorePassword custom trust store password (optional)
+   * @return SSLConnectionSocketFactory for HTTPS connections, or null for HTTP
+   * @throws Exception if SSL configuration fails
+   */
+  private static SSLConnectionSocketFactory createSslSocketFactory(
+      String opaServerUrl, boolean verifySsl, String trustStorePath, String trustStorePassword)
+      throws Exception {
+
+    // Only configure SSL for HTTPS URLs
+    if (opaServerUrl == null || !opaServerUrl.toLowerCase(Locale.ROOT).startsWith("https")) {
+      return null;
+    }
+
+    SSLContextBuilder sslContextBuilder = SSLContextBuilder.create();
+    SSLContext sslContext;
+
+    if (!verifySsl) {
+      // Disable SSL verification (for development/testing)
+      sslContextBuilder.loadTrustMaterial(TrustAllStrategy.INSTANCE);
+      sslContext = sslContextBuilder.build();
+      return new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
+    } else if (trustStorePath != null && !trustStorePath.isEmpty()) {

Review Comment:
   nit: -> `Strings.isNullOrEmpty()`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2417316328


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Strings;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+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.CloseableHttpResponse;
+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.NoopHostnameVerifier;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.TrustAllStrategy;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.ssl.SSLContextBuilder;
+import org.apache.hc.core5.util.Timeout;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ *
+ * <p><strong>Beta Feature:</strong> This implementation is currently in Beta and is not a stable
+ * release. It may undergo breaking changes in future versions. Use with caution in production
+ * environments.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final BearerTokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      BearerTokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a BearerTokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      BearerTokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client) {
+
+    if (Strings.isNullOrEmpty(opaServerUrl)) {

Review Comment:
   Nit: -> `Preconditions.checkArgument(...)`



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility
+      ObjectMapper mapper) {
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(timeoutMs)
+              .setSocketTimeout(timeoutMs)
+              .setConnectionRequestTimeout(timeoutMs)
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);
+
+      // Create HTTP client with SSL configuration
+      CloseableHttpClient httpClient;
+      if (client instanceof CloseableHttpClient) {
+        httpClient = (CloseableHttpClient) client;
+      } else {
+        if (sslSocketFactory != null) {
+          httpClient =
+              HttpClients.custom()
+                  .setDefaultRequestConfig(requestConfig)
+                  .setSSLSocketFactory(sslSocketFactory)
+                  .build();
+        } else {
+          httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
+        }
+      }
+
+      ObjectMapper objectMapperWithDefaults = mapper != null ? mapper : new ObjectMapper();
+      return new OpaPolarisAuthorizer(
+          opaServerUrl, opaPolicyPath, tokenProvider, httpClient, objectMapperWithDefaults);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to create OpaPolarisAuthorizer with SSL configuration", e);
+    }
+  }
+
+  /**
+   * Authorizes a single target and secondary entity for the given principal and operation.
+   *
+   * <p>Delegates to the multi-target version for consistency.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param target the main target entity
+   * @param secondary the secondary entity (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable PolarisResolvedPathWrapper target,
+      @Nullable PolarisResolvedPathWrapper secondary) {
+    authorizeOrThrow(
+        polarisPrincipal,
+        activatedEntities,
+        authzOp,
+        target == null ? null : List.of(target),
+        secondary == null ? null : List.of(secondary));
+  }
+
+  /**
+   * Authorizes one or more target and secondary entities for the given principal and operation.
+   *
+   * <p>Sends the authorization context to OPA and throws if not allowed.
+   *
+   * @param polarisPrincipal the principal requesting authorization
+   * @param activatedEntities the set of activated entities (roles, etc.)
+   * @param authzOp the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @throws ForbiddenException if authorization is denied by OPA
+   */
+  @Override
+  public void authorizeOrThrow(
+      @Nonnull PolarisPrincipal polarisPrincipal,
+      @Nonnull Set<PolarisBaseEntity> activatedEntities,
+      @Nonnull PolarisAuthorizableOperation authzOp,
+      @Nullable List<PolarisResolvedPathWrapper> targets,
+      @Nullable List<PolarisResolvedPathWrapper> secondaries) {
+    boolean allowed = queryOpa(polarisPrincipal, activatedEntities, authzOp, targets, secondaries);
+    if (!allowed) {
+      throw new ForbiddenException("OPA denied authorization");
+    }
+  }
+
+  /**
+   * Sends an authorization query to the OPA server and parses the response.
+   *
+   * <p>Builds the OPA input JSON, sends it via HTTP POST, and checks the 'allow' field in the
+   * response.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return true if OPA allows the operation, false otherwise
+   * @throws RuntimeException if the OPA query fails
+   */
+  private boolean queryOpa(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries) {
+    try {
+      String inputJson = buildOpaInputJson(principal, entities, op, targets, secondaries);
+
+      // Create HTTP POST request using Apache HttpComponents
+      HttpPost httpPost = new HttpPost(opaServerUrl + opaPolicyPath);
+      httpPost.setHeader("Content-Type", "application/json");
+
+      // Add bearer token authentication if provided
+      if (tokenProvider != null) {
+        String token = tokenProvider.getToken();
+        if (token != null && !token.isEmpty()) {
+          httpPost.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
+        }
+      }
+
+      httpPost.setEntity(new StringEntity(inputJson, StandardCharsets.UTF_8));
+
+      // Execute request
+      try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+        int statusCode = response.getStatusLine().getStatusCode();
+        if (statusCode != 200) {
+          return false;
+        }
+
+        // Read and parse response
+        String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+        ObjectNode respNode = (ObjectNode) objectMapper.readTree(responseBody);
+        return respNode.path("result").path("allow").asBoolean(false);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("OPA query failed", e);
+    }
+  }
+
+  /**
+   * Builds the OPA input JSON for the authorization query.
+   *
+   * <p>Assembles the actor, action, resource, and context sections into the expected OPA input
+   * format.
+   *
+   * @param principal the principal requesting authorization
+   * @param entities the set of activated entities
+   * @param op the operation to authorize
+   * @param targets the list of main target entities
+   * @param secondaries the list of secondary entities (if any)
+   * @return the OPA input JSON string
+   * @throws IOException if JSON serialization fails
+   */
+  private String buildOpaInputJson(
+      PolarisPrincipal principal,
+      Set<PolarisBaseEntity> entities,
+      PolarisAuthorizableOperation op,
+      List<PolarisResolvedPathWrapper> targets,
+      List<PolarisResolvedPathWrapper> secondaries)
+      throws IOException {
+    ObjectNode input = objectMapper.createObjectNode();
+    input.set("actor", buildActorNode(principal));
+    input.put("action", op.name());
+    input.set("resource", buildResourceNode(targets, secondaries));
+    input.set("context", buildContextNode());
+    ObjectNode root = objectMapper.createObjectNode();
+    root.set("input", input);

Review Comment:
   A followup PR sounds good to me.
   



##########
runtime/service/src/main/java/org/apache/polaris/service/auth/OpaPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,97 @@
+/*
+ * 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.service.auth;
+
+import io.smallrye.common.annotation.Identifier;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import java.time.Duration;
+import org.apache.polaris.core.auth.FileTokenProvider;
+import org.apache.polaris.core.auth.OpaPolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
+import org.apache.polaris.core.auth.StaticTokenProvider;
+import org.apache.polaris.core.auth.TokenProvider;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.service.config.AuthorizationConfiguration;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@RequestScoped
+@Identifier("opa")
+public class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private final AuthorizationConfiguration authorizationConfig;
+
+  @Inject
+  public OpaPolarisAuthorizerFactory(AuthorizationConfiguration authorizationConfig) {
+    this.authorizationConfig = authorizationConfig;
+  }
+
+  @Override
+  public PolarisAuthorizer create(RealmConfig realmConfig) {
+    AuthorizationConfiguration.OpaConfig opa = authorizationConfig.opa();
+
+    // Create appropriate token provider based on configuration
+    TokenProvider tokenProvider = createTokenProvider(opa);
+
+    return OpaPolarisAuthorizer.create(
+        opa.url().orElse(null),
+        opa.policyPath().orElse(null),
+        tokenProvider,
+        opa.timeoutMs().orElse(2000), // Default to 2000ms if not specified
+        opa.verifySsl(), // Default is true from @WithDefault annotation
+        opa.trustStorePath().orElse(null),
+        opa.trustStorePassword().orElse(null),
+        null,
+        null);
+  }
+
+  /**
+   * Creates a token provider based on the OPA configuration.
+   *
+   * <p>Prioritizes static token over file-based token:
+   *
+   * <ol>
+   *   <li>If bearerToken.staticValue is set, uses StaticTokenProvider
+   *   <li>If bearerToken.filePath is set, uses FileTokenProvider
+   *   <li>Otherwise, returns StaticTokenProvider with null token
+   * </ol>
+   */
+  private TokenProvider createTokenProvider(AuthorizationConfiguration.OpaConfig opa) {
+    AuthorizationConfiguration.BearerTokenConfig bearerToken = opa.bearerToken();
+
+    // Static token takes precedence
+    if (bearerToken.staticValue().isPresent()) {
+      return new StaticTokenProvider(bearerToken.staticValue().get());
+    }
+
+    // File-based token as fallback
+    if (bearerToken.filePath().isPresent()) {
+      Duration refreshInterval = Duration.ofSeconds(bearerToken.refreshInterval());
+      boolean jwtExpirationRefresh = bearerToken.jwtExpirationRefresh();
+      Duration jwtExpirationBuffer = Duration.ofSeconds(bearerToken.jwtExpirationBuffer());
+
+      return new FileTokenProvider(

Review Comment:
   The change looks good to me! Thanks. We could introduce per-realm one with a new factory as a followup.



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Strings;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+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.CloseableHttpResponse;
+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.NoopHostnameVerifier;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.TrustAllStrategy;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.ssl.SSLContextBuilder;
+import org.apache.hc.core5.util.Timeout;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ *
+ * <p><strong>Beta Feature:</strong> This implementation is currently in Beta and is not a stable
+ * release. It may undergo breaking changes in future versions. Use with caution in production
+ * environments.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final BearerTokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      BearerTokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a BearerTokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      BearerTokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client) {
+
+    if (Strings.isNullOrEmpty(opaServerUrl)) {
+      throw new IllegalArgumentException("opaServerUrl cannot be null or empty");
+    }
+    if (Strings.isNullOrEmpty(opaPolicyPath)) {
+      throw new IllegalArgumentException("opaPolicyPath cannot be null or empty");
+    }
+
+    try {
+      // Create request configuration with timeouts
+      RequestConfig requestConfig =
+          RequestConfig.custom()
+              .setConnectTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .setResponseTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .setConnectionRequestTimeout(Timeout.ofMilliseconds(timeoutMs))
+              .build();
+
+      // Configure SSL for HTTPS connections
+      SSLConnectionSocketFactory sslSocketFactory =
+          createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword);
+
+      // Create HTTP client with SSL configuration
+      CloseableHttpClient httpClient;
+      if (client instanceof CloseableHttpClient) {
+        httpClient = (CloseableHttpClient) client;
+      } else {
+        if (sslSocketFactory != null) {
+          httpClient =
+              HttpClients.custom()
+                  .setDefaultRequestConfig(requestConfig)
+                  .setConnectionManager(
+                      PoolingHttpClientConnectionManagerBuilder.create()
+                          .setSSLSocketFactory(sslSocketFactory)
+                          .build())
+                  .build();
+        } else {
+          httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
+        }

Review Comment:
   Given the http client is passed from the parameter, do we still need line 129 to line 145?  Should `createSslSocketFactory` also be part of `ServiceProducer::opaHttpClient()`? Otherwise, the ssl config may not take any effect. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2416783079


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/JwtDecoder.java:
##########
@@ -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 {

Review Comment:
   Hi @flyrain - I initially did this to avoid pulling in an additional dependency. I see now that `java-jwt` is already a part of the version catalog. I'll add this dependency to `polaris-core` - let me know if that sounds good to you!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2451806440


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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")

Review Comment:
   Actually, I don't think that `verifySsl` should have any meaning for non-TLS connections. Mean, what should it verify without TLS(SSL)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2451830502


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  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 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
+   * @param clock clock instance for time operations
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer);
+  }
+
+  @Override
+  @Nullable
+  public String getToken() {
+    checkState(!closed, "Token provider is closed");
+
+    // Check if we need to refresh
+    if (shouldRefresh()) {
+      refreshToken();
+    }
+
+    return cachedToken;
+  }
+
+  @Override
+  public void close() {
+    closed = true;
+    cachedToken = null;
+  }
+
+  private boolean shouldRefresh() {
+    return clock.instant().isAfter(nextRefresh);
+  }
+
+  private void refreshToken() {
+    if (!refreshLock.compareAndSet(false, true)) {
+      return;
+    }
+    try {
+      String newToken = loadTokenFromFile();
+
+      // If we couldn't load a token and have no cached token, this is a fatal error
+      if (newToken == null && cachedToken == null) {
+        throw new RuntimeException(
+            "Unable to load bearer token from file: "
+                + tokenFilePath
+                + ". This is required for OPA authorization.");
+      }
+
+      // Only update cached token if we successfully loaded a new one
+      if (newToken != null) {
+        cachedToken = newToken;
+      }
+      // If newToken is null but cachedToken exists, we keep using the cached token
+
+      lastRefresh = clock.instant();
+
+      // Calculate next refresh time based on current token (may be cached)
+      nextRefresh = calculateNextRefresh(cachedToken);
+
+      logger.debug(
+          "Token refreshed from file: {} (token present: {}), next refresh: {}",
+          tokenFilePath,
+          cachedToken != null && !cachedToken.isEmpty(),
+          nextRefresh);
+    } finally {
+      refreshLock.set(false);
+    }
+  }
+
+  /** Calculate when the next refresh should occur based on JWT expiration or fixed interval. */
+  private Instant calculateNextRefresh(@Nullable String token) {
+    if (token == null || !jwtExpirationRefresh) {

Review Comment:
   With the above suggestion, `token` can never be `null`.
   
   ```suggestion
     private Instant calculateNextRefresh(String token) {
       if (!jwtExpirationRefresh) {
   ```



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Clock clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+
+  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 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
+   * @param clock clock instance for time operations
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Clock clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.clock = clock;
+    this.lastRefresh = Instant.MIN; // Force initial load
+    this.nextRefresh = Instant.MIN; // Force initial refresh
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer);
+  }
+
+  @Override
+  @Nullable
+  public String getToken() {
+    checkState(!closed, "Token provider is closed");
+
+    // Check if we need to refresh
+    if (shouldRefresh()) {
+      refreshToken();
+    }
+
+    return cachedToken;
+  }
+
+  @Override
+  public void close() {
+    closed = true;
+    cachedToken = null;
+  }
+
+  private boolean shouldRefresh() {
+    return clock.instant().isAfter(nextRefresh);
+  }
+
+  private void refreshToken() {
+    if (!refreshLock.compareAndSet(false, true)) {
+      return;
+    }
+    try {
+      String newToken = loadTokenFromFile();
+
+      // If we couldn't load a token and have no cached token, this is a fatal error
+      if (newToken == null && cachedToken == null) {
+        throw new RuntimeException(
+            "Unable to load bearer token from file: "
+                + tokenFilePath
+                + ". This is required for OPA authorization.");
+      }
+
+      // Only update cached token if we successfully loaded a new one
+      if (newToken != null) {
+        cachedToken = newToken;
+      }

Review Comment:
   Seems we need to shortcut here, otherwise we will retry too late.
   ```suggestion
   
         // Only update cached token if we successfully loaded a new one
         if (newToken == null) {
           logger.debug("Couldn't load new bearer token from {}, will retry.", tokenFilePath);
           return;
         }
         cachedToken = newToken;
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2454822745


##########
runtime/service/src/intTest/java/org/apache/polaris/service/it/ServiceProducersIT.java:
##########
@@ -0,0 +1,55 @@
+/*
+ * 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.service.it;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;

Review Comment:
   Done :)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2455081109


##########
runtime/service/src/intTest/java/org/apache/polaris/service/it/ServiceProducersIT.java:
##########
@@ -0,0 +1,55 @@
+/*
+ * 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.service.it;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;

Review Comment:
   thx!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2480016338


##########
runtime/service/build.gradle.kts:
##########
@@ -106,6 +106,8 @@ dependencies {
 
   implementation(libs.jakarta.servlet.api)
 
+  runtimeOnly(project(":polaris-async-vertx"))

Review Comment:
   I believe this actually is the CDI implementation of `polaris-async-api` `AsyncExec` that's optimized for Quarkus applications. Hence, we need it as a `runtimeOnly` dependency in `polaris-runtime-service`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3443583203

   > Just so I'm clear: is your suggestion to move the QuarkusIntegrationTest annotated tests back into `polaris-runtime-service`?
   
   Yes and no ;)
   
   Have non-Quarkus integration tests (like `OpaAuthorizerIT`) in the "main" OPA module and have a minimal set of OPA integration tests in polaris-runtime-service just to verify the integration with Quarkus. I suspect we don't need that many ITs in runtime-service, probably more or less just a "smoke test".
   I'm not really familiar with what's worth being integration tested in the non-Quarkus ITs.
   Test coverage for the OPA factory and the authorizer itself is pretty good.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2382356651


##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -193,6 +193,15 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization implementation settings
+# Which authorizer to use: "default" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+polaris.authorization.implementation=default
+
+# OPA Authorizer Configuration: effective only if polaris.authorization.implementation=opa
+# polaris.authorization.opa.url=http://localhost:8181

Review Comment:
   @dimas-b it's not required by default, but you can enable mTLS or Token based authentication for use with `basic` authorization that involves describing the policy with rego :) :
   
   https://www.openpolicyagent.org/docs/security#token-based-authentication-example



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2380312380


##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -193,6 +193,15 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization implementation settings
+# Which authorizer to use: "default" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+polaris.authorization.implementation=default
+
+# OPA Authorizer Configuration: effective only if polaris.authorization.implementation=opa
+# polaris.authorization.opa.url=http://localhost:8181

Review Comment:
   No, I meant do we need a username/password for talking to OPA?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453377643


##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,8 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa/impl
+polaris-extensions-auth-opa-tests=extensions/auth/opa/tests

Review Comment:
   After re-reading the current code, I see that a lot of the integration tests are currently in the `runtime/service` module. With that in mind, plus CI time concerns, I'd be ok with keeping OPA ITs in `runtime/service` in this PR as long as it introduces only "test" dependencies to the `runtime/service` module.
   
   We can discuss moving IT tests into `runtime/server`  later.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2454161946


##########
extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactoryTest.java:
##########
@@ -0,0 +1,163 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.Optional;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class OpaPolarisAuthorizerFactoryTest {
+
+  @TempDir Path tempDir;
+
+  @Test
+  public void testFactoryWithStaticTokenConfiguration() {
+    // Mock configuration for static token
+    OpaAuthorizationConfig.BearerTokenConfig.StaticTokenConfig staticTokenConfig =
+        mock(OpaAuthorizationConfig.BearerTokenConfig.StaticTokenConfig.class);

Review Comment:
   I had the same thought. But opted to not ask for it, as it's only for tests.
   (NB: Not a fan of mocking all-the-things - but open for compromises.)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2461341854


##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,252 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+import org.apache.polaris.nosql.async.AsyncExec;
+import org.apache.polaris.nosql.async.Cancelable;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Supplier<Instant> clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+  private final AsyncExec asyncExec;
+  private final CompletableFuture<String> initialTokenFuture = new CompletableFuture<>();
+  private final long initialTokenWaitMillis;
+
+  private volatile String cachedToken;
+  private volatile Instant lastRefresh;
+  private volatile Instant nextRefresh;
+  private volatile Cancelable<?> refreshTask;
+
+  /**
+   * 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
+   * @param clock clock instance for time operations
+   * @throws IllegalStateException if the initial token cannot be loaded from the file
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Duration initialTokenWait,
+      AsyncExec asyncExec,
+      Supplier<Instant> clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.initialTokenWaitMillis = initialTokenWait.toMillis();
+    this.clock = clock;
+    this.asyncExec = asyncExec;
+
+    this.nextRefresh = Instant.MIN;
+    this.lastRefresh = Instant.MIN;
+    // start refreshing the token (immediately)
+    scheduleRefreshAttempt(Duration.ZERO);
+
+    checkState(Files.isReadable(tokenFilePath), "OPA token file does not exist or is not readable");
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}, next refresh: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer,
+        nextRefresh);
+  }
+
+  @Override
+  public String getToken() {
+    String token = cachedToken;
+    if (token != null) {
+      // Regular case, we have a cached token
+      return token;
+    }
+    // We get here if the cached token is null, which means that the initial token
+    // has not been loaded yet.
+    // In this case we wait for the configured amount of time
+    // (5 seconds in production, much lower in tests).
+    try {
+      return initialTokenFuture.get(initialTokenWaitMillis, TimeUnit.MILLISECONDS);
+    } catch (Exception e) {
+      throw new IllegalStateException("Failed to read initial OPA bearer token", e);
+    }
+  }
+
+  @Override
+  public void close() {
+    cachedToken = null;
+    Cancelable<?> task = refreshTask;
+    if (task != null) {
+      refreshTask.cancel();
+    }
+  }
+
+  private void refreshTokenAttempt() {
+    boolean isInitialRefresh = cachedToken == null;
+    Duration delay;
+    if (doRefreshToken()) {
+      delay = Duration.between(clock.get(), nextRefresh);
+      if (isInitialRefresh) {
+        // If we have never cached a token, complete the initial token-future to "unblock"
+        // getToken() call sites waiting for it.
+        initialTokenFuture.complete(cachedToken);
+      }
+    } else {
+      // Token refresh did not succeed, retry soon
+      delay = Duration.ofSeconds(1); // TODO: make configurable
+    }
+    scheduleRefreshAttempt(delay);
+  }
+
+  private void scheduleRefreshAttempt(Duration delay) {
+    this.refreshTask = asyncExec.schedule(this::refreshTokenAttempt, delay);

Review Comment:
   Nice reuse of existing functionality 🚀 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3471419070

   Thanks a lot for working on it, @sungwy ! Thanks @snazy @dimas-b @jbonofre for the review.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "dimas-b (via GitHub)" <gi...@apache.org>.
dimas-b commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2461381495


##########
polaris-core/build.gradle.kts:
##########
@@ -112,6 +112,7 @@ dependencies {
   testFixturesApi(libs.jakarta.ws.rs.api)
 
   compileOnly(libs.jakarta.annotation.api)
+  compileOnly(libs.jakarta.enterprise.cdi.api)

Review Comment:
   IIRC, we had some prior discussions about avoiding CDI annotations in core as a convention...
   
   If it's required only for `@Identifier("internal")` on `DefaultPolarisAuthorizerFactory`, I'd propose remove the annotation from the class and make a producer method for it in `ServiceProducers`.
   
   Alternatively, we could remove `DefaultPolarisAuthorizerFactory` completely and update `ServiceProducers` to "manually" use `PolarisAuthorizerImpl` if `Instance<PolarisAuthorizerFactory>` is not resolvable in CDI.
   
   WDYT?



##########
extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,252 @@
+/*
+ * 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.token;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+import org.apache.polaris.nosql.async.AsyncExec;
+import org.apache.polaris.nosql.async.Cancelable;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.class);
+
+  private final Path tokenFilePath;
+  private final Duration refreshInterval;
+  private final boolean jwtExpirationRefresh;
+  private final Duration jwtExpirationBuffer;
+  private final Supplier<Instant> clock;
+  private final AtomicBoolean refreshLock = new AtomicBoolean();
+  private final AsyncExec asyncExec;
+  private final CompletableFuture<String> initialTokenFuture = new CompletableFuture<>();
+  private final long initialTokenWaitMillis;
+
+  private volatile String cachedToken;
+  private volatile Instant lastRefresh;
+  private volatile Instant nextRefresh;
+  private volatile Cancelable<?> refreshTask;
+
+  /**
+   * 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
+   * @param clock clock instance for time operations
+   * @throws IllegalStateException if the initial token cannot be loaded from the file
+   */
+  public FileBearerTokenProvider(
+      Path tokenFilePath,
+      Duration refreshInterval,
+      boolean jwtExpirationRefresh,
+      Duration jwtExpirationBuffer,
+      Duration initialTokenWait,
+      AsyncExec asyncExec,
+      Supplier<Instant> clock) {
+    this.tokenFilePath = tokenFilePath;
+    this.refreshInterval = refreshInterval;
+    this.jwtExpirationRefresh = jwtExpirationRefresh;
+    this.jwtExpirationBuffer = jwtExpirationBuffer;
+    this.initialTokenWaitMillis = initialTokenWait.toMillis();
+    this.clock = clock;
+    this.asyncExec = asyncExec;
+
+    this.nextRefresh = Instant.MIN;
+    this.lastRefresh = Instant.MIN;
+    // start refreshing the token (immediately)
+    scheduleRefreshAttempt(Duration.ZERO);
+
+    checkState(Files.isReadable(tokenFilePath), "OPA token file does not exist or is not readable");
+
+    logger.debug(
+        "Created file token provider for path: {} with refresh interval: {}, JWT expiration refresh: {}, JWT buffer: {}, next refresh: {}",
+        tokenFilePath,
+        refreshInterval,
+        jwtExpirationRefresh,
+        jwtExpirationBuffer,
+        nextRefresh);
+  }
+
+  @Override
+  public String getToken() {
+    String token = cachedToken;
+    if (token != null) {
+      // Regular case, we have a cached token
+      return token;
+    }
+    // We get here if the cached token is null, which means that the initial token
+    // has not been loaded yet.
+    // In this case we wait for the configured amount of time
+    // (5 seconds in production, much lower in tests).
+    try {
+      return initialTokenFuture.get(initialTokenWaitMillis, TimeUnit.MILLISECONDS);

Review Comment:
   optional: I do not want to disturb the impl. at this stage as it looks pretty solid to me... but I suppose it could be simplified this way:
   
   - keep a `volatile` reference to a `CompletionStage` with token data
   - `getToken` always calls `.get()` with a timeout
   - the initial ref. to the `CompletionStage` is make in the constructor by scheduling a refresh task
   - when refresh completes it will update the `CompletionStage` and reschedule
   - the `CompletionStage` will be "complete" in all cases but the initial refresh, so `.get()` calls will not wait except possibly on the first call.
   - we have less fields and less `if`s (hopefully) in this class (only `refreshTask` and the `CompletionStage`)
   
   WDYT?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2452923988


##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,8 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa/impl
+polaris-extensions-auth-opa-tests=extensions/auth/opa/tests

Review Comment:
   Having the OPA integration test within the service module will force downstream users who aren’t using OPA to disable those tests manually. I believe @dimas-b has similar concerns. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "flyrain (via GitHub)" <gi...@apache.org>.
flyrain commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2452932879


##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,8 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa/impl
+polaris-extensions-auth-opa-tests=extensions/auth/opa/tests

Review Comment:
   How much CI time was added after splitting out the integration tests? I also don't think this should be a blocker.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2453014715


##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -197,6 +197,40 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization type settings
+# Which authorizer to use: "internal" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+# polaris.authorization.type=internal
+
+# OPA Authorizer Configuration: effective only if polaris.authorization.type=opa
+# NOTE: The OPA Authorizer is currently in Beta and is not a stable release.
+# It may undergo breaking changes in future versions.
+# polaris.authorization.opa.url=http://localhost:8181
+# polaris.authorization.opa.policy-path=/v1/data/polaris/authz/allow
+
+# OPA HTTP configuration
+# polaris.authorization.opa.http.timeout-ms=5000
+# polaris.authorization.opa.http.verify-ssl=false

Review Comment:
   Mind adding the comment about verify-ssl?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on PR #2680:
URL: https://github.com/apache/polaris/pull/2680#issuecomment-3392293058

   > Will post my complete review soon. Adding one thing first, currently all grant and revoke privilege operation (e.g. `grantPrivilegeOnCatalogToRole`) won’t work as expected, since these mapping has to happen in the OPA side. How do we position these APIs? If we still want to support them within Polaris, we need new interfaces between the OPA service and Polaris. I think the least thing we could do now is blocking these operation for OPA authorizer. Given the current PR is large, I’m OK to leave it in another PR. Thanks @singhpk234 for bring it up for discussion.
   
   Hi @flyrain - I agree! In our planned architecture, we are thinking of returning `false` on the Management APIs through the OPA Server rego. But I agree that we should review whether it would make sense to just systematically fail on these actions within the OpaPolarisAuthorizer itself before sending the request to OPA .


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2416792799


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,407 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final TokenProvider tokenProvider;
+  private final CloseableHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      CloseableHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.tokenProvider = tokenProvider;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory that accepts a TokenProvider for advanced token management.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param tokenProvider Token provider for authentication (optional)
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param verifySsl Whether to verify SSL certificates for HTTPS connections
+   * @param trustStorePath Custom SSL trust store path (optional)
+   * @param trustStorePassword Custom SSL trust store password (optional)
+   * @param client Apache HttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(
+      String opaServerUrl,
+      String opaPolicyPath,
+      TokenProvider tokenProvider,
+      int timeoutMs,
+      boolean verifySsl,
+      String trustStorePath,
+      String trustStorePassword,
+      Object client, // Accept any client type for compatibility
+      ObjectMapper mapper) {
+

Review Comment:
   Appreciate the careful review @flyrain - I've added the checks



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2395874717


##########
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+// Removed Quarkus/MicroProfile annotations for portability
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {
+  private final String opaServerUrl;
+  private final String opaPolicyPath;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+
+  /** Private constructor for factory method and advanced wiring. */
+  private OpaPolarisAuthorizer(
+      String opaServerUrl,
+      String opaPolicyPath,
+      OkHttpClient httpClient,
+      ObjectMapper objectMapper) {
+    this.opaServerUrl = opaServerUrl;
+    this.opaPolicyPath = opaPolicyPath;
+    this.httpClient = httpClient;
+    this.objectMapper = objectMapper;
+  }
+
+  /**
+   * Static factory for runtime configuration and CDI producer compatibility.
+   *
+   * @param opaServerUrl OPA server URL
+   * @param opaPolicyPath OPA policy path
+   * @param timeoutMs HTTP call timeout in milliseconds
+   * @param client OkHttpClient (optional, can be null)
+   * @param mapper ObjectMapper (optional, can be null)
+   * @return OpaPolarisAuthorizer instance
+   */
+  public static OpaPolarisAuthorizer create(

Review Comment:
   Noted - I've made changes to the code to make use of `OpaPolarisAuthorizerFactory` and `DefaultPolarisAuthorizerFactory` to use the `@Identifier` annotation instead. Please let me know if this is what you meant!



##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -193,6 +193,15 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization implementation settings
+# Which authorizer to use: "default" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+polaris.authorization.implementation=default

Review Comment:
   done !



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2395876803


##########
polaris-core/build.gradle.kts:
##########
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(project(":polaris-api-management-model"))
+  implementation("com.squareup.okhttp3:okhttp:4.12.0")

Review Comment:
   Took both feedback into account



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2439483935


##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   Is this property really necessary? Feels like a redundant setting because `staticToken()` and `fileBased()` are mutually exclusive.



##########
runtime/test-common/src/main/java/org/apache/polaris/test/commons/OpaTestResource.java:
##########
@@ -0,0 +1,123 @@
+/*
+ * 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.test.commons;
+
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+public class OpaTestResource implements QuarkusTestResourceLifecycleManager {
+  private static GenericContainer<?> opa;
+  private int mappedPort;
+  private Map<String, String> resourceConfig;
+
+  @Override
+  public void init(Map<String, String> initArgs) {
+    this.resourceConfig = initArgs;
+  }
+
+  @Override
+  public Map<String, String> start() {
+    try {
+      // Reuse container across tests to speed up execution
+      if (opa == null || !opa.isRunning()) {
+        opa =
+            new GenericContainer<>(DockerImageName.parse("openpolicyagent/opa:0.63.0"))

Review Comment:
   Better use `ContainerSpecHelper`, see `MinioContainer` for an example.



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   ```suggestion
         String value();
   ```
   This property has to be present when using the static token provider.



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   Mind unifying both properties and changing the type to `Optional<URI>`?



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java:
##########
@@ -0,0 +1,343 @@
+/*
+ * 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.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+import org.apache.polaris.extension.auth.opa.token.BearerTokenProvider;
+
+/**
+ * OPA-based implementation of {@link PolarisAuthorizer}.
+ *
+ * <p>This authorizer delegates authorization decisions to an Open Policy Agent (OPA) server using a
+ * configurable REST API endpoint and policy path. The input to OPA is constructed from the
+ * principal, entities, operation, and resource context.
+ *
+ * <p><strong>Beta Feature:</strong> This implementation is currently in Beta and is not a stable
+ * release. It may undergo breaking changes in future versions. Use with caution in production
+ * environments.
+ */
+public class OpaPolarisAuthorizer implements PolarisAuthorizer {

Review Comment:
   Does this need to be `public`?



##########
runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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.service.auth;
+
+import io.smallrye.common.annotation.Identifier;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
+import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
+import org.apache.polaris.core.config.RealmConfig;
+
+/** Factory for creating the default Polaris authorizer implementation. */
+@ApplicationScoped
+@Identifier("internal")
+public class DefaultPolarisAuthorizerFactory implements PolarisAuthorizerFactory {

Review Comment:
   Should this go into `:polaris-core` and `PolarisAuthorizerImpl` become package-private?



##########
runtime/service/src/intTest/java/org/apache/polaris/service/auth/opa/OpaFileTokenIntegrationTest.java:
##########
@@ -0,0 +1,293 @@
+/*
+ * 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.service.auth.opa;
+
+import static io.restassured.RestAssured.given;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import io.quarkus.test.junit.QuarkusTestProfile.TestResourceEntry;
+import io.quarkus.test.junit.TestProfile;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.polaris.test.commons.OpaTestResource;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+@TestProfile(OpaFileTokenIntegrationTest.FileTokenOpaProfile.class)
+public class OpaFileTokenIntegrationTest {
+
+  @ConfigProperty(name = "polaris.authorization.opa.auth.bearer.file-based.path")
+  String tokenFilePath;
+
+  public static class FileTokenOpaProfile implements QuarkusTestProfile {
+
+    @Override
+    public Map<String, String> getConfigOverrides() {
+      Map<String, String> config = new HashMap<>();
+      config.put("polaris.authorization.type", "opa");
+      config.put("polaris.authorization.opa.policy-path", "/v1/data/polaris/authz");
+      config.put("polaris.authorization.opa.http.timeout-ms", "2000");
+
+      // Configure OPA server authentication with file-based bearer token
+      config.put("polaris.authorization.opa.auth.type", "bearer");
+      config.put("polaris.authorization.opa.auth.bearer.type", "file-based");
+      // Token file path will be provided by OpaFileTokenTestResource
+      config.put(
+          "polaris.authorization.opa.http.verify-ssl",
+          "false"); // Disable SSL verification for tests
+
+      // TODO: Add tests for OIDC and federated principal
+      config.put("polaris.authentication.type", "internal");
+
+      return config;
+    }
+
+    @Override
+    public List<TestResourceEntry> testResources() {
+      String customRegoPolicy =
+          """
+        package polaris.authz
+
+        default allow := false
+
+        # Allow root user for all operations
+        allow {
+          input.actor.principal == "root"
+        }
+
+        # Allow admin user for all operations
+        allow {
+          input.actor.principal == "admin"
+        }
+
+        # Deny stranger user explicitly (though default is false)
+        allow {
+          input.actor.principal == "stranger"
+          false
+        }
+        """;
+
+      return List.of(
+          new TestResourceEntry(
+              OpaTestResource.class,
+              Map.of("policy-name", "polaris-authz", "rego-policy", customRegoPolicy)),
+          new TestResourceEntry(OpaFileTokenTestResource.class));
+    }
+  }
+
+  /**
+   * Test demonstrates OPA integration with file-based bearer token authentication. This test
+   * verifies that the FileBearerTokenProvider correctly reads tokens from a file and that the full
+   * integration works with file-based configuration.
+   */
+  @Test
+  void testOpaAllowsRootUserWithFileToken() {
+    // Test demonstrates the complete integration flow with file-based tokens:
+    // 1. OAuth token acquisition with internal authentication
+    // 2. OPA policy allowing root users
+    // 3. Bearer token read from file by FileBearerTokenProvider
+
+    // Get a token using the catalog service OAuth endpoint
+    String response =
+        given()
+            .contentType("application/x-www-form-urlencoded")
+            .formParam("grant_type", "client_credentials")
+            .formParam("client_id", "test-admin")
+            .formParam("client_secret", "test-secret")
+            .formParam("scope", "PRINCIPAL_ROLE:ALL")
+            .when()
+            .post("/api/catalog/v1/oauth/tokens")
+            .then()
+            .statusCode(200)
+            .extract()
+            .body()
+            .asString();
+
+    // Parse JSON response to get access_token
+    String accessToken = extractJsonValue(response, "access_token");
+
+    if (accessToken == null) {
+      fail("Failed to parse access_token from OAuth response: " + response);
+    }
+
+    // Use the Bearer token to test OPA authorization
+    // The JWT token has principal "root" which our policy allows
+    given()
+        .header("Authorization", "Bearer " + accessToken)
+        .when()
+        .get("/api/management/v1/principals")
+        .then()
+        .statusCode(200); // Should succeed - "root" user is allowed by policy
+  }
+
+  @Test
+  void testFileTokenRefresh() throws IOException, InterruptedException {
+    // This test verifies that the FileBearerTokenProvider refreshes tokens from the file
+
+    // First verify the system works with the initial token
+    String rootToken = getRootToken();
+
+    given()
+        .header("Authorization", "Bearer " + rootToken)
+        .when()
+        .get("/api/management/v1/principals")
+        .then()
+        .statusCode(200);
+
+    // Get the token file path from injected configuration
+    Path tokenFile = Path.of(tokenFilePath);
+    if (Files.exists(tokenFile)) {
+      String originalContent = Files.readString(tokenFile);
+
+      // Update the file content
+      Files.writeString(tokenFile, "test-opa-bearer-token-updated-12345");
+
+      // Wait for refresh interval (1 second as configured) plus some buffer
+      Thread.sleep(1500); // 1.5 seconds to ensure refresh happens

Review Comment:
   Hm, I'm not a fan of sleeps in tests at all. It's effectively a recipe for flaky tests (beside that those tests run longer than necessary).
   
   Instead let's use a `Clock` where it's needed and use `MutableClock` from tests.



##########
polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,38 @@
+/*
+ * 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 org.apache.polaris.core.config.RealmConfig;
+
+/**
+ * Factory interface for creating PolarisAuthorizer instances.
+ *
+ * <p>This follows the standard Polaris pattern of using CDI with @Identifier annotations to select
+ * different implementations at runtime.
+ */
+public interface PolarisAuthorizerFactory {

Review Comment:
   I like the introduction of these abstractions :)



##########
extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaHttpClientFactoryTest.java:
##########
@@ -0,0 +1,71 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Optional;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for OpaHttpClientFactory. */
+public class OpaHttpClientFactoryTest {
+
+  @Test
+  void testCreateHttpClientWithHttpUrl() throws Exception {
+    OpaAuthorizationConfig.HttpConfig httpConfig = createMockHttpConfig(5000, true, null, null);
+
+    CloseableHttpClient client = OpaHttpClientFactory.createHttpClient(httpConfig);
+
+    assertNotNull(client);
+    client.close();
+  }
+
+  @Test
+  void testCreateHttpClientWithHttpsUrl() throws Exception {
+    OpaAuthorizationConfig.HttpConfig httpConfig = createMockHttpConfig(5000, false, null, null);
+
+    CloseableHttpClient client = OpaHttpClientFactory.createHttpClient(httpConfig);
+
+    assertNotNull(client);
+    client.close();
+  }
+
+  @Test
+  void testCreateHttpClientWithCustomTimeout() throws Exception {

Review Comment:
   What's the effective difference to `testCreateHttpClientWithHttpUrl()`?



##########
extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProviderTest.java:
##########
@@ -0,0 +1,355 @@
+/*
+ * 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.token;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class FileBearerTokenProviderTest {
+
+  @TempDir Path tempDir;
+
+  @Test
+  public void testLoadTokenFromFile() throws IOException {
+    // Create a temporary token file
+    Path tokenFile = tempDir.resolve("token.txt");
+    String expectedToken = "test-bearer-token-123";
+    Files.writeString(tokenFile, expectedToken);
+
+    // Create file token provider
+    FileBearerTokenProvider provider =
+        new FileBearerTokenProvider(tokenFile.toString(), Duration.ofMinutes(5));
+
+    // Test token retrieval
+    String actualToken = provider.getToken();
+    assertEquals(expectedToken, actualToken);
+
+    provider.close();
+  }
+
+  @Test
+  public void testLoadTokenFromFileWithWhitespace() throws IOException {
+    // Create a temporary token file with whitespace
+    Path tokenFile = tempDir.resolve("token.txt");
+    String tokenWithWhitespace = "  test-bearer-token-456  \n\t";
+    String expectedToken = "test-bearer-token-456";
+    Files.writeString(tokenFile, tokenWithWhitespace);
+
+    // Create file token provider
+    FileBearerTokenProvider provider =
+        new FileBearerTokenProvider(tokenFile.toString(), Duration.ofMinutes(5));
+
+    // Test token retrieval (should trim whitespace)
+    String actualToken = provider.getToken();
+    assertEquals(expectedToken, actualToken);
+
+    provider.close();
+  }
+
+  @Test
+  public void testTokenRefresh() throws IOException, InterruptedException {
+    // Create a temporary token file
+    Path tokenFile = tempDir.resolve("token.txt");
+    String initialToken = "initial-token";
+    Files.writeString(tokenFile, initialToken);
+
+    // Create file token provider with short refresh interval
+    FileBearerTokenProvider provider =
+        new FileBearerTokenProvider(tokenFile.toString(), Duration.ofMillis(100));
+
+    // Test initial token
+    String token1 = provider.getToken();
+    assertEquals(initialToken, token1);
+
+    // Wait for refresh interval to pass
+    Thread.sleep(200);
+
+    // Update the file
+    String updatedToken = "updated-token";
+    Files.writeString(tokenFile, updatedToken);
+
+    // Test that token is refreshed
+    String token2 = provider.getToken();
+    assertEquals(updatedToken, token2);
+
+    provider.close();
+  }
+
+  @Test
+  public void testNonExistentFile() {
+    // Create file token provider for non-existent file
+    FileBearerTokenProvider provider =
+        new FileBearerTokenProvider("/non/existent/file.txt", Duration.ofMinutes(5));
+
+    // Test token retrieval (should return null)
+    String token = provider.getToken();
+    assertNull(token);
+
+    provider.close();
+  }
+
+  @Test
+  public void testEmptyFile() throws IOException {
+    // Create an empty token file
+    Path tokenFile = tempDir.resolve("empty.txt");
+    Files.writeString(tokenFile, "");
+
+    // Create file token provider
+    FileBearerTokenProvider provider =
+        new FileBearerTokenProvider(tokenFile.toString(), Duration.ofMinutes(5));
+
+    // Test token retrieval (should return null for empty file)
+    String token = provider.getToken();
+    assertNull(token);
+
+    provider.close();
+  }
+
+  @Test
+  public void testClosedProvider() throws IOException {
+    // Create a temporary token file
+    Path tokenFile = tempDir.resolve("token.txt");
+    Files.writeString(tokenFile, "test-token");
+
+    // Create and close file token provider
+    FileBearerTokenProvider provider =
+        new FileBearerTokenProvider(tokenFile.toString(), Duration.ofMinutes(5));
+    provider.close();
+
+    // Test token retrieval after closing (should return null)
+    String token = provider.getToken();
+    assertNull(token);
+  }
+
+  @Test
+  public void testJwtExpirationRefresh() throws IOException, InterruptedException {
+    // Create a temporary token file with a JWT that expires in 10 seconds
+    Path tokenFile = tempDir.resolve("jwt-token.txt");
+    String jwtToken = createJwtWithExpiration(Instant.now().plusSeconds(10));
+    Files.writeString(tokenFile, jwtToken);
+
+    // Create file token provider with JWT expiration refresh enabled
+    // Buffer of 3 seconds means it should refresh 3 seconds before expiration (at 7 seconds)
+    FileBearerTokenProvider provider =
+        new FileBearerTokenProvider(
+            tokenFile.toString(), Duration.ofMinutes(10), true, Duration.ofSeconds(3));
+
+    // Test initial token
+    String token1 = provider.getToken();
+    assertEquals(jwtToken, token1);
+
+    // Wait for 7.1 seconds (should trigger refresh due to 3 second buffer)
+    Thread.sleep(7100);

Review Comment:
   Sorry, that's way too long for a simple unit test. Let's use `MutableClock`.



##########
extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.entity.PolarisEntity;
+import org.apache.polaris.core.entity.PolarisEntityType;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+import org.apache.polaris.core.persistence.ResolvedPolarisEntity;
+import org.apache.polaris.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Unit tests for OpaPolarisAuthorizer including basic functionality and bearer token authentication
+ */
+public class OpaPolarisAuthorizerTest {
+
+  @Test
+  void testOpaInputJsonFormat() throws Exception {
+    // Capture the request body for verification
+    final String[] capturedRequestBody = new String[1];
+
+    HttpServer server = createServerWithRequestCapture(capturedRequestBody);
+
+    String url = "http://localhost:" + server.getAddress().getPort();
+
+    OpaPolarisAuthorizer authorizer =
+        createWithStringToken(
+            url, "/v1/data/polaris/authz/allow", (String) null, HttpClients.createDefault());

Review Comment:
   The client should be properly closed here and in other tests.



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   Can use `Duration` here to remove ambiguity.



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/token/BearerTokenProvider.java:
##########
@@ -0,0 +1,51 @@
+/*
+ * 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.token;
+
+import jakarta.annotation.Nullable;
+
+/**
+ * Interface for providing bearer tokens for authentication.
+ *
+ * <p>Implementations can provide tokens from various sources such as:
+ *
+ * <ul>
+ *   <li>Static string values
+ *   <li>Files (with automatic reloading)
+ *   <li>External token services
+ * </ul>
+ */
+public interface BearerTokenProvider {
+
+  /**
+   * Get the current bearer token.
+   *
+   * @return the bearer token, or null if no token is available
+   */
+  @Nullable
+  String getToken();
+
+  /**
+   * Clean up any resources used by this token provider. Should be called when the provider is no
+   * longer needed.
+   */
+  default void close() {

Review Comment:
   Better let the interface extend `AutoCloseable`. 



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,130 @@
+/*
+ * 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 io.smallrye.common.annotation.Identifier;
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.time.Duration;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+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.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@ApplicationScoped
+@Identifier("opa")
+public class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private final OpaAuthorizationConfig opaConfig;
+  private CloseableHttpClient httpClient;
+  private BearerTokenProvider bearerTokenProvider;

Review Comment:
   Need to close this one in a @PreDestroy



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   Can use `Duration` here to remove ambiguity.



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,130 @@
+/*
+ * 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 io.smallrye.common.annotation.Identifier;
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.time.Duration;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+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.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@ApplicationScoped
+@Identifier("opa")
+public class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
+
+  private final OpaAuthorizationConfig opaConfig;
+  private CloseableHttpClient httpClient;

Review Comment:
   Need to close this one in a `@PreDestroy`



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   ```suggestion
         String path();
   ```
   This property has to be present when using the file-based token provider.



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   ```suggestion
       Optional<Path> trustStorePath();
   ```



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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")

Review Comment:
   Blindly trusting all presented server certificates is a security risk. Could you add a _severe_ production-readiness (see `ProductionReadinessChecks`) check that fails startup if this is set to `false`?



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   Mind using an `enum` instead of `String`?



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.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 FileBearerTokenProvider(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 FileBearerTokenProvider(
+      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);
+
+      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 = getJwtExpirationTime(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;

Review Comment:
   This read can race with the write of a new token file. For example (similarly when k8s uses a temp-file+rename):
   1. New token file gets created (it's empty)
   2. This function reads the empty file
   
   This leads to authorization failures for the whole `refreshInterval` duration. A better way is to retry in such cases and continue to use the already known token.
   
   IMO `getToken()` should never return `null`, either there is a token or it fails hard with an exception. But this one shouldn't cause explicit or implicit failures.



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.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 FileBearerTokenProvider(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 FileBearerTokenProvider(
+      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);
+
+      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 = getJwtExpirationTime(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)) {

Review Comment:
   `.exists()` is superfluous, implicit via `.isReadable()`.



##########
gradle/libs.versions.toml:
##########
@@ -18,6 +18,7 @@
 #
 
 [versions]
+apache-httpclient5 = "5.5"

Review Comment:
   Only one usage, can be inlined.
   
   BTW: Any reason why this doesn't use 5.5.1?



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.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 FileBearerTokenProvider(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 FileBearerTokenProvider(
+      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);
+
+      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 = getJwtExpirationTime(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);

Review Comment:
   This will fail if the token-file-path is a directory, but I think that's fine.



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaHttpClientFactory.java:
##########
@@ -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 {

Review Comment:
   ```suggestion
   class OpaHttpClientFactory {
   ```



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerFactory.java:
##########
@@ -0,0 +1,130 @@
+/*
+ * 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 io.smallrye.common.annotation.Identifier;
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.time.Duration;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+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.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.FileBearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+
+/** Factory for creating OPA-based Polaris authorizer implementations. */
+@ApplicationScoped
+@Identifier("opa")
+public class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {

Review Comment:
   Looks like this class should only be used via CDI.
   ```suggestion
   class OpaPolarisAuthorizerFactory implements PolarisAuthorizerFactory {
   ```
   



##########
runtime/defaults/src/main/resources/application.properties:
##########
@@ -197,6 +197,40 @@ polaris.oidc.principal-roles-mapper.type=default
 # polaris.storage.gcp.token=token
 # polaris.storage.gcp.lifespan=PT1H
 
+# Polaris authorization type settings
+# Which authorizer to use: "internal" (PolarisAuthorizerImpl) or "opa" (OpaPolarisAuthorizer)
+polaris.authorization.type=internal

Review Comment:
   Nit: this is the implicit default, no need to explicitly set it.
   ```suggestion
   #polaris.authorization.type=internal
   ```



##########
extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProviderTest.java:
##########
@@ -0,0 +1,355 @@
+/*
+ * 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.token;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class FileBearerTokenProviderTest {
+
+  @TempDir Path tempDir;
+
+  @Test
+  public void testLoadTokenFromFile() throws IOException {
+    // Create a temporary token file
+    Path tokenFile = tempDir.resolve("token.txt");
+    String expectedToken = "test-bearer-token-123";
+    Files.writeString(tokenFile, expectedToken);
+
+    // Create file token provider
+    FileBearerTokenProvider provider =
+        new FileBearerTokenProvider(tokenFile.toString(), Duration.ofMinutes(5));
+
+    // Test token retrieval
+    String actualToken = provider.getToken();
+    assertEquals(expectedToken, actualToken);
+
+    provider.close();

Review Comment:
   `.close()` here and elsewhere should happen in a try-finally (or better try-with-resources)



##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProvider.java:
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+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.Date;
+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 FileBearerTokenProvider implements BearerTokenProvider {
+
+  private static final Logger logger = LoggerFactory.getLogger(FileBearerTokenProvider.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 FileBearerTokenProvider(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 FileBearerTokenProvider(
+      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) {

Review Comment:
   The function should rather fail hard (Guava's `checkState(!closed, "...")`) as this should never be called once the provider's closed.



##########
extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/token/StaticBearerTokenProviderTest.java:
##########
@@ -0,0 +1,43 @@
+/*
+ * 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.token;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+public class StaticBearerTokenProviderTest {
+
+  @Test
+  public void testStaticBearerTokenProvider() {
+    String expectedToken = "static-bearer-token";
+    StaticBearerTokenProvider provider = new StaticBearerTokenProvider(expectedToken);
+
+    String actualToken = provider.getToken();
+    assertEquals(expectedToken, actualToken);
+  }
+
+  @Test
+  public void testStaticBearerTokenProviderWithEmptyString() {
+    StaticBearerTokenProvider provider = new StaticBearerTokenProvider("");

Review Comment:
   IMO an empty string or null should never be allowed and the class should fail hard early.



##########
extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/token/FileBearerTokenProviderTest.java:
##########
@@ -0,0 +1,355 @@
+/*
+ * 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.token;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class FileBearerTokenProviderTest {
+
+  @TempDir Path tempDir;
+
+  @Test
+  public void testLoadTokenFromFile() throws IOException {
+    // Create a temporary token file
+    Path tokenFile = tempDir.resolve("token.txt");
+    String expectedToken = "test-bearer-token-123";
+    Files.writeString(tokenFile, expectedToken);
+
+    // Create file token provider
+    FileBearerTokenProvider provider =
+        new FileBearerTokenProvider(tokenFile.toString(), Duration.ofMinutes(5));
+
+    // Test token retrieval
+    String actualToken = provider.getToken();
+    assertEquals(expectedToken, actualToken);
+
+    provider.close();
+  }
+
+  @Test
+  public void testLoadTokenFromFileWithWhitespace() throws IOException {
+    // Create a temporary token file with whitespace
+    Path tokenFile = tempDir.resolve("token.txt");
+    String tokenWithWhitespace = "  test-bearer-token-456  \n\t";
+    String expectedToken = "test-bearer-token-456";
+    Files.writeString(tokenFile, tokenWithWhitespace);
+
+    // Create file token provider
+    FileBearerTokenProvider provider =
+        new FileBearerTokenProvider(tokenFile.toString(), Duration.ofMinutes(5));
+
+    // Test token retrieval (should trim whitespace)
+    String actualToken = provider.getToken();
+    assertEquals(expectedToken, actualToken);
+
+    provider.close();
+  }
+
+  @Test
+  public void testTokenRefresh() throws IOException, InterruptedException {
+    // Create a temporary token file
+    Path tokenFile = tempDir.resolve("token.txt");
+    String initialToken = "initial-token";
+    Files.writeString(tokenFile, initialToken);
+
+    // Create file token provider with short refresh interval
+    FileBearerTokenProvider provider =
+        new FileBearerTokenProvider(tokenFile.toString(), Duration.ofMillis(100));
+
+    // Test initial token
+    String token1 = provider.getToken();
+    assertEquals(initialToken, token1);
+
+    // Wait for refresh interval to pass
+    Thread.sleep(200);

Review Comment:
   Hm, I'm not a fan of sleeps in tests at all. It's effectively a recipe for flaky tests (beside that those tests run longer than necessary).
   Instead let's use a Clock where it's needed and use MutableClock from tests.



##########
extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaHttpClientFactoryTest.java:
##########
@@ -0,0 +1,71 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Optional;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for OpaHttpClientFactory. */
+public class OpaHttpClientFactoryTest {
+
+  @Test
+  void testCreateHttpClientWithHttpUrl() throws Exception {
+    OpaAuthorizationConfig.HttpConfig httpConfig = createMockHttpConfig(5000, true, null, null);
+
+    CloseableHttpClient client = OpaHttpClientFactory.createHttpClient(httpConfig);
+
+    assertNotNull(client);
+    client.close();

Review Comment:
   Similar to the provider tests, should use `AutoCloseable` + try-with-resources.



##########
runtime/defaults/src/main/resources/application-it.properties:
##########
@@ -56,3 +56,5 @@ polaris.realm-context.realms=POLARIS,OTHER
 polaris.storage.gcp.token=token
 polaris.storage.gcp.lifespan=PT1H
 
+

Review Comment:
   Nit: noop change



##########
extensions/auth/opa/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.entity.PolarisEntity;
+import org.apache.polaris.core.entity.PolarisEntityType;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+import org.apache.polaris.core.persistence.ResolvedPolarisEntity;
+import org.apache.polaris.extension.auth.opa.token.BearerTokenProvider;
+import org.apache.polaris.extension.auth.opa.token.StaticBearerTokenProvider;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Unit tests for OpaPolarisAuthorizer including basic functionality and bearer token authentication
+ */
+public class OpaPolarisAuthorizerTest {
+
+  @Test
+  void testOpaInputJsonFormat() throws Exception {
+    // Capture the request body for verification
+    final String[] capturedRequestBody = new String[1];
+
+    HttpServer server = createServerWithRequestCapture(capturedRequestBody);
+
+    String url = "http://localhost:" + server.getAddress().getPort();
+
+    OpaPolarisAuthorizer authorizer =
+        createWithStringToken(
+            url, "/v1/data/polaris/authz/allow", (String) null, HttpClients.createDefault());
+
+    PolarisPrincipal principal =
+        PolarisPrincipal.of("eve", Map.of("department", "finance"), Set.of("auditor"));
+
+    Set<PolarisBaseEntity> entities = Set.of();
+    PolarisResolvedPathWrapper target = new PolarisResolvedPathWrapper(List.of());
+    PolarisResolvedPathWrapper secondary = new PolarisResolvedPathWrapper(List.of());
+
+    assertDoesNotThrow(
+        () ->
+            authorizer.authorizeOrThrow(
+                principal, entities, PolarisAuthorizableOperation.LOAD_VIEW, target, secondary));
+
+    // Parse and verify JSON structure from captured request
+    ObjectMapper mapper = new ObjectMapper();
+    JsonNode root = mapper.readTree(capturedRequestBody[0]);
+    assertTrue(root.has("input"), "Root should have 'input' field");
+    var input = root.get("input");
+    assertTrue(input.has("actor"), "Input should have 'actor' field");
+    assertTrue(input.has("action"), "Input should have 'action' field");
+    assertTrue(input.has("resource"), "Input should have 'resource' field");
+    assertTrue(input.has("context"), "Input should have 'context' field");
+
+    server.stop(0);

Review Comment:
   Please use try-finally or try-with-resources, also in the other tests.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "sungwy (via GitHub)" <gi...@apache.org>.
sungwy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2444954281


##########
gradle/projects.main.properties:
##########
@@ -43,6 +43,8 @@ polaris-version=tools/version
 polaris-misc-types=tools/misc-types
 polaris-extensions-federation-hadoop=extensions/federation/hadoop
 polaris-extensions-federation-hive=extensions/federation/hive
+polaris-extensions-auth-opa=extensions/auth/opa/impl
+polaris-extensions-auth-opa-tests=extensions/auth/opa/tests

Review Comment:
   I just had moved this out from the runtime module as a response to this [mailing list discussion thread here](https://lists.apache.org/thread/54qdbsxs3j7wwhv3tsccqj6qng5lqgmz).
   
   It looks like we are still working on finding a consensus on what the right approach to organize extensions would be. So I'd like to push for settling with a state that makes it easier to refactor to move into a different state without introducing breaking changes, to allow us to merge this PR.
   
   I think having it as a separate module is fine, and it'll also be relatively simple to get rid of it and move the integration tests into the runtime module in the future.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Implement OpaPolarisAuthorizer [polaris]

Posted by "snazy (via GitHub)" <gi...@apache.org>.
snazy commented on code in PR #2680:
URL: https://github.com/apache/polaris/pull/2680#discussion_r2443977897


##########
extensions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaAuthorizationConfig.java:
##########
@@ -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();

Review Comment:
   Yep.
   (Both values are always concatenated.)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@polaris.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org