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: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to