Skip to content

Refactor PolarisPrincipal to contain generic attributes#5085

Merged
adutra merged 2 commits into
apache:mainfrom
adutra:auth-refactor2
Jul 21, 2026
Merged

Refactor PolarisPrincipal to contain generic attributes#5085
adutra merged 2 commits into
apache:mainfrom
adutra:auth-refactor2

Conversation

@adutra

@adutra adutra commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Checklist

  • 🛡️ Don't disclose security issues! (contact security@apache.org)
  • 🔗 Clearly explained why the changes are needed, or linked related issues: Fixes #
  • 🧪 Added/updated tests with good coverage, or manually tested (and explained how)
  • 💡 Added comments for complex logic
  • 🧾 Updated CHANGELOG.md (if needed)
  • 📚 Updated documentation in site/content/in-dev/unreleased (if needed)

Copilot AI review requested due to automatic review settings July 17, 2026 13:29
@github-project-automation github-project-automation Bot moved this to PRs In Progress in Basic Kanban Board Jul 17, 2026
@adutra adutra changed the title Auth refactor2 Refactor PolarisPrincipal to contain generic attributes Jul 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors PolarisPrincipal to carry a generic attributes map (instead of a string-only properties map and separate token), and updates the runtime auth flow and related tests to construct principals with these attributes (including storing the PrincipalEntity as an attribute).

Changes:

  • Refactor PolarisPrincipal to expose Map<String, Object> getAttributes() with standard attribute keys (principal entity + optional JWT).
  • Update authentication flow: Authenticator / DefaultAuthenticator now authenticate from a Quarkus SecurityIdentity and build principal attributes from identity + entity.
  • Update affected unit/integration tests and extension code to use the new PolarisPrincipal.of(name, attributes, roles) factory and updated error messaging.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java Update test fixture principal creation to populate principal attributes.
runtime/service/src/test/java/org/apache/polaris/service/catalog/RootPrincipalAugmentor.java Update test augmentor to create principals with attributes map.
runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java Update test principals to include PrincipalEntity attribute.
runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java Update authenticated root principal setup to use attributes.
runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java Update test principal construction for new PolarisPrincipal API.
runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractLocalIcebergCatalogViewTest.java Update authenticated root principal setup for new attributes-based principal.
runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java Update multiple test principal constructions to include attributes map + entity attribute.
runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java Update test wrapper principal creation for new principal API.
runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java Update authenticated root principal setup to include principal entity attribute.
runtime/service/src/test/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentorTest.java Update OIDC augmentor tests to reflect credential/token handling changes.
runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java Update tests for SecurityIdentity-based auth and new principal attributes behavior (incl. JWT attribute).
runtime/service/src/test/java/org/apache/polaris/service/auth/AuthenticatingAugmentorTest.java Update tests for new authenticator signature and identity augmentation behavior.
runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java Update authenticated root principal construction to include attributes.
runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java Update admin service tests to construct principals with attributes.
runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java Update test principal setup to include principal entity attribute.
runtime/service/src/main/java/org/apache/polaris/service/auth/PolarisCredential.java Remove token field from PolarisCredential factory/contract.
runtime/service/src/main/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentor.java Stop embedding JWT raw token into PolarisCredential; credential contains id/name/roles only.
runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java Authenticate using SecurityIdentity, build PolarisPrincipal with attributes (identity attrs + principal entity + optional JWT).
runtime/service/src/main/java/org/apache/polaris/service/auth/Authenticator.java Change authenticator contract to accept SecurityIdentity.
runtime/service/src/main/java/org/apache/polaris/service/auth/AuthenticatingAugmentor.java Simplify augmentation: authenticate via identity and add principal attributes onto the SecurityIdentity.
polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java Update test fixture principal creation to set principal entity attribute.
polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationPreConditionsTest.java Update tests to reflect new rotation-required check path and updated message text.
polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisPrincipal.java Introduce generic attributes map and standard attribute keys.
polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationPreConditions.java Switch rotation-required enforcement to inspect PrincipalEntity via principal attributes.
integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java Update expected error message string for rotation-required enforcement.
extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/utils/RangerUtils.java Update Ranger user-attribute extraction to use PrincipalEntity attribute instead of getProperties().

Comment thread polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisPrincipal.java Outdated
Comment thread runtime/service/src/main/java/org/apache/polaris/service/auth/Authenticator.java Outdated
@dimas-b

dimas-b commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

This change probably overshadows #5032 🤔

@adutra

adutra commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

This change probably overshadows #5032 🤔

Yes, that's the intent 😄

dimas-b
dimas-b previously approved these changes Jul 17, 2026

/** Returns the principal attributes. */
@Value.Redacted
Map<String, Object> getAttributes();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe Map<String, ?>?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning Map<String, ?> would make it hard to use the returned value, e.g. this code in AuthenticatingAugmentor wouldn't compile:

 return QuarkusSecurityIdentity.builder(identity)
        .setAnonymous(false)
        .setPrincipal(polarisPrincipal)
        .addRoles(polarisPrincipal.getRoles())
        .addAttributes(polarisPrincipal.getAttributes()) // doesn't compile
        .build();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: Map<String, ? super Object> should work, I hope 😅

Side question: do we really need PolarisPrincipal attributes as top-level attributes in QuarkusSecurityIdentity? Quarkus probably does not need them and Polaris code should probably go through PolarisPrincipal 🤔

static PolarisPrincipal of(String name, Map<String, String> properties, Set<String> roles) {
return of(name, properties, roles, Optional.empty());
}
String JWT_ATTRIBUTE_KEY = "org.apache.polaris.core.auth.JWT";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adutra : This change may be worth mentioning in the CHANGELOG for downstream project visibility, WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was reluctant because there is no user-facing changes, only downstream-facing changes, but OK.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine either way... but other contributors tend to mention java changes in CHANGELOG 🤔

Map<String, String> properties =
principal
.getAttribute(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, PrincipalEntity.class)
.map(PrincipalEntity::getInternalPropertiesAsMap)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we still need #5032 to forward user-level principal properties to authorizers... It will be a follow-up change. WDYT? CC: @iprithv

@adutra adutra Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we'd need #5032, authorizers can get the user-facing properties as follows :

principal
  .getAttribute(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, PrincipalEntity.class)
  .map(PrincipalEntity::getPropertiesAsMap)
  .orElse(Map.of());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I mean that #5032 actually forwards user-level principal properties to OPA and Ranger (IIRC), so some extra change will be required to keep that behaviour after this PR.

@github-project-automation github-project-automation Bot moved this from PRs In Progress to Ready to merge in Basic Kanban Board Jul 17, 2026
dimas-b
dimas-b previously approved these changes Jul 17, 2026
}
}

private static boolean mustRotateCredentials(PolarisPrincipal principal) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PolarisPrincipal's own javadoc says callers must never assume this attribute is present. But the new logic here mandate the property PRINCIPAL_ENTITY_ATTRIBUTE_KEY, otherwise, rotation will skip. I'm not sure the new behavior is correct. Maybe we should make PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE the first level property instead of embedded in PRINCIPAL_ENTITY_ATTRIBUTE_KEY. This is also related to my another comments. Directly embedding the whole Principal entity may expose too much internal state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the new logic here mandate the property PRINCIPAL_ENTITY_ATTRIBUTE_KEY, otherwise, rotation will skip. I'm not sure the new behavior is correct.

I think that's correct. If the principal entity does not exist, it's because the principal is fully external. In that case, Polaris cannot know if the credentials need rotation. It's the external IDP's responsibility to deny the token request from such a user.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Polaris should not interfere with token generation for external principals.

.log("Unable to authenticate user with token");
throw new ServiceFailureException("Unable to fetch principal entity");
.log("Unable to resolve principal entity from credentials");
throw new InternalServerErrorException("Unable to fetch principal entity");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to throw Iceberg's ServiceFailureException, which IcebergExceptionMapper maps to 503 (SERVICE_UNAVAILABLE). InternalServerErrorException is 500. So a transient metastore hiccup during auth flips from a retryable 503 to a non-retryable 500 for clients. Is the 500 intended? If so, it's worth a CHANGELOG line next to the 401 note.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! No, I intended to keep using 503 but used the wrong exception class. I will amend.


protected Map<String, Object> resolvePrincipalAttributes(
SecurityIdentity identity, PrincipalEntity principalEntity) {
ImmutableMap.Builder<String, Object> principalAttributes =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I'm wondering about is exposing the entire PrincipalEntity as an attribute. Do we really want authorizers to depend on our internal persistence model? The tighter coupling between the authorization logic and the metastore schema is concerning. It also makes it easier for authorizers to rely on fields that weren't intended to be part of the authorization contract.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't say that authorizers are now depending on our internal persistence model. They have internal access to the PrincipalEntity, but they don't have to send the whole entity to OPA or Ranger.

IMHO there is no tight coupling between the metastore schema and the authorizer here, but rather a lose one. Each authorizer is free to convert the entity to whatever representation fits best for their use cases.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want authorizers to depend on our internal persistence model

PrincipalEntity is part of the core entity model. I suppose "core" should be fine as a dependency in any Polaris extension.

I do not think PrincipalEntity is tied to persistence implementations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metastore schema coupling is one thing. Besides, are we conformable to expose the whole PrincipalEntity to authorizer? For example, it will automatically expose all internal properties to authorizer. Is that a expected behavior? What if some internal properties are not supposed to be exposed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were already exposing the entity's internal properties to the authorizers. This isn't new.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the context. I agree this is not necessarily a blocker for this PR, especially if internal properties were already exposed.

I still think the broader security question is valid. Passing the entire PrincipalEntity means any future internal property automatically becomes accessible to authorizers, and potentially to downstream systems such as OPA or Ranger. That makes it easier to expose sensitive or implementation specific fields unintentionally, and it also allows policies to depend on fields that are not part of a stable authorization contract.

It may be safer to define an explicit, allowlisted authorization view of the principal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authorizer implementations are capable of loading anything from Polaris Persistence. If this code is not to be trusted, it will require a substantial overhaul of the whole Authorizer framework, I think.

IMHO, Authorizer implementations can be trusted. They are "activated" by a deployment-time decision. Runtime actions cannot replace an Authorizer or alter how it processes data (assuming the impl. is correct by itself).

Explicit filtering of PrincipalEntity properties does make sense, though. However, I believe it is an Authorizer implementation concern. I'd propose to keep PrincipalEntity as a PolarisPrincipal attribute in this PR, but move actual processing of its attributes on the AuthZ side to another PR... possibly to #5032 and have a thorough review of that code there. WDYT?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Let's discuss this separately. I don't think it is purely an Authorizer implementation concern though. I think we should have a unified filter, instead of each authorizer does it.

@dimas-b

dimas-b commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@adutra : lots of merge conflicts popped up 😅

@adutra

adutra commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@adutra : lots of merge conflicts popped up 😅

Yup, I had to squash and rebase to fix all the conflicts.

FYI there is a new attribute now: org.apache.polaris.core.auth.PRINCIPAL_ROLE:ALL.

PTAL!

/** Returns how principal roles should be selected during role resolution. */
RoleSelection getRoleSelection();
/** Returns the attribute value associated with the given key, if any. */
default <T> Optional<T> getAttribute(String key, Class<T> type) {

@dimas-b dimas-b Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Late comment, but it might be worth defining a key class (or interface) with specific Class<X> value attributes for each standard key.... then getAttribute(PolarisPrincipalAttribute.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY) could immediately be scoped down to Boolean without a per-call type parameter.

Extensibility can be achieved by PrincipalAttribute defining type() and key() methods, which could be customized downstream (if required). I guess it could be an "immutable" interface with utility methods.

WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I thought about that and in fact, we already use this pattern for event attributes.

The reason why I went for a simpler, type-unsafe idiom is because this idiom is already used in SecurityIdentity, cf. getPrincipal(Class) and getCredential(Class). So I thought it would look familiar.

A tricky problem to solve though is how to capture the type information. org.apache.polaris.service.events.AttributeKey uses Guava's TypeToken for that. We cannot do that here since this is a public component in polaris-core, so It would be bad to leak a Guava type in the API.

How about we tackle this in a follow-up work? I think this PR is already quite big.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM 👍

@adutra

adutra commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

In the spirit of moving forward with follow-up tasks, I'm going to merge this PR now. Hope that's OK.

@adutra
adutra merged commit 43ab107 into apache:main Jul 21, 2026
25 checks passed
@adutra
adutra deleted the auth-refactor2 branch July 21, 2026 08:35
@github-project-automation github-project-automation Bot moved this from Ready to merge to Done in Basic Kanban Board Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants