Skip to content

Introduce ExternalAuthenticator and isExternal flag on PolarisCredential#3250

Draft
sungwy wants to merge 1 commit intoapache:mainfrom
sungwy:ext-principal
Draft

Introduce ExternalAuthenticator and isExternal flag on PolarisCredential#3250
sungwy wants to merge 1 commit intoapache:mainfrom
sungwy:ext-principal

Conversation

@sungwy
Copy link
Contributor

@sungwy sungwy commented Dec 10, 2025

Draft PR to introduce an External mode in the PolarisPrincipal. The goal of this PR is to introduce an authentication flow that skips principal resolution in the metastore for enhanced compatibility with authentication -> authorization flows that rely on external IDPs and PDPs.

This PR is motivated by @adutra 's mailing list proposal

Checklist

  • 🛡️ Don't disclose security issues! (contact [email protected])
  • 🔗 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)

Copy link
Contributor

@dimas-b dimas-b left a comment

Choose a reason for hiding this comment

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

Thanks for pushing this feature forward, @sungwy !

Some preliminary comments, while the proposal is still being discussed.

// if the principal was not found, we can end right there
if (this.resolvedCallerPrincipal == null
|| this.resolvedCallerPrincipal.getEntity().isDropped()) {
if (isExternalPrincipal()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

can this be true without returning on line 759?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It cannot - this was before I moved the condition upto line 748 to avoid principal resolution altogether if it is external. I'll remove this check here to remove redundancy

List<ResolvedPolarisEntity> toValidate) {

if (isExternalPrincipal()) {
PrincipalEntity externalPrincipal = createExternalPrincipalEntity();
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an entity without a (real) ID... Some it's a kind of ephemeral entity... Would it be possible to avoid creating it at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good question - I'm a bit confused about the role of PrincipalEntity. We do have a public method for getResolvedCallerPrincipal() that returns resolvedCallerPrincipal. It's not being used anywhere in the codebase today, but I thought it'd be safe to populate it as it requires it to be @Nonnull: https://github.com/apache/polaris/blob/23ba2a05adc9c75f3e72aaf2ca370b4886964328/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java#L272C19-L280

Copy link
Contributor

@dimas-b dimas-b Dec 11, 2025

Choose a reason for hiding this comment

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

Conceptually, IMHO, the principal entity should only be required for AuthZ checks (which do not actually require it if it's external). So, any intermediate code that requires it may need to be refactored... but TBH, I do not remember that code very well 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the ideal solution would be to not materialize this entity at all – and very likely, pass an empty set of activatedEntities to the PolarisAuthorizer.

But I also understand that this may increase the blast radius of this PR, and also interfere with #3228.

So, I think having a temporary entity being created here can be a good compromise, as long as we don't forget to revisit this later.

Copy link
Contributor

Choose a reason for hiding this comment

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

In fact I think that #3427 and resolvePathsOnly could help here.

}

private boolean isExternalPrincipal() {
return Boolean.parseBoolean(polarisPrincipal.getProperties().getOrDefault("external", "false"));
Copy link
Contributor

Choose a reason for hiding this comment

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

WDYT about making isExternal() a method of PolarisPrincipal?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that's worth considering. I put it into PolarisPrincipal as a part of the property for now because it was already available, but I think adding it as an attribute or adding a class method that infers the property value is up for discussion

Copy link
Contributor

@adutra adutra Jan 19, 2026

Choose a reason for hiding this comment

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

Same remark: maybe we should expose a getType method in PolarisPrincipal that returns PolarisPrincipalType?

String grantType,
String scope,
TokenType requestedTokenType) {
return TokenResponse.of(OAuthError.invalid_request);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure about invalid_request... it may actually be well-formed... How about unsupported_grant_type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a great suggestion - thank you!

private ResolverStatus resolveCallerPrincipalAndPrincipalRoles(
List<ResolvedPolarisEntity> toValidate) {

if (isExternalPrincipal()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Cf. #3228


@Override
public PolarisPrincipal authenticate(PolarisCredential credentials) {
PolarisCredential externalCredentials = ensureExternal(credentials);
Copy link
Contributor Author

@sungwy sungwy Dec 11, 2025

Choose a reason for hiding this comment

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

instead of overriding the Principal to be external here, maybe we could validate that it is external and raise an exception if it isn't instead. Then we have a clearer separation of responsibility between:

  1. detecting whether a PolarisCredential's origin was internal or external
  2. and determining if a PolarisPrincipal should be internal, federated or external.

Copy link
Contributor

Choose a reason for hiding this comment

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

@adutra : WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think indeed it would be better if this authenticator rejected any non-external PolarisCredential.

However, the DefaultAuthenticator should not do the opposite, as it is valid to have persisted principals with an external IDP so it should accept any credential, internal or external.

Copy link
Contributor

Choose a reason for hiding this comment

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

In fact I'm wondering whether we need this second Authenticator.

We could instead introduce a polaris.authentication.principal.type setting (overridable per realm). Valid values would be the constants of the PolarisPrincipalType enum (e.g. persisted, federated, external).

Then the DefaultAuthenticator should be able to handle all kinds of principals. It would simply read this configuration setting to determine whether it should fetch the PolarisEntity or not. (It would also, of course, validate that if the principal type is external then the PolarisCredential must be external too.)

WDYT?

@github-actions
Copy link

This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

@github-actions github-actions bot added the stale label Jan 17, 2026
Copy link
Contributor

@adutra adutra left a comment

Choose a reason for hiding this comment

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

Thanks a lot @sungwy for this PR, and my apologies for getting to it so late!


/** True if the credential represents an external principal. */
@Value.Default
default boolean isExternal() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this property have a default value at all? I think it's safer if the code building the credential explicitly decides if it's external or not.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also thinking forward, maybe we should have an enum PolarisPrincipalType + method getPrincipalType() ?

If federated principals are implemented eventually, it would be cumbersome to have two methods isExternal and isFederated as they are mutually exclusive.

Copy link
Contributor

Choose a reason for hiding this comment

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

Update: maybe not here... a credential can only be internal or external, there is no "federated" credential. Please disregard my comment above.

/** A no-op token broker factory used when authentication is delegated to an external IdP. */
@ApplicationScoped
@Identifier("none")
public class NoneTokenBrokerFactory implements TokenBrokerFactory {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm this token broker existed and was deleted a while ago by #2634. Why are we adding it back?

@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return Collections.singleton(TokenAuthenticationRequest.class);
return Collections.singleton(InternalAuthenticationRequest.class);
Copy link
Contributor

Choose a reason for hiding this comment

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

Whoops this is an interesting bug that you fixed 😅 But I wonder how it worked before 🤔


@Override
public PolarisPrincipal authenticate(PolarisCredential credentials) {
PolarisCredential externalCredentials = ensureExternal(credentials);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think indeed it would be better if this authenticator rejected any non-external PolarisCredential.

However, the DefaultAuthenticator should not do the opposite, as it is valid to have persisted principals with an external IDP so it should accept any credential, internal or external.


@Override
public PolarisPrincipal authenticate(PolarisCredential credentials) {
PolarisCredential externalCredentials = ensureExternal(credentials);
Copy link
Contributor

Choose a reason for hiding this comment

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

In fact I'm wondering whether we need this second Authenticator.

We could instead introduce a polaris.authentication.principal.type setting (overridable per realm). Valid values would be the constants of the PolarisPrincipalType enum (e.g. persisted, federated, external).

Then the DefaultAuthenticator should be able to handle all kinds of principals. It would simply read this configuration setting to determine whether it should fetch the PolarisEntity or not. (It would also, of course, validate that if the principal type is external then the PolarisCredential must be external too.)

WDYT?

}

private boolean isExternalPrincipal() {
return Boolean.parseBoolean(polarisPrincipal.getProperties().getOrDefault("external", "false"));
Copy link
Contributor

@adutra adutra Jan 19, 2026

Choose a reason for hiding this comment

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

Same remark: maybe we should expose a getType method in PolarisPrincipal that returns PolarisPrincipalType?

List<ResolvedPolarisEntity> toValidate) {

if (isExternalPrincipal()) {
PrincipalEntity externalPrincipal = createExternalPrincipalEntity();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the ideal solution would be to not materialize this entity at all – and very likely, pass an empty set of activatedEntities to the PolarisAuthorizer.

But I also understand that this may increase the blast radius of this PR, and also interfere with #3228.

So, I think having a temporary entity being created here can be a good compromise, as long as we don't forget to revisit this later.

@SBylemans
Copy link

Hello, I'm looking to use Polaris with a keycloak integration but would appreciate this feature. Any idea in which release this PR would be made available?

@sungwy
Copy link
Contributor Author

sungwy commented Feb 4, 2026

Hi @SBylemans - we are working on a refactor in the PolarisAuthorizer that will introduce this functionality.

I'm doing a pass through an RFC doc that will be shared in the dev mailing list, and once the proposal is accepted, we'll progress on with the implementation. I can cross post the RFC here as well when it is ready.

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