-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
[broker][authentication]Support pass http auth status #14044
Changes from 17 commits
77b049f
ce3c5d3
1badfaa
cbc79e2
8924aa6
3cc0a9d
3407283
a4d8718
78c7726
713b1d1
6c34f62
8c2ef67
c9f8000
3fb6e3b
97ae44d
197411f
b20a7d5
0467c96
3fc7d57
5b08617
ae5b447
e4efa86
bb1ebfe
3764121
673818c
49c1080
b1def58
485e961
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,7 @@ | |
import java.util.List; | ||
import javax.naming.AuthenticationException; | ||
import javax.net.ssl.SSLSession; | ||
import javax.servlet.http.HttpServletRequest; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.apache.pulsar.broker.ServiceConfiguration; | ||
import org.apache.pulsar.broker.authentication.metrics.AuthenticationMetrics; | ||
|
@@ -165,6 +166,11 @@ public AuthenticationState newAuthState(AuthData authData, SocketAddress remoteA | |
return new TokenAuthenticationState(this, authData, remoteAddress, sslSession); | ||
} | ||
|
||
@Override | ||
public AuthenticationState newHttpAuthState(HttpServletRequest request) throws AuthenticationException { | ||
return new TokenAuthenticationHttpState(this, request); | ||
} | ||
|
||
public static String getToken(AuthenticationDataSource authData) throws AuthenticationException { | ||
if (authData.hasDataFromCommand()) { | ||
// Authenticate Pulsar binary connection | ||
|
@@ -363,4 +369,63 @@ public boolean isExpired() { | |
return expiration < System.currentTimeMillis(); | ||
} | ||
} | ||
|
||
private static final class TokenAuthenticationHttpState implements AuthenticationState { | ||
|
||
private final AuthenticationProviderToken provider; | ||
private final AuthenticationDataSource authenticationDataSource; | ||
private final Jwt<?, Claims> jwt; | ||
private final long expiration; | ||
|
||
TokenAuthenticationHttpState(AuthenticationProviderToken provider, HttpServletRequest request) | ||
throws AuthenticationException { | ||
this.provider = provider; | ||
String httpHeaderValue = request.getHeader(HTTP_HEADER_NAME); | ||
if (httpHeaderValue == null || !httpHeaderValue.startsWith(HTTP_HEADER_VALUE_PREFIX)) { | ||
throw new AuthenticationException("Invalid HTTP Authorization header"); | ||
} | ||
|
||
// Remove prefix | ||
String token = httpHeaderValue.substring(HTTP_HEADER_VALUE_PREFIX.length()); | ||
this.jwt = provider.authenticateToken(token); | ||
this.authenticationDataSource = new AuthenticationDataHttps(request); | ||
if (jwt.getBody().getExpiration() != null) { | ||
this.expiration = jwt.getBody().getExpiration().getTime(); | ||
} else { | ||
// Disable expiration | ||
this.expiration = Long.MAX_VALUE; | ||
} | ||
} | ||
|
||
@Override | ||
public String getAuthRole() throws AuthenticationException { | ||
return provider.getPrincipal(jwt); | ||
} | ||
|
||
@Override | ||
public AuthenticationDataSource getAuthDataSource() { | ||
return authenticationDataSource; | ||
} | ||
|
||
/** | ||
* Here is an explanation of why the null value is returned. | ||
* pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationState.java#L49 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lines change in the future, please refer to a method name, using javadoc syntax |
||
*/ | ||
@Override | ||
public AuthData authenticate(AuthData authData) throws AuthenticationException { | ||
return null; | ||
BewareMyPower marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
@Override | ||
public boolean isComplete() { | ||
// The authentication of tokens is always done in one single stage | ||
return true; | ||
} | ||
|
||
@Override | ||
public boolean isExpired() { | ||
return expiration < System.currentTimeMillis(); | ||
} | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,7 @@ | |
import org.apache.commons.lang3.StringUtils; | ||
import org.apache.pulsar.broker.PulsarServerException; | ||
import org.apache.pulsar.broker.ServiceConfiguration; | ||
import org.apache.pulsar.broker.web.AuthenticationFilter; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
|
@@ -84,17 +85,21 @@ public AuthenticationService(ServiceConfiguration conf) throws PulsarServerExcep | |
} | ||
} | ||
|
||
public String authenticateHttpRequest(HttpServletRequest request) throws AuthenticationException { | ||
public String authenticateHttpRequest(HttpServletRequest request, AuthenticationDataSource authData) | ||
throws AuthenticationException { | ||
AuthenticationException authenticationException = null; | ||
AuthenticationDataSource authData = new AuthenticationDataHttps(request); | ||
String authMethodName = request.getHeader("X-Pulsar-Auth-Method-Name"); | ||
String authMethodName = request.getHeader(AuthenticationFilter.PULSAR_AUTH_METHOD_NAME); | ||
|
||
if (authMethodName != null) { | ||
AuthenticationProvider providerToUse = providers.get(authMethodName); | ||
if (providerToUse == null) { | ||
throw new AuthenticationException( | ||
String.format("Unsupported authentication method: [%s].", authMethodName)); | ||
} | ||
if (authData == null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be compatible with the previous implementation |
||
AuthenticationState authenticationState = providerToUse.newHttpAuthState(request); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
authData = authenticationState.getAuthDataSource(); | ||
} | ||
try { | ||
return providerToUse.authenticate(authData); | ||
} catch (AuthenticationException e) { | ||
|
@@ -109,7 +114,8 @@ public String authenticateHttpRequest(HttpServletRequest request) throws Authent | |
} else { | ||
for (AuthenticationProvider provider : providers.values()) { | ||
try { | ||
return provider.authenticate(authData); | ||
AuthenticationState authenticationState = provider.newHttpAuthState(request); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is necessary because providers are a list |
||
return provider.authenticate(authenticationState.getAuthDataSource()); | ||
} catch (AuthenticationException e) { | ||
if (LOG.isDebugEnabled()) { | ||
LOG.debug("Authentication failed for provider " + provider.getAuthMethodName() + ": " | ||
|
@@ -137,6 +143,10 @@ public String authenticateHttpRequest(HttpServletRequest request) throws Authent | |
} | ||
} | ||
|
||
public String authenticateHttpRequest(HttpServletRequest request) throws AuthenticationException { | ||
michaeljmarshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return authenticateHttpRequest(request, null); | ||
} | ||
|
||
public AuthenticationProvider getAuthenticationProvider(String authMethodName) { | ||
return providers.get(authMethodName); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ | |
import javax.servlet.http.HttpServletResponse; | ||
import org.apache.pulsar.broker.authentication.AuthenticationDataHttps; | ||
import org.apache.pulsar.broker.authentication.AuthenticationService; | ||
import org.apache.pulsar.broker.authentication.AuthenticationState; | ||
import org.apache.pulsar.common.sasl.SaslConstants; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
@@ -44,6 +45,8 @@ public class AuthenticationFilter implements Filter { | |
|
||
public static final String AuthenticatedRoleAttributeName = AuthenticationFilter.class.getName() + "-role"; | ||
public static final String AuthenticatedDataAttributeName = AuthenticationFilter.class.getName() + "-data"; | ||
public static final String PULSAR_AUTH_METHOD_NAME = "X-Pulsar-Auth-Method-Name"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To avoid introducing the pulsar-client-api package, define it again here |
||
|
||
|
||
public AuthenticationFilter(AuthenticationService authenticationService) { | ||
this.authenticationService = authenticationService; | ||
|
@@ -71,10 +74,25 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha | |
|
||
if (!isSaslRequest(httpRequest)) { | ||
// not sasl type, return role directly. | ||
String role = authenticationService.authenticateHttpRequest((HttpServletRequest) request); | ||
String authMethodName = httpRequest.getHeader(PULSAR_AUTH_METHOD_NAME); | ||
AuthenticationState authenticationState = null; | ||
if (authMethodName != null && authenticationService.getAuthenticationProvider(authMethodName) != null) { | ||
authenticationState = authenticationService | ||
.getAuthenticationProvider(authMethodName).newHttpAuthState(httpRequest); | ||
request.setAttribute(AuthenticatedDataAttributeName, authenticationState.getAuthDataSource()); | ||
michaeljmarshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
request.setAttribute(AuthenticatedDataAttributeName, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Backward compatible |
||
new AuthenticationDataHttps((HttpServletRequest) request)); | ||
} | ||
String role; | ||
if (authenticationState != null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I think we could consolidate this conditional and the one above starting on line 79. That could help make it clear that the |
||
role = authenticationService.authenticateHttpRequest( | ||
(HttpServletRequest) request, authenticationState.getAuthDataSource()); | ||
} else { | ||
role = authenticationService.authenticateHttpRequest((HttpServletRequest) request); | ||
} | ||
request.setAttribute(AuthenticatedRoleAttributeName, role); | ||
request.setAttribute(AuthenticatedDataAttributeName, | ||
new AuthenticationDataHttps((HttpServletRequest) request)); | ||
|
||
if (LOG.isDebugEnabled()) { | ||
LOG.debug("[{}] Authenticated HTTP request with role {}", request.getRemoteAddr(), role); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,7 +48,6 @@ | |
import org.apache.commons.lang3.tuple.Pair; | ||
import org.apache.pulsar.broker.PulsarService; | ||
import org.apache.pulsar.broker.ServiceConfiguration; | ||
import org.apache.pulsar.broker.authentication.AuthenticationDataHttps; | ||
import org.apache.pulsar.broker.authentication.AuthenticationDataSource; | ||
import org.apache.pulsar.broker.authorization.AuthorizationService; | ||
import org.apache.pulsar.broker.namespace.LookupOptions; | ||
|
@@ -136,8 +135,8 @@ public String originalPrincipal() { | |
return httpRequest.getHeader(ORIGINAL_PRINCIPAL_HEADER); | ||
} | ||
|
||
public AuthenticationDataHttps clientAuthData() { | ||
return (AuthenticationDataHttps) httpRequest.getAttribute(AuthenticationFilter.AuthenticatedDataAttributeName); | ||
public AuthenticationDataSource clientAuthData() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return (AuthenticationDataSource) httpRequest.getAttribute(AuthenticationFilter.AuthenticatedDataAttributeName); | ||
} | ||
|
||
public boolean isRequestHttps() { | ||
|
@@ -1149,7 +1148,8 @@ && pulsar().getBrokerService().isAuthorizationEnabled()) { | |
return FutureUtil.failedFuture( | ||
new RestException(Status.UNAUTHORIZED, "Need to authenticate to perform the request")); | ||
} | ||
AuthenticationDataHttps authData = clientAuthData(); | ||
|
||
AuthenticationDataSource authData = clientAuthData(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AuthenticationDataHttps is the return value of the error, use AuthenticationDataSource instead of it |
||
authData.setSubscription(subscription); | ||
return pulsar().getBrokerService().getAuthorizationService() | ||
.allowTopicOperationAsync(topicName, operation, originalPrincipal(), clientAppId(), authData) | ||
|
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.
Nit: I think we could avoid creating this class by adding a new constructor to the
TokenAuthenticationState
class. As far as I can tell, the only differences come in the constructor (and in theauthenticate
method, but as @tuteng pointed out, that method can returnnull
by capturing the relevant fields in the constructor).