This is an automated email from the ASF dual-hosted git repository.

sungwy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 26dcf0b401 core: Refactor PolarisAuthorizer SPI (Phase 1): new 
interfaces and utilities (#3760)
26dcf0b401 is described below

commit 26dcf0b401bc6ee5eb6971bd4ef9edb0cfd2675b
Author: Sung Yun <[email protected]>
AuthorDate: Fri Feb 27 19:26:13 2026 -0500

    core: Refactor PolarisAuthorizer SPI (Phase 1): new interfaces and 
utilities (#3760)
    
    * refactoring to PolarisAuthorizer SPI
    
    * spotless
    
    * request scoped CDI bean RequestAuthorizationState
    
    * AuthorizationDecision
    
    * update resolveSelection logic
    
    * docstring
    
    * introduce guards in resolver when Resolvables were not selected
    
    * Resolver guard fixes
    
    * spotless
    
    * adopt feedback - thanks dmitri
    
    * adopt feedback and add tests
    
    * adopt review feedback
---
 .../extension/auth/opa/OpaPolarisAuthorizer.java   |  17 +++
 .../polaris/core/auth/AuthorizationDecision.java   |  44 ++++++
 .../polaris/core/auth/AuthorizationRequest.java    | 109 +++++++++++++++
 .../polaris/core/auth/AuthorizationState.java      |  53 +++++++
 .../core/auth/AuthorizationTargetBinding.java      |  56 ++++++++
 .../polaris/core/auth/PolarisAuthorizer.java       |  36 +++++
 .../polaris/core/auth/PolarisAuthorizerImpl.java   |  14 ++
 .../apache/polaris/core/auth/PolarisSecurable.java |  47 +++++++
 .../resolver/PolarisResolutionManifest.java        |  16 +++
 .../core/persistence/resolver/Resolvable.java      |  44 ++++++
 .../core/persistence/resolver/Resolver.java        | 153 ++++++++++++++++-----
 .../core/auth/AuthorizationRequestTest.java        |  88 ++++++++++++
 .../polaris/core/persistence/ResolverTest.java     | 118 ++++++++++++++++
 .../polaris/service/config/ServiceProducers.java   |   7 +
 14 files changed, 765 insertions(+), 37 deletions(-)

diff --git 
a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java
 
b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java
index 6c216121ef..bd26e92595 100644
--- 
a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java
+++ 
b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java
@@ -41,6 +41,9 @@ import org.apache.hc.core5.http.io.HttpClientResponseHandler;
 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.AuthorizationDecision;
+import org.apache.polaris.core.auth.AuthorizationRequest;
+import org.apache.polaris.core.auth.AuthorizationState;
 import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.auth.PolarisPrincipal;
@@ -95,6 +98,20 @@ class OpaPolarisAuthorizer implements PolarisAuthorizer {
     this.objectMapper = objectMapper;
   }
 
+  @Override
+  public void resolveAuthorizationInputs(
+      @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest 
request) {
+    throw new UnsupportedOperationException(
+        "resolveAuthorizationInputs is not implemented yet for 
OpaPolarisAuthorizer");
+  }
+
+  @Override
+  public AuthorizationDecision authorize(
+      @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest 
request) {
+    throw new UnsupportedOperationException(
+        "authorize is not implemented yet for OpaPolarisAuthorizer");
+  }
+
   /**
    * Authorizes a single target and secondary entity for the given principal 
and operation.
    *
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationDecision.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationDecision.java
new file mode 100644
index 0000000000..db5b24806b
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationDecision.java
@@ -0,0 +1,44 @@
+/*
+ * 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.util.Optional;
+import org.apache.polaris.immutables.PolarisImmutable;
+import org.immutables.value.Value;
+
+/** Authorization decision returned by authorizer implementations. */
+@PolarisImmutable
+public interface AuthorizationDecision {
+  AuthorizationDecision ALLOW = ImmutableAuthorizationDecision.of(true, 
Optional.empty());
+
+  static AuthorizationDecision allow() {
+    return ALLOW;
+  }
+
+  static AuthorizationDecision deny(@Nullable String message) {
+    return ImmutableAuthorizationDecision.of(false, 
Optional.ofNullable(message));
+  }
+
+  @Value.Parameter(order = 1)
+  boolean isAllowed();
+
+  @Value.Parameter(order = 2)
+  Optional<String> getMessage();
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java
new file mode 100644
index 0000000000..c02432dc0d
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.Nonnull;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.polaris.core.entity.PolarisEntityType;
+import org.apache.polaris.immutables.PolarisImmutable;
+import org.immutables.value.Value;
+
+/**
+ * Authorization request inputs for pre-authorization and core authorization.
+ *
+ * <p>This wrapper keeps authorization inputs together and conveys the intent 
to be authorized via
+ * {@link AuthorizationTargetBinding} target bindings.
+ */
+@PolarisImmutable
+public interface AuthorizationRequest {
+  static AuthorizationRequest of(
+      @Nonnull PolarisPrincipal principal,
+      @Nonnull PolarisAuthorizableOperation operation,
+      @Nonnull List<AuthorizationTargetBinding> targetBindings) {
+    return ImmutableAuthorizationRequest.builder()
+        .principal(principal)
+        .operation(operation)
+        .targetBindings(targetBindings)
+        .build();
+  }
+
+  /** Returns the principal requesting authorization. */
+  @Nonnull
+  PolarisPrincipal getPrincipal();
+
+  /** Returns the operation being authorized. */
+  @Nonnull
+  PolarisAuthorizableOperation getOperation();
+
+  /** Returns the target/secondary target bindings. */
+  @Nonnull
+  List<AuthorizationTargetBinding> getTargetBindings();
+
+  /**
+   * Returns the primary target securables, if any.
+   *
+   * <p>Compatibility accessor derived from {@link #getTargetBindings()}.
+   */
+  @Nonnull
+  @Value.Derived
+  default List<PolarisSecurable> getTargets() {
+    return 
getTargetBindings().stream().map(AuthorizationTargetBinding::getTarget).toList();
+  }
+
+  /**
+   * Returns secondary securables, if any.
+   *
+   * <p>Compatibility accessor derived from {@link #getTargetBindings()}.
+   */
+  @Nonnull
+  @Value.Derived
+  default List<PolarisSecurable> getSecondaries() {
+    List<PolarisSecurable> secondaries = new ArrayList<>();
+    for (AuthorizationTargetBinding targetBinding : getTargetBindings()) {
+      if (targetBinding.getSecondary() != null) {
+        secondaries.add(targetBinding.getSecondary());
+      }
+    }
+    return secondaries;
+  }
+
+  default boolean hasSecurableType(PolarisEntityType... types) {
+    for (AuthorizationTargetBinding targetBinding : getTargetBindings()) {
+      if (containsType(targetBinding.getTarget(), types)) {
+        return true;
+      }
+      if (targetBinding.getSecondary() != null
+          && containsType(targetBinding.getSecondary(), types)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  static boolean containsType(PolarisSecurable securable, PolarisEntityType... 
types) {
+    PolarisEntityType entityType = securable.getEntityType();
+    for (PolarisEntityType type : types) {
+      if (entityType == type) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationState.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationState.java
new file mode 100644
index 0000000000..84213da6cb
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationState.java
@@ -0,0 +1,53 @@
+/*
+ * 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.Nonnull;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest;
+
+/**
+ * Request-scoped authorization state shared across authorization phases.
+ *
+ * <p>Used to carry authorization-specific data, such as a {@link 
PolarisResolutionManifest} created
+ * by the caller and populated by the authorizer.
+ */
+public class AuthorizationState {
+  private final AtomicReference<PolarisResolutionManifest> resolutionManifest =
+      new AtomicReference<>();
+
+  /** Returns the request-scoped resolution manifest used for authorization. */
+  @Nonnull
+  public PolarisResolutionManifest getResolutionManifest() {
+    PolarisResolutionManifest manifest = resolutionManifest.get();
+    if (manifest == null) {
+      throw new IllegalStateException("AuthorizationState resolution manifest 
is not set");
+    }
+    return manifest;
+  }
+
+  /** Sets the request-scoped resolution manifest used for authorization. */
+  public void setResolutionManifest(@Nonnull PolarisResolutionManifest 
resolutionManifest) {
+    Objects.requireNonNull(resolutionManifest, "resolutionManifest");
+    if (!this.resolutionManifest.compareAndSet(null, resolutionManifest)) {
+      throw new IllegalStateException("AuthorizationState resolution manifest 
already set");
+    }
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationTargetBinding.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationTargetBinding.java
new file mode 100644
index 0000000000..2e5ae4ab7a
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationTargetBinding.java
@@ -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.
+ */
+package org.apache.polaris.core.auth;
+
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import org.apache.polaris.immutables.PolarisImmutable;
+
+/** A resource binding containing a primary target and optional secondary. */
+@PolarisImmutable
+public interface AuthorizationTargetBinding {
+  static AuthorizationTargetBinding of(
+      @Nonnull PolarisSecurable target, @Nullable PolarisSecurable secondary) {
+    return ImmutableAuthorizationTargetBinding.builder()
+        .target(target)
+        .secondary(secondary)
+        .build();
+  }
+
+  /** Returns the primary target securable for the binding. */
+  @Nonnull
+  PolarisSecurable getTarget();
+
+  /**
+   * Returns the optional secondary securable associated with the target.
+   *
+   * <p>Secondaries are related resources needed to evaluate the authorization 
decision but are not
+   * the direct object of the operation. Examples in current Polaris 
authorization flows include:
+   *
+   * <ul>
+   *   <li>Table rename: the destination namespace (target is the source 
table).
+   *   <li>Role grants: the grantee role/principal (target may be the role or 
the resource being
+   *       granted on).
+   *   <li>Policy attach/detach: the catalog/namespace/table being attached to 
(target is the
+   *       policy).
+   * </ul>
+   */
+  @Nullable
+  PolarisSecurable getSecondary();
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java
index 55c3792067..975eae5f03 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java
@@ -22,11 +22,47 @@ import jakarta.annotation.Nonnull;
 import jakarta.annotation.Nullable;
 import java.util.List;
 import java.util.Set;
+import org.apache.iceberg.exceptions.ForbiddenException;
 import org.apache.polaris.core.entity.PolarisBaseEntity;
 import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
 
 /** Interface for invoking authorization checks. */
 public interface PolarisAuthorizer {
+  /**
+   * Resolve authorizer-specific inputs before authorization.
+   *
+   * <p>Implementations may resolve only the entities required for the request 
(for example, the
+   * caller principal, principal roles, catalog roles, and requested targets) 
and store that state
+   * in {@link AuthorizationState}.
+   *
+   * <p>This method should not perform authorization decisions directly.
+   */
+  void resolveAuthorizationInputs(
+      @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest 
request);
+
+  /**
+   * Core authorization entry point for the new SPI.
+   *
+   * <p>Implementations should rely on any required state in {@link 
AuthorizationState} and the
+   * intent captured by {@link AuthorizationRequest} (principal, operation, 
and target securables).
+   */
+  @Nonnull
+  AuthorizationDecision authorize(
+      @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest 
request);
+
+  /**
+   * Convenience method that throws a {@link ForbiddenException} when 
authorization is denied.
+   *
+   * <p>Implementations should provide allow/deny decisions via {@link 
#authorize}.
+   */
+  default void authorizeOrThrow(
+      @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest 
request) {
+    AuthorizationDecision decision = authorize(authzState, request);
+    if (!decision.isAllowed()) {
+      String message = decision.getMessage().orElse("Authorization denied");
+      throw new ForbiddenException("%s", message);
+    }
+  }
 
   void authorizeOrThrow(
       @Nonnull PolarisPrincipal polarisPrincipal,
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java
index 635f255a8e..d878577f58 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java
@@ -744,6 +744,20 @@ public class PolarisAuthorizerImpl implements 
PolarisAuthorizer {
     this.realmConfig = realmConfig;
   }
 
+  @Override
+  public void resolveAuthorizationInputs(
+      @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest 
request) {
+    throw new UnsupportedOperationException(
+        "resolveAuthorizationInputs is not implemented yet for 
PolarisAuthorizerImpl");
+  }
+
+  @Override
+  public AuthorizationDecision authorize(
+      @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest 
request) {
+    throw new UnsupportedOperationException(
+        "authorize is not implemented yet for PolarisAuthorizerImpl");
+  }
+
   /**
    * Checks whether the {@code grantedPrivilege} is sufficient to confer 
{@code desiredPrivilege},
    * assuming the privileges are referring to the same securable object. In 
other words, whether the
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java
new file mode 100644
index 0000000000..739815fab9
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java
@@ -0,0 +1,47 @@
+/*
+ * 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.Nonnull;
+import java.util.List;
+import org.apache.polaris.core.entity.PolarisEntityType;
+import org.apache.polaris.immutables.PolarisImmutable;
+
+/**
+ * Intent-only target reference for authorization decisions.
+ *
+ * <p>Represents the target name and entity type without any resolved RBAC 
state. This captures the
+ * pure authorization intent of a request. Callers should provide the full 
name path as {@link
+ * #getNameParts()} for hierarchical entities.
+ */
+@PolarisImmutable
+public interface PolarisSecurable {
+  static PolarisSecurable of(
+      @Nonnull PolarisEntityType entityType, @Nonnull List<String> nameParts) {
+    return 
ImmutablePolarisSecurable.builder().entityType(entityType).nameParts(nameParts).build();
+  }
+
+  /** Returns the entity type of the securable. */
+  @Nonnull
+  PolarisEntityType getEntityType();
+
+  /** Returns the name parts that identify the securable in hierarchical 
order. */
+  @Nonnull
+  List<String> getNameParts();
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifest.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifest.java
index 31d95e7bba..a3965bd4f1 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifest.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifest.java
@@ -143,6 +143,22 @@ public class PolarisResolutionManifest implements 
PolarisResolutionManifestCatal
     return primaryResolverStatus;
   }
 
+  /**
+   * Resolves explicitly requested components.
+   *
+   * <p>Selections control which resolver components are executed. Callers are 
expected to add paths
+   * or top-level entity names before invoking this method.
+   */
+  public ResolverStatus resolveSelections(Set<Resolvable> selections) {
+    diagnostics.checkNotNull(selections, "resolver_selections_is_null");
+    primaryResolverStatus = primaryResolver.resolveSelections(selections);
+    diagnostics.check(
+        primaryResolverStatus.getStatus()
+            != ResolverStatus.StatusEnum.CALLER_PRINCIPAL_DOES_NOT_EXIST,
+        "caller_principal_does_not_exist_at_resolution_time");
+    return primaryResolverStatus;
+  }
+
   public boolean getIsPassthroughFacade() {
     return primaryResolver.getIsPassthroughFacade();
   }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolvable.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolvable.java
new file mode 100644
index 0000000000..28f3f09b9c
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolvable.java
@@ -0,0 +1,44 @@
+/*
+ * 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.persistence.resolver;
+
+/**
+ * Explicit resolution components for {@link
+ * PolarisResolutionManifest#resolveSelections(java.util.Set)}.
+ *
+ * <p>Selections control which entity groups the resolver should fetch for a 
request.
+ */
+public enum Resolvable {
+  /** Resolve the authenticated caller principal entity. */
+  CALLER_PRINCIPAL,
+  /** Resolve the caller's activated principal-role entities. */
+  CALLER_PRINCIPAL_ROLES,
+  /** Resolve catalog-role entities (e.g., roles attached in the reference 
catalog). */
+  CATALOG_ROLES,
+  /** Resolve the reference catalog entity. */
+  REFERENCE_CATALOG,
+  /** Resolve explicitly registered paths (via addPath/addPassthroughPath). */
+  REQUESTED_PATHS,
+  /**
+   * Resolve any additional top-level entities explicitly registered via 
addTopLevelName, such as
+   * catalog/principal/principal-role names used for authorization beyond the 
caller and reference
+   * catalog.
+   */
+  REQUESTED_TOP_LEVEL_ENTITIES
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java
index 6d7f274e0c..532e9aa69e 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java
@@ -238,6 +238,23 @@ public class Resolver {
    *     getResolvedXYZ() method can be called.
    */
   public ResolverStatus resolveAll() {
+    return resolveWithPlan(ResolvePlan.all(referenceCatalogName));
+  }
+
+  /**
+   * Run the resolution process for a subset of resolvables.
+   *
+   * @param selections explicit selection of resolvables to resolve
+   * @return the status of the resolver. If success, the requested entities 
have been resolved and
+   *     the corresponding getResolvedXYZ() methods can be called.
+   */
+  public ResolverStatus resolveSelections(@Nonnull Set<Resolvable> selections) 
{
+    diagnostics.checkNotNull(selections, "resolver_selections_is_null");
+    diagnostics.check(!selections.isEmpty(), "resolver_selections_is_empty");
+    return resolveWithPlan(ResolvePlan.fromSelections(selections, 
referenceCatalogName));
+  }
+
+  private ResolverStatus resolveWithPlan(ResolvePlan plan) {
     // can only be called if the resolver has not yet been called
     this.diagnostics.check(resolverStatus == null, "resolver_called");
 
@@ -247,9 +264,9 @@ public class Resolver {
     int count = 0;
     ResolverStatus status;
     do {
-      status = runResolvePass();
+      status = runResolvePass(plan);
       count++;
-    } while (status == null && ++count < 1000);
+    } while (status == null && count < 1000);
 
     // assert if status is null
     this.diagnostics.checkNotNull(status, "cannot_resolve_all_entities");
@@ -274,6 +291,7 @@ public class Resolver {
     this.diagnostics.check(
         resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS,
         "resolver_must_be_successful");
+    this.diagnostics.check(resolvedCallerPrincipal != null, 
"caller_principal_not_resolved");
 
     return resolvedCallerPrincipal;
   }
@@ -287,7 +305,6 @@ public class Resolver {
     this.diagnostics.check(
         resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS,
         "resolver_must_be_successful");
-
     return resolvedCallerPrincipalRoles;
   }
 
@@ -382,6 +399,7 @@ public class Resolver {
     if (entityType.isTopLevel()) {
       return this.resolvedEntriesByName.get(new 
EntityCacheByNameKey(entityType, entityName));
     } else {
+      diagnostics.checkNotNull(resolvedReferenceCatalog, 
"reference_catalog_not_resolved");
       long catalogId = this.resolvedReferenceCatalog.getEntity().getId();
       return this.resolvedEntriesByName.get(
           new EntityCacheByNameKey(catalogId, catalogId, entityType, 
entityName));
@@ -393,7 +411,7 @@ public class Resolver {
    *
    * @return status of the resolve pass
    */
-  private ResolverStatus runResolvePass() {
+  private ResolverStatus runResolvePass(ResolvePlan plan) {
 
     // we will resolve those again
     this.resolvedCallerPrincipal = null;
@@ -409,23 +427,30 @@ public class Resolver {
     List<ResolvedPolarisEntity> toValidate = new ArrayList<>();
 
     // first resolve the principal and determine the set of activated 
principal roles
-    ResolverStatus status = 
this.resolveCallerPrincipalAndPrincipalRoles(toValidate);
+    ResolverStatus status =
+        plan.resolveCallerPrincipal()
+            ? this.resolveCallerPrincipalAndPrincipalRoles(toValidate, 
plan.resolvePrincipalRoles())
+            : new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS);
 
     // if success, continue resolving
     if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS) {
       // then resolve the reference catalog if one was specified
-      if (this.referenceCatalogName != null) {
-        status = this.resolveReferenceCatalog(toValidate, 
this.referenceCatalogName);
+      if (plan.resolveReferenceCatalog()) {
+        this.diagnostics.checkNotNull(this.referenceCatalogName, 
"reference_catalog_expected");
+        status =
+            this.resolveReferenceCatalog(
+                toValidate, this.referenceCatalogName, 
plan.resolveCatalogRoles());
       }
 
       // if success, continue resolving
       if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS) {
         // then resolve all the additional entities we were asked to resolve
-        status = this.resolveEntities(toValidate, this.entitiesToResolve);
+        if (plan.resolveTopLevelEntities()) {
+          status = this.resolveEntities(toValidate, this.entitiesToResolve);
+        }
 
         // if success, continue resolving
-        if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS
-            && this.referenceCatalogName != null) {
+        if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS && 
plan.resolvePaths()) {
           // finally, resolve all paths we need to resolve
           status = this.resolvePaths(toValidate, this.pathsToResolve);
         }
@@ -679,7 +704,6 @@ public class Resolver {
    */
   private ResolverStatus resolvePaths(
       List<ResolvedPolarisEntity> toValidate, List<ResolverPath> 
pathsToResolve) {
-
     // id of the catalog for all these paths
     final long catalogId = this.resolvedReferenceCatalog.getEntity().getId();
 
@@ -742,7 +766,7 @@ public class Resolver {
    * @return the status of resolution
    */
   private ResolverStatus resolveCallerPrincipalAndPrincipalRoles(
-      List<ResolvedPolarisEntity> toValidate) {
+      List<ResolvedPolarisEntity> toValidate, boolean resolvePrincipalRoles) {
 
     // resolve the principal, by name or id
     this.resolvedCallerPrincipal =
@@ -755,10 +779,14 @@ public class Resolver {
     }
 
     // activate all principal roles specified in the authenticated principal
-    resolvedCallerPrincipalRoles =
-        this.polarisPrincipal.getRoles().isEmpty()
-            ? resolveAllPrincipalRoles(toValidate, resolvedCallerPrincipal)
-            : resolvePrincipalRolesByName(toValidate, 
this.polarisPrincipal.getRoles());
+    if (resolvePrincipalRoles) {
+      resolvedCallerPrincipalRoles =
+          this.polarisPrincipal.getRoles().isEmpty()
+              ? resolveAllPrincipalRoles(toValidate, resolvedCallerPrincipal)
+              : resolvePrincipalRolesByName(toValidate, 
this.polarisPrincipal.getRoles());
+    } else {
+      resolvedCallerPrincipalRoles = new ArrayList<>();
+    }
 
     // total success
     return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS);
@@ -811,7 +839,9 @@ public class Resolver {
    * @return the status of resolution
    */
   private ResolverStatus resolveReferenceCatalog(
-      @Nonnull List<ResolvedPolarisEntity> toValidate, @Nonnull String 
referenceCatalogName) {
+      @Nonnull List<ResolvedPolarisEntity> toValidate,
+      @Nonnull String referenceCatalogName,
+      boolean resolveCatalogRoles) {
     // resolve the catalog
     this.resolvedReferenceCatalog =
         this.resolveByName(toValidate, PolarisEntityType.CATALOG, 
referenceCatalogName);
@@ -822,26 +852,28 @@ public class Resolver {
       return new ResolverStatus(PolarisEntityType.CATALOG, 
this.referenceCatalogName);
     }
 
-    // determine the set of catalog roles which have been activated
-    long catalogId = this.resolvedReferenceCatalog.getEntity().getId();
-    for (ResolvedPolarisEntity principalRole : resolvedCallerPrincipalRoles) {
-      for (PolarisGrantRecord grantRecord : 
principalRole.getGrantRecordsAsGrantee()) {
-        // the securable is a catalog role belonging to
-        if (grantRecord.getPrivilegeCode() == 
PolarisPrivilege.CATALOG_ROLE_USAGE.getCode()
-            && grantRecord.getSecurableCatalogId() == catalogId) {
-          // the id of the catalog role
-          long catalogRoleId = grantRecord.getSecurableId();
-
-          // skip if it has already been added
-          if (!this.resolvedCatalogRoles.containsKey(catalogRoleId)) {
-            // see if this catalog can be resolved
-            ResolvedPolarisEntity catalogRole =
-                this.resolveById(
-                    toValidate, PolarisEntityType.CATALOG_ROLE, catalogId, 
catalogRoleId);
-
-            // if found and not dropped, add it to the list of activated 
catalog roles
-            if (catalogRole != null && !catalogRole.getEntity().isDropped()) {
-              this.resolvedCatalogRoles.put(catalogRoleId, catalogRole);
+    if (resolveCatalogRoles) {
+      // determine the set of catalog roles which have been activated
+      long catalogId = this.resolvedReferenceCatalog.getEntity().getId();
+      for (ResolvedPolarisEntity principalRole : resolvedCallerPrincipalRoles) 
{
+        for (PolarisGrantRecord grantRecord : 
principalRole.getGrantRecordsAsGrantee()) {
+          // the securable is a catalog role belonging to
+          if (grantRecord.getPrivilegeCode() == 
PolarisPrivilege.CATALOG_ROLE_USAGE.getCode()
+              && grantRecord.getSecurableCatalogId() == catalogId) {
+            // the id of the catalog role
+            long catalogRoleId = grantRecord.getSecurableId();
+
+            // skip if it has already been added
+            if (!this.resolvedCatalogRoles.containsKey(catalogRoleId)) {
+              // see if this catalog can be resolved
+              ResolvedPolarisEntity catalogRole =
+                  this.resolveById(
+                      toValidate, PolarisEntityType.CATALOG_ROLE, catalogId, 
catalogRoleId);
+
+              // if found and not dropped, add it to the list of activated 
catalog roles
+              if (catalogRole != null && !catalogRole.getEntity().isDropped()) 
{
+                this.resolvedCatalogRoles.put(catalogRoleId, catalogRole);
+              }
             }
           }
         }
@@ -856,6 +888,53 @@ public class Resolver {
     return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS);
   }
 
+  private static record ResolvePlan(
+      boolean resolveCallerPrincipal,
+      boolean resolvePrincipalRoles,
+      boolean resolveReferenceCatalog,
+      boolean resolveCatalogRoles,
+      boolean resolveTopLevelEntities,
+      boolean resolvePaths) {
+
+    private static ResolvePlan all(@Nullable String referenceCatalogName) {
+      boolean hasReferenceCatalog = referenceCatalogName != null;
+      return new ResolvePlan(
+          true, true, hasReferenceCatalog, hasReferenceCatalog, true, 
hasReferenceCatalog);
+    }
+
+    private static ResolvePlan fromSelections(
+        Set<Resolvable> selections, @Nullable String referenceCatalogName) {
+      boolean resolvePaths = selections.contains(Resolvable.REQUESTED_PATHS);
+      boolean resolveTopLevelEntities =
+          selections.contains(Resolvable.REQUESTED_TOP_LEVEL_ENTITIES);
+      boolean resolveCatalogRoles = 
selections.contains(Resolvable.CATALOG_ROLES);
+      // Principal roles depend on resolving the caller principal, and catalog 
roles depend on
+      // principal roles. Only those selections require caller principal 
resolution.
+      boolean resolvePrincipalRoles =
+          selections.contains(Resolvable.CALLER_PRINCIPAL_ROLES) || 
resolveCatalogRoles;
+      // Reference catalog is required for path resolution and catalog role 
expansion.
+      boolean resolveReferenceCatalog =
+          selections.contains(Resolvable.REFERENCE_CATALOG) || resolvePaths || 
resolveCatalogRoles;
+      boolean resolveCallerPrincipal =
+          selections.contains(Resolvable.CALLER_PRINCIPAL) || 
resolvePrincipalRoles;
+
+      boolean hasReferenceCatalog = referenceCatalogName != null;
+      if (!hasReferenceCatalog && resolveReferenceCatalog) {
+        throw new IllegalArgumentException(
+            "Reference-catalog-dependent selections were requested, but no 
reference catalog name "
+                + "was provided");
+      }
+
+      return new ResolvePlan(
+          resolveCallerPrincipal,
+          resolvePrincipalRoles,
+          resolveReferenceCatalog,
+          resolveCatalogRoles,
+          resolveTopLevelEntities,
+          resolvePaths);
+    }
+  }
+
   /**
    * Add a resolved entity to the current resolution collection's set of 
resolved entities
    *
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java
new file mode 100644
index 0000000000..b46cb906a1
--- /dev/null
+++ 
b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.polaris.core.entity.PolarisEntityType;
+import org.junit.jupiter.api.Test;
+
+public class AuthorizationRequestTest {
+
+  @Test
+  void hasSecurableTypeReturnsTrueForPrincipalTarget() {
+    AuthorizationRequest request =
+        AuthorizationRequest.of(
+            PolarisPrincipal.of("alice", Map.of(), Set.of("role")),
+            PolarisAuthorizableOperation.LOAD_TABLE,
+            List.of(
+                AuthorizationTargetBinding.of(
+                    PolarisSecurable.of(PolarisEntityType.PRINCIPAL, 
List.of("alice")), null)));
+
+    assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL)).isTrue();
+  }
+
+  @Test
+  void hasSecurableTypeReturnsTrueForPrincipalRoleSecondary() {
+    AuthorizationRequest request =
+        AuthorizationRequest.of(
+            PolarisPrincipal.of("alice", Map.of(), Set.of("role")),
+            PolarisAuthorizableOperation.ASSIGN_PRINCIPAL_ROLE,
+            List.of(
+                AuthorizationTargetBinding.of(
+                    PolarisSecurable.of(PolarisEntityType.PRINCIPAL, 
List.of("alice")),
+                    PolarisSecurable.of(
+                        PolarisEntityType.PRINCIPAL_ROLE, 
List.of("analytics-admin")))));
+
+    
assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL_ROLE)).isTrue();
+  }
+
+  @Test
+  void hasSecurableTypeReturnsTrueForCatalogRoleAcrossMultipleBindings() {
+    AuthorizationRequest request =
+        AuthorizationRequest.of(
+            PolarisPrincipal.of("alice", Map.of(), Set.of("role")),
+            PolarisAuthorizableOperation.ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE,
+            List.of(
+                AuthorizationTargetBinding.of(
+                    PolarisSecurable.of(PolarisEntityType.NAMESPACE, 
List.of("catalog", "ns")),
+                    null),
+                AuthorizationTargetBinding.of(
+                    PolarisSecurable.of(PolarisEntityType.CATALOG, 
List.of("catalog")),
+                    PolarisSecurable.of(PolarisEntityType.CATALOG_ROLE, 
List.of("catalog-role")))));
+
+    
assertThat(request.hasSecurableType(PolarisEntityType.CATALOG_ROLE)).isTrue();
+  }
+
+  @Test
+  void hasSecurableTypeReturnsFalseWhenTypeAbsent() {
+    AuthorizationRequest request =
+        AuthorizationRequest.of(
+            PolarisPrincipal.of("alice", Map.of(), Set.of("role")),
+            PolarisAuthorizableOperation.LOAD_VIEW,
+            List.of(
+                AuthorizationTargetBinding.of(
+                    PolarisSecurable.of(PolarisEntityType.CATALOG, 
List.of("catalog")), null)));
+
+    
assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL_ROLE)).isFalse();
+  }
+}
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java
index 3c8633a827..3af68107a2 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java
@@ -21,10 +21,21 @@ package org.apache.polaris.core.persistence;
 import static 
org.apache.polaris.core.persistence.PrincipalSecretsGenerator.RANDOM_SECRETS;
 
 import java.time.Clock;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import org.apache.polaris.core.PolarisCallContext;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.entity.PolarisEntityType;
+import org.apache.polaris.core.persistence.resolver.Resolvable;
+import org.apache.polaris.core.persistence.resolver.Resolver;
+import org.apache.polaris.core.persistence.resolver.ResolverPath;
+import org.apache.polaris.core.persistence.resolver.ResolverStatus;
 import 
org.apache.polaris.core.persistence.transactional.TransactionalMetaStoreManagerImpl;
 import org.apache.polaris.core.persistence.transactional.TreeMapMetaStore;
 import 
org.apache.polaris.core.persistence.transactional.TreeMapTransactionalPersistenceImpl;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
 public class ResolverTest extends BaseResolverTest {
@@ -62,4 +73,111 @@ public class ResolverTest extends BaseResolverTest {
     }
     return tm;
   }
+
+  @Test
+  public void testResolveSelectionsSkipsCallerPrincipalForReferenceCatalog() {
+    Resolver resolver =
+        new Resolver(
+            diagServices,
+            callCtx(),
+            metaStoreManager(),
+            PolarisPrincipal.of("missing", Map.of(), Set.of()),
+            null,
+            "test");
+    ResolverStatus status = 
resolver.resolveSelections(Set.of(Resolvable.REFERENCE_CATALOG));
+    
Assertions.assertThat(status.getStatus()).isEqualTo(ResolverStatus.StatusEnum.SUCCESS);
+  }
+
+  @Test
+  public void testResolveSelectionsSkipsCallerPrincipalForRequestedPaths() {
+    Resolver pathResolver =
+        new Resolver(
+            diagServices,
+            callCtx(),
+            metaStoreManager(),
+            PolarisPrincipal.of("missing", Map.of(), Set.of()),
+            null,
+            "test");
+    pathResolver.addPath(new ResolverPath(List.of("N1"), 
PolarisEntityType.NAMESPACE));
+    ResolverStatus pathStatus = 
pathResolver.resolveSelections(Set.of(Resolvable.REQUESTED_PATHS));
+    
Assertions.assertThat(pathStatus.getStatus()).isEqualTo(ResolverStatus.StatusEnum.SUCCESS);
+  }
+
+  @Test
+  public void 
testResolveSelectionsSkipsCallerPrincipalForRequestedTopLevelEntities() {
+    Resolver entityResolver =
+        new Resolver(
+            diagServices,
+            callCtx(),
+            metaStoreManager(),
+            PolarisPrincipal.of("missing", Map.of(), Set.of()),
+            null,
+            null);
+    entityResolver.addEntityByName(PolarisEntityType.PRINCIPAL, "P1");
+    ResolverStatus entityStatus =
+        
entityResolver.resolveSelections(Set.of(Resolvable.REQUESTED_TOP_LEVEL_ENTITIES));
+    
Assertions.assertThat(entityStatus.getStatus()).isEqualTo(ResolverStatus.StatusEnum.SUCCESS);
+  }
+
+  @Test
+  public void testResolveSelectionsRequiresCallerPrincipalForCatalogRoles() {
+    Resolver resolver =
+        new Resolver(
+            diagServices,
+            callCtx(),
+            metaStoreManager(),
+            PolarisPrincipal.of("missing", Map.of(), Set.of()),
+            null,
+            "test");
+    ResolverStatus status = 
resolver.resolveSelections(Set.of(Resolvable.CATALOG_ROLES));
+    Assertions.assertThat(status.getStatus())
+        .isEqualTo(ResolverStatus.StatusEnum.CALLER_PRINCIPAL_DOES_NOT_EXIST);
+  }
+
+  @Test
+  public void testResolveSelectionsRequiresCallerPrincipalForCallerPrincipal() 
{
+    Resolver resolver =
+        new Resolver(
+            diagServices,
+            callCtx(),
+            metaStoreManager(),
+            PolarisPrincipal.of("missing", Map.of(), Set.of()),
+            null,
+            "test");
+    ResolverStatus status = 
resolver.resolveSelections(Set.of(Resolvable.CALLER_PRINCIPAL));
+    Assertions.assertThat(status.getStatus())
+        .isEqualTo(ResolverStatus.StatusEnum.CALLER_PRINCIPAL_DOES_NOT_EXIST);
+  }
+
+  @Test
+  public void 
testResolveSelectionsRequiresCallerPrincipalForCallerPrincipalRoles() {
+    Resolver resolver =
+        new Resolver(
+            diagServices,
+            callCtx(),
+            metaStoreManager(),
+            PolarisPrincipal.of("missing", Map.of(), Set.of()),
+            null,
+            "test");
+    ResolverStatus status = 
resolver.resolveSelections(Set.of(Resolvable.CALLER_PRINCIPAL_ROLES));
+    Assertions.assertThat(status.getStatus())
+        .isEqualTo(ResolverStatus.StatusEnum.CALLER_PRINCIPAL_DOES_NOT_EXIST);
+  }
+
+  @Test
+  public void testResolveSelectionsThrowsOnGetResolvedCallerPrincipal() {
+    Resolver resolver =
+        new Resolver(
+            diagServices,
+            callCtx(),
+            metaStoreManager(),
+            PolarisPrincipal.of("missing", Map.of(), Set.of()),
+            null,
+            "test");
+    ResolverStatus status = 
resolver.resolveSelections(Set.of(Resolvable.REFERENCE_CATALOG));
+    
Assertions.assertThat(status.getStatus()).isEqualTo(ResolverStatus.StatusEnum.SUCCESS);
+    Assertions.assertThatThrownBy(resolver::getResolvedCallerPrincipal)
+        .isInstanceOf(IllegalStateException.class)
+        .hasMessageContaining("caller_principal_not_resolved");
+  }
 }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
index 8f906c6f99..688243171b 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
@@ -35,6 +35,7 @@ import java.util.stream.Collectors;
 import org.apache.polaris.core.PolarisCallContext;
 import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
 import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.auth.AuthorizationState;
 import org.apache.polaris.core.auth.DefaultPolarisAuthorizerFactory;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
@@ -164,6 +165,12 @@ public class ServiceProducers {
     return factory.create(realmConfig);
   }
 
+  @Produces
+  @RequestScoped
+  public AuthorizationState authorizationState() {
+    return new AuthorizationState();
+  }
+
   @Produces
   @RequestScoped
   public ResolverFactory resolverFactory(


Reply via email to