Refactor PolarisPrincipal to contain generic attributes#5085
Conversation
There was a problem hiding this comment.
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
PolarisPrincipalto exposeMap<String, Object> getAttributes()with standard attribute keys (principal entity + optional JWT). - Update authentication flow:
Authenticator/DefaultAuthenticatornow authenticate from a QuarkusSecurityIdentityand 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(). |
|
This change probably overshadows #5032 🤔 |
Yes, that's the intent 😄 |
|
|
||
| /** Returns the principal attributes. */ | ||
| @Value.Redacted | ||
| Map<String, Object> getAttributes(); |
There was a problem hiding this comment.
nit: maybe Map<String, ?>?
There was a problem hiding this comment.
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();There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
@adutra : This change may be worth mentioning in the CHANGELOG for downstream project visibility, WDYT?
There was a problem hiding this comment.
I was reluctant because there is no user-facing changes, only downstream-facing changes, but OK.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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());There was a problem hiding this comment.
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.
| } | ||
| } | ||
|
|
||
| private static boolean mustRotateCredentials(PolarisPrincipal principal) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
We were already exposing the entity's internal properties to the authorizers. This isn't new.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
@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: 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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
In the spirit of moving forward with follow-up tasks, I'm going to merge this PR now. Hope that's OK. |
Checklist
CHANGELOG.md(if needed)site/content/in-dev/unreleased(if needed)