Skip to content

Commit eec2913

Browse files
authored
Replace authentication filters with Quarkus Security (#1373)
1 parent 0b4539e commit eec2913

File tree

10 files changed

+243
-175
lines changed

10 files changed

+243
-175
lines changed

quarkus/defaults/src/main/resources/application.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ quarkus.config.mapping.validate-unknown=true
3939

4040
quarkus.http.access-log.enabled=true
4141
# quarkus.http.access-log.pattern=common
42+
# Cannot use proactive authentication since Polaris requires CDI request context for authentication
43+
quarkus.http.auth.proactive=false
4244
quarkus.http.body.handle-file-uploads=false
4345
quarkus.http.limits.max-body-size=10240K
4446

quarkus/service/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ dependencies {
5353
implementation("io.quarkus:quarkus-micrometer")
5454
implementation("io.quarkus:quarkus-micrometer-registry-prometheus")
5555
implementation("io.quarkus:quarkus-opentelemetry")
56+
implementation("io.quarkus:quarkus-security")
5657
implementation("io.quarkus:quarkus-smallrye-context-propagation")
5758

5859
implementation(libs.jakarta.enterprise.cdi.api)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.service.quarkus.auth;
20+
21+
import io.quarkus.security.AuthenticationFailedException;
22+
import io.quarkus.security.identity.AuthenticationRequestContext;
23+
import io.quarkus.security.identity.SecurityIdentity;
24+
import io.quarkus.security.identity.SecurityIdentityAugmentor;
25+
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
26+
import io.smallrye.mutiny.Uni;
27+
import jakarta.enterprise.context.ApplicationScoped;
28+
import jakarta.inject.Inject;
29+
import java.util.Set;
30+
import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
31+
import org.apache.polaris.service.auth.ActiveRolesProvider;
32+
33+
/**
34+
* A custom {@link SecurityIdentityAugmentor} that adds active roles to the {@link
35+
* SecurityIdentity}. This is used to augment the identity with active roles after authentication.
36+
*/
37+
@ApplicationScoped
38+
public class ActiveRolesAugmentor implements SecurityIdentityAugmentor {
39+
40+
@Inject ActiveRolesProvider activeRolesProvider;
41+
42+
@Override
43+
public Uni<SecurityIdentity> augment(
44+
SecurityIdentity identity, AuthenticationRequestContext context) {
45+
if (identity.isAnonymous()) {
46+
return Uni.createFrom().item(identity);
47+
}
48+
return context.runBlocking(() -> augmentWithActiveRoles(identity));
49+
}
50+
51+
private SecurityIdentity augmentWithActiveRoles(SecurityIdentity identity) {
52+
AuthenticatedPolarisPrincipal polarisPrincipal =
53+
identity.getPrincipal(AuthenticatedPolarisPrincipal.class);
54+
if (polarisPrincipal == null) {
55+
throw new AuthenticationFailedException("No Polaris principal found");
56+
}
57+
Set<String> validRoleNames = activeRolesProvider.getActiveRoles(polarisPrincipal);
58+
return QuarkusSecurityIdentity.builder()
59+
.setAnonymous(false)
60+
.setPrincipal(polarisPrincipal)
61+
.addRoles(validRoleNames)
62+
.addCredentials(identity.getCredentials())
63+
.addAttributes(identity.getAttributes())
64+
.addPermissionChecker(identity::checkPermission)
65+
.build();
66+
}
67+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.service.quarkus.auth;
20+
21+
import io.netty.handler.codec.http.HttpHeaderNames;
22+
import io.netty.handler.codec.http.HttpResponseStatus;
23+
import io.quarkus.security.credential.TokenCredential;
24+
import io.quarkus.security.identity.IdentityProviderManager;
25+
import io.quarkus.security.identity.SecurityIdentity;
26+
import io.quarkus.security.identity.request.AuthenticationRequest;
27+
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
28+
import io.quarkus.vertx.http.runtime.security.ChallengeData;
29+
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
30+
import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport;
31+
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
32+
import io.smallrye.mutiny.Uni;
33+
import io.vertx.ext.web.RoutingContext;
34+
import jakarta.enterprise.context.ApplicationScoped;
35+
import java.util.Collections;
36+
import java.util.Set;
37+
38+
/** A custom {@link HttpAuthenticationMechanism} that handles Polaris token authentication. */
39+
@ApplicationScoped
40+
public class PolarisAuthenticationMechanism implements HttpAuthenticationMechanism {
41+
42+
private static final String BEARER = "Bearer";
43+
44+
@Override
45+
public Uni<SecurityIdentity> authenticate(
46+
RoutingContext context, IdentityProviderManager identityProviderManager) {
47+
48+
String authHeader = context.request().getHeader("Authorization");
49+
if (authHeader == null) {
50+
return Uni.createFrom().nullItem();
51+
}
52+
53+
int spaceIdx = authHeader.indexOf(' ');
54+
if (spaceIdx <= 0 || !authHeader.substring(0, spaceIdx).equalsIgnoreCase(BEARER)) {
55+
return Uni.createFrom().nullItem();
56+
}
57+
58+
String credential = authHeader.substring(spaceIdx + 1);
59+
return identityProviderManager.authenticate(
60+
HttpSecurityUtils.setRoutingContextAttribute(
61+
new TokenAuthenticationRequest(new PolarisTokenCredential(credential)), context));
62+
}
63+
64+
@Override
65+
public Uni<ChallengeData> getChallenge(RoutingContext context) {
66+
ChallengeData result =
67+
new ChallengeData(
68+
HttpResponseStatus.UNAUTHORIZED.code(), HttpHeaderNames.WWW_AUTHENTICATE, BEARER);
69+
return Uni.createFrom().item(result);
70+
}
71+
72+
@Override
73+
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
74+
return Collections.singleton(TokenAuthenticationRequest.class);
75+
}
76+
77+
@Override
78+
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
79+
return Uni.createFrom()
80+
.item(new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, BEARER));
81+
}
82+
83+
static class PolarisTokenCredential extends TokenCredential {
84+
PolarisTokenCredential(String credential) {
85+
super(credential, "bearer");
86+
}
87+
}
88+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.service.quarkus.auth;
20+
21+
import io.quarkus.security.AuthenticationFailedException;
22+
import io.quarkus.security.identity.AuthenticationRequestContext;
23+
import io.quarkus.security.identity.IdentityProvider;
24+
import io.quarkus.security.identity.SecurityIdentity;
25+
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
26+
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
27+
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
28+
import io.smallrye.mutiny.Uni;
29+
import io.vertx.ext.web.RoutingContext;
30+
import jakarta.enterprise.context.ApplicationScoped;
31+
import jakarta.inject.Inject;
32+
import jakarta.ws.rs.NotAuthorizedException;
33+
import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
34+
import org.apache.polaris.service.auth.Authenticator;
35+
import org.apache.polaris.service.quarkus.auth.PolarisAuthenticationMechanism.PolarisTokenCredential;
36+
37+
/** A custom {@link IdentityProvider} that handles Polaris token authentication requests. */
38+
@ApplicationScoped
39+
public class PolarisIdentityProvider implements IdentityProvider<TokenAuthenticationRequest> {
40+
41+
@Inject Authenticator<String, AuthenticatedPolarisPrincipal> authenticator;
42+
43+
@Override
44+
public Class<TokenAuthenticationRequest> getRequestType() {
45+
return TokenAuthenticationRequest.class;
46+
}
47+
48+
@Override
49+
public Uni<SecurityIdentity> authenticate(
50+
TokenAuthenticationRequest request, AuthenticationRequestContext context) {
51+
if (!(request.getToken() instanceof PolarisTokenCredential)) {
52+
return Uni.createFrom().nullItem();
53+
}
54+
return context.runBlocking(() -> createSecurityIdentity(request, authenticator));
55+
}
56+
57+
public SecurityIdentity createSecurityIdentity(
58+
TokenAuthenticationRequest request,
59+
Authenticator<String, AuthenticatedPolarisPrincipal> authenticator) {
60+
try {
61+
AuthenticatedPolarisPrincipal principal =
62+
authenticator
63+
.authenticate(request.getToken().getToken())
64+
.orElseThrow(() -> new NotAuthorizedException("Authentication failed"));
65+
QuarkusSecurityIdentity.Builder builder =
66+
QuarkusSecurityIdentity.builder()
67+
.setPrincipal(principal)
68+
.addCredential(request.getToken())
69+
.addRoles(principal.getActivatedPrincipalRoleNames())
70+
.addAttribute(SecurityIdentity.USER_ATTRIBUTE, principal);
71+
RoutingContext routingContext = HttpSecurityUtils.getRoutingContextAttribute(request);
72+
if (routingContext != null) {
73+
builder.addAttribute(RoutingContext.class.getName(), routingContext);
74+
}
75+
return builder.build();
76+
} catch (RuntimeException e) {
77+
throw new AuthenticationFailedException(e);
78+
}
79+
}
80+
}

server-templates/api.mustache

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ public class {{classname}} {
105105
@{{httpMethod}}{{#subresourceOperation}}
106106
@Path("{{{path}}}"){{/subresourceOperation}}{{#hasConsumes}}
107107
@Consumes({ {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }){{/hasConsumes}}{{#hasProduces}}
108-
@Produces({ {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }){{/hasProduces}}
108+
@Produces({ {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }){{/hasProduces}}{{#hasAuthMethods}}
109+
{{#authMethods}}{{#isOAuth}}@RolesAllowed("**"){{/isOAuth}}{{/authMethods}}{{/hasAuthMethods}}
109110
@Timed("{{metricsPrefix}}.{{baseName}}.{{nickname}}")
110111
public Response {{nickname}}({{#isMultipart}}MultipartFormDataInput input,{{/isMultipart}}{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{^isMultipart}}{{>formParams}},{{/isMultipart}}{{#isMultipart}}{{^isFormParam}},{{/isFormParam}}{{/isMultipart}}{{/allParams}}@Context @MeterTag(key="realm_id",expression="realmIdentifier") RealmContext realmContext,@Context SecurityContext securityContext) {
111112
{{! Don't log form or header params in case there are secrets, e.g., OAuth tokens }}

service/common/src/main/java/org/apache/polaris/service/auth/DefaultActiveRolesProvider.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.apache.polaris.core.PolarisCallContext;
3030
import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
3131
import org.apache.polaris.core.context.CallContext;
32-
import org.apache.polaris.core.context.RealmContext;
3332
import org.apache.polaris.core.entity.PolarisEntity;
3433
import org.apache.polaris.core.entity.PolarisEntityType;
3534
import org.apache.polaris.core.entity.PrincipalRoleEntity;
@@ -51,7 +50,7 @@
5150
public class DefaultActiveRolesProvider implements ActiveRolesProvider {
5251
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultActiveRolesProvider.class);
5352

54-
@Inject RealmContext realmContext;
53+
@Inject CallContext callContext;
5554
@Inject MetaStoreManagerFactory metaStoreManagerFactory;
5655

5756
@Override
@@ -60,13 +59,13 @@ public Set<String> getActiveRoles(AuthenticatedPolarisPrincipal principal) {
6059
loadActivePrincipalRoles(
6160
principal.getActivatedPrincipalRoleNames(),
6261
principal.getPrincipalEntity(),
63-
metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext));
62+
metaStoreManagerFactory.getOrCreateMetaStoreManager(callContext.getRealmContext()));
6463
return activeRoles.stream().map(PrincipalRoleEntity::getName).collect(Collectors.toSet());
6564
}
6665

6766
protected List<PrincipalRoleEntity> loadActivePrincipalRoles(
6867
Set<String> tokenRoles, PolarisEntity principal, PolarisMetaStoreManager metaStoreManager) {
69-
PolarisCallContext polarisContext = CallContext.getCurrentContext().getPolarisCallContext();
68+
PolarisCallContext polarisContext = callContext.getPolarisCallContext();
7069
LoadGrantsResult principalGrantResults =
7170
metaStoreManager.loadGrantsToGrantee(polarisContext, principal);
7271
polarisContext

service/common/src/main/java/org/apache/polaris/service/auth/PolarisPrincipalAuthenticatorFilter.java

Lines changed: 0 additions & 89 deletions
This file was deleted.

0 commit comments

Comments
 (0)