-
Notifications
You must be signed in to change notification settings - Fork 224
Support for external identity providers #1397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
For those willing to test out integration with Keycloak:
./gradlew :polaris-quarkus-service:quarkusDev \
-Dpolaris.realm-context.realms=realm1,realm2,realm3 \
-Dpolaris.bootstrap.credentials="realm1,root,secret;realm2,root,secret;realm3,root,secret" \
-Dpolaris.authentication.realm2.type=external \
-Dpolaris.authentication.realm3.type=mixed \
-Dquarkus.oidc.tenant-enabled=true \
-Dquarkus.oidc.auth-server-url=http://localhost:8080/realms/master \
-Dquarkus.oidc.roles.role-claim-path=polaris/roles \
-Dpolaris.oidc.principal-mapper.id-claim-path=polaris/principal_id \
-Dpolaris.oidc.principal-mapper.name-claim-path=polaris/principal_name
token=$(curl -v http://localhost:8080/realms/master/protocol/openid-connect/token \
-d client_id=client1 \
-d client_secret=s3cr3t \
-d grant_type=client_credentials | jq -r .access_token)
token=$(curl -v http://localhost:8181/api/catalog/v1/oauth/tokens \
-H "Polaris-Realm: realm1" \
-d client_id=root \
-d client_secret=secret \
-d grant_type=client_credentials \
-d scope=PRINCIPAL_ROLE:ALL | jq -r .access_token)
curl -v http://localhost:8181/api/catalog/v1/config\?warehouse\=default \
-H "Polaris-Realm: realm1" \
-H "Authorization: Bearer $token" |
* | ||
* <p>By convention, this method returns an empty set when the principal is requesting all | ||
* available principal roles. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to document things as I go. This is the best knowledge I can get for this principal attribute.
quarkus.oidc.application-type=service | ||
quarkus.oidc.resolve-tenants-with-issuer=true | ||
# Default tenant (disabled by default, set this to true if you use external authentication) | ||
quarkus.oidc.tenant-enabled=false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could enable this conditionally with a ConfigSourceInterceptor
that would check if any realm is using external or mixed auth, in which case it makes sense to enable the default OIDC tenant, especially if no other tenant is configured. But I'm leaving this as a follow-up improvement.
|
||
/** A custom {@link IdentityProvider} that handles Polaris token authentication requests. */ | ||
@ApplicationScoped | ||
public class PolarisIdentityProvider implements IdentityProvider<TokenAuthenticationRequest> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI this was just moved to org.apache.polaris.service.quarkus.auth.internal.InternalIdentityProvider
but git lost track of it.
@@ -61,10 +58,6 @@ public Map<String, String> getConfigOverrides() { | |||
} | |||
} | |||
|
|||
@Inject | |||
@Identifier("rsa-key-pair") | |||
TokenBrokerFactory tokenBrokerFactory; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused.
@@ -77,7 +77,11 @@ public Map<String, String> getConfigOverrides() { | |||
"polaris.metrics.tags.environment", | |||
"prod", | |||
"polaris.realm-context.type", | |||
"test"); | |||
"test", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test would fail without this change because each realm would have a different pair of RSA keys. So switching to shared secret.
/** | ||
* Authenticates the given credentials and returns an optional principal. | ||
* | ||
* <p>If the credentials are not valid or if the authentication fails, implementations may choose |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again trying to document things a posteriori.
I don't really see the value of returning an Optional
here, as the semantics of returning "empty" conflict with that of throwing an error. But I didn't want to change the method signature without knowing what others think.
0fff0f7
to
abb7483
Compare
dd3d992
to
42befb1
Compare
* available principal roles. When this is true, {@link #getActivatedPrincipalRoleNames()} returns | ||
* an empty set. | ||
*/ | ||
public boolean allRoles() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need this method to distinguish the case when the principal is requesting all roles from the case where no roles were found in the token – since both cases translate into an empty set of role names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But a better solution imho would be to remove roles from this class completely, and let SecurityIdentity.getRoles()
expose the roles.
I checked the call sites for getActivatedPrincipalRoles()
and it's in fact never used. So it seems that setActivatedPrincipalRoles()
could be removed as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the method because it doesn't work well with the Resolver
. TLDR we can't distinguish "no roles" from "all roles" for now.
activatedPrincipalRoles.addAll( | ||
Arrays.stream(tokenInfo.getScope().split(" ")) | ||
credentials.getPrincipalRoles().stream() | ||
.map( | ||
s -> // strip the principal_role prefix, if present | ||
s.startsWith(PRINCIPAL_ROLE_PREFIX) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is dangerous and should be changed. For example, if the roles were initially:
["PRINCIPAL_ROLE:catalog-admin', "service-admin"]
, this would be transformed into: ["catalog-admin', "service-admin"]
, potentially requesting the service-admin
role, but that likely wasn't intentional.
I think it would be safer to discard all roles that don't have the PRINCIPAL_ROLE:
prefix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, this should be stream().filter(s -> s.startsWith(PRINCIPAL_ROLE_PREFIX)).map(...)
7d192a0
to
6b76c39
Compare
Fixes #336 and #976.
Overview
This change is globally backwards-compatible, there are no configuration breaking changes and the default server behavior is the same as today (internal authentication only).
The main interfaces are also unchanged – but some implementations changed.
TODOs
Things to do in follow-up PRs:
Authentication Types
To support external identity providers, the notion of "authentication type" has been introduced. The following values are supported:
internal
: Polaris internal authentication only (default)external
: Polaris external authentication only (using Quarkus OIDC)mixed
: Polaris internal and external authentication (using Quarkus OIDC)The following authentication type is also present, but I would be in favor of removing it:
test
: The oldTestInlineBearerTokenAuthenticator
, currently unused in PolarisThe default authentication type is
internal
, but can be either changed globally or overridden per realm.The authentication type is configured in the
application.properties
file as below:See below for details of each authentication type.
Configuration Changes
The
polaris.authentication
configuration section remains backwards-compatible, but everything now can be overridden per realm.The following options are effective only when using internal auth (can be overridden in per realm):
polaris.authentication.token-service.*
polaris.authentication.token-broker.*
The
polaris.oidc
configuration section is new and is used to further configure the OIDC tenants. See below for examples.Authenticator Changes
The
Authenticator
interface remains the same, butBasePolarisAuthenticator
has been modified and becameDefaultAuthenticator
.This is because Quarkus Security distinguishes two phases of authentication:
Therefore, the
Authenticator
is not responsible anymore for decoding the token, but only for authenticating it.A new
PrincipalCredential
interface has been introduced to hold the principal ID, name and roles, and pass it to the authenticator. The existingDecodedToken
interface, which is used for internal authentication, is now a sub-type ofPrincipalCredential
.The default logic for authenticating the principal, i.e. by a metastore lookup by ID or by name, remains the same.
Token Broker Changes
Token brokers now need to be injected directly, instead of injecting their factories. They are now request-scoped beans.
As a bonus, token brokers can now be configured per realm, i.e., one realm could use RSA key pairs, and another one shared secrets.
Authentication Workflows in Detail
Internal
InternalAuthenticationMechanism
handles the authentication headerTokenBroker
to decode the token and creates aPrincipalCredential
InternalIdentityProvider
creates a firstSecurityIdentity
with thePrincipalCredential
AuthenticatingAugmentor
authenticates thePrincipalCredential
Authenticator.authenticate()
AuthenticatedPolarisPrincipal
as the SecurityIdentity's principalActiveRolesAugmentor
sets the active roles in the SecurityIdentityActiveRolesProvider
External
OidcAuthenticationMechanism
handles the authentication headerOidcIdentityProvider
creates a firstSecurityIdentity
with the tokenOidcTenantResolvingAugmentor
resolves the Polaris OIDC tenant configuration to use and store it as an attribute in theSecurityIdentity
PrincipalCredentialAugmentor
maps the JWT claims to aPrincipalCredential
and adds it to theSecurityIdentity
AuthenticatingAugmentor
authenticates thePolarisCredential
Authenticator.authenticate()
AuthenticatedPolarisPrincipal
as the SecurityIdentity's principalActiveRolesAugmentor
sets the active roles in the SecurityIdentityActiveRolesProvider
Mixed
InternalAuthenticationMechanism
handles the authentication headerTokenBroker
to decode the tokenTokenBroker
validates the token:InternalIdentityProvider
creates a first SecurityIdentity with the token as aPolarisCredential
InternalAuthenticationMechanism
yields to the OIDC mechanismOidcAuthenticationMechanism
handles the authentication headerOidcIdentityProvider
creates a firstSecurityIdentity
with the tokenOidcTenantResolvingAugmentor
resolves the Polaris OIDC tenant configuration to use and store it as an attribute in theSecurityIdentity
PrincipalCredentialAugmentor
maps the JWT claims to aPrincipalCredential
and adds it to theSecurityIdentity
AuthenticatingAugmentor
authenticates thePolarisCredential
Authenticator.authenticate()
AuthenticatedPolarisPrincipal
as the SecurityIdentity's principalActiveRolesAugmentor
sets the active roles in the SecurityIdentityActiveRolesProvider
OIDC Principal Mapping
Main interface:
org.apache.polaris.service.quarkus.auth.external.mapping.PrincipalMapper
.Currently, the following
polaris.oidc
options are available:polaris.oidc.principal-mapper.type
: the Principal mapper implementation to usepolaris.oidc.principal-mapper.id-claim-path
: the claim path to the principal id in the JWT token, e.g.polaris/principal_id
polaris.oidc.principal-mapper.name-claim-path
: the claim path to the principal name in the JWT token, e.g.polaris/principal_name
They can be overridden per OIDC tenant (this is different from the realm!). The OIDC tenants will be looked up in the
quarkus.oidc
configuration section, e.g.:If a tenant is not found in the
quarkus.oidc
or in thepolaris.oidc
section, the default OIDC tenant will be used.Important: the default OIDC tenant is disabled by default, it must be enabled if you intend to use external auth.
OIDC Principal Roles Mapping
Main interface:
org.apache.polaris.service.quarkus.auth.external.mapping.PrincipalRolesMapper
.Roles will be mapped from the JWT by Quarkus OIDC. This is done by setting the
quarkus.oidc.roles.role-claim-path
property:quarkus.oidc.roles.role-claim-path=polaris/roles
See https://quarkus.io/guides/security-oidc-bearer-token-authentication#token-claims-and-security-identity-roles for more information.
The roles mapped by Quarkus will be available in
SecurityIdentity.getRoles()
. On the Polaris side, thePrincipalRolesMapper
interface converts those roles to Polaris roles; the following properties are available:polaris.oidc.principal-roles-mapper.type
: the Principal Roles mapper implementation to use.polaris.oidc.principal-roles-mapper.filter
: optional regex to filter out roles.polaris.oidc.principal-roles-mapper.mappings[0].regex
: optional regex to modify role names.polaris.oidc.principal-roles-mapper.mappings[0].replacement
: replacement string.These properties can be overridden per realm.
Mapping Examples
Example: for a JWT token like this (some claims omitted for brevity):
You can configure Polaris like this:
Another example; if the JWT token is like this:
You can configure Polaris like this:
The resulting Polaris principal roles will be:
PRINCIPAL_ROLE:service_admin
andPRINCIPAL_ROLE:catalog_admin
.