Skip to content

Commit

Permalink
Kerberos Spnego Authentication Router Issue (#5706) (#5757)
Browse files Browse the repository at this point in the history
* Adding decoration method to proxy servlet

Change-Id: I872f9282fb60bfa20524271535980a36a87b9621

* moving the proxy request decoration to authenticators

Change-Id: I7f94b9ff5ecf08e8abf7169b58bc410f33148448

* added docs

Change-Id: I901543e52f0faf4666bfea6256a7c05593b1ae70

* use the authentication result to decorate request

Change-Id: I052650de9cd02b4faefdbcdaf2332dd3b2966af5

* adding authenticated by name

Change-Id: I074d2933460165feeddb19352eac9bd0f96f42ca

* ensure that authenticator is not null

Change-Id: Idb58e308f90db88224a06f3759114872165b24f5

* fix types and minor bug

Change-Id: I6801d49a05d5d8324406fc0280286954eb66db10

* fix typo

Change-Id: I390b12af74f44d760d0812a519125fbf0df4e97b

* use actual type names

Change-Id: I62c3ee763363781e52809ec912aafd50b8486b8e

* set authenitcatedBy to null for AutheticationResults created by
Escalator.

Change-Id: I4a675c372f59ebd8a8d19c61b85a1e4bf227a8ba
  • Loading branch information
b-slim authored and jihoonson committed May 8, 2018
1 parent 7dc4f4b commit 8b7a2da
Show file tree
Hide file tree
Showing 23 changed files with 216 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public AuthenticationResult authenticateJDBCContext(Map<String, Object> context)
}

if (checkCredentials(user, password.toCharArray())) {
return new AuthenticationResult(user, name, null);
return new AuthenticationResult(user, authorizerName, name, null);
} else {
return null;
}
Expand Down Expand Up @@ -173,7 +173,7 @@ public void doFilter(
char[] password = splits[1].toCharArray();

if (checkCredentials(user, password)) {
AuthenticationResult authenticationResult = new AuthenticationResult(user, authorizerName, null);
AuthenticationResult authenticationResult = new AuthenticationResult(user, authorizerName, name, null);
servletRequest.setAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT, authenticationResult);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ public HttpClient createEscalatedClient(HttpClient baseClient)
@Override
public AuthenticationResult createEscalatedAuthenticationResult()
{
return new AuthenticationResult(internalClientUsername, authorizerName, null);
// if you found your self asking why the authenticatedBy field is set to null please read this:
// https://github.com/druid-io/druid/pull/5706#discussion_r185940889
return new AuthenticationResult(internalClientUsername, authorizerName, null, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public void testAuth()

updater.setPermissions(AUTHORIZER_NAME, "druidRole", permissions);

AuthenticationResult authenticationResult = new AuthenticationResult("druid", "druid", null);
AuthenticationResult authenticationResult = new AuthenticationResult("druid", "druid", null, null);

Access access = authorizer.authorize(
authenticationResult,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import io.druid.guice.annotations.Self;
import io.druid.java.util.common.StringUtils;
Expand All @@ -41,6 +43,8 @@
import org.apache.hadoop.security.authentication.util.Signer;
import org.apache.hadoop.security.authentication.util.SignerException;
import org.apache.hadoop.security.authentication.util.SignerSecretProvider;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import sun.security.krb5.EncryptedData;
import sun.security.krb5.EncryptionKey;
import sun.security.krb5.internal.APReq;
Expand Down Expand Up @@ -72,25 +76,33 @@
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.HttpCookie;
import java.security.Principal;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;


@JsonTypeName("kerberos")
public class KerberosAuthenticator implements Authenticator
{
private static final Logger log = new Logger(KerberosAuthenticator.class);
private static final Pattern HADOOP_AUTH_COOKIE_REGEX = Pattern.compile(".*p=(\\S+)&t=.*");
public static final List<String> DEFAULT_EXCLUDED_PATHS = Collections.emptyList();
public static final String SIGNED_TOKEN_ATTRIBUTE = "signedToken";

private final DruidNode node;
private final String serverPrincipal;
Expand All @@ -99,6 +111,7 @@ public class KerberosAuthenticator implements Authenticator
private final List<String> excludedPaths;
private final String cookieSignatureSecret;
private final String authorizerName;
private final String name;
private LoginContext loginContext;

@JsonCreator
Expand All @@ -109,6 +122,7 @@ public KerberosAuthenticator(
@JsonProperty("excludedPaths") List<String> excludedPaths,
@JsonProperty("cookieSignatureSecret") String cookieSignatureSecret,
@JsonProperty("authorizerName") String authorizerName,
@JsonProperty("name") String name,
@JacksonInject @Self DruidNode node
)
{
Expand All @@ -119,6 +133,7 @@ public KerberosAuthenticator(
this.excludedPaths = excludedPaths == null ? DEFAULT_EXCLUDED_PATHS : excludedPaths;
this.cookieSignatureSecret = cookieSignatureSecret;
this.authorizerName = authorizerName;
this.name = Preconditions.checkNotNull(name);
}

@Override
Expand Down Expand Up @@ -253,7 +268,7 @@ public void doFilter(
if (clientPrincipal != null) {
request.setAttribute(
AuthConfig.DRUID_AUTHENTICATION_RESULT,
new AuthenticationResult(clientPrincipal, authorizerName, null)
new AuthenticationResult(clientPrincipal, authorizerName, name, null)
);
}
}
Expand Down Expand Up @@ -327,11 +342,19 @@ public Principal getUserPrincipal()
createAuthCookie(httpResponse, signedToken, getCookieDomain(),
getCookiePath(), token.getExpires(), isHttps
);
request.setAttribute(SIGNED_TOKEN_ATTRIBUTE, tokenToCookieString(
signedToken,
getCookieDomain(),
getCookiePath(),
token.getExpires(),
!token.isExpired() && token.getExpires() > 0,
isHttps
));
}
// Since this request is validated also set DRUID_AUTHENTICATION_RESULT
request.setAttribute(
AuthConfig.DRUID_AUTHENTICATION_RESULT,
new AuthenticationResult(token.getName(), authorizerName, null)
new AuthenticationResult(token.getName(), authorizerName, name, null)
);
doFilter(filterChain, httpRequest, httpResponse);
}
Expand All @@ -344,7 +367,7 @@ public Principal getUserPrincipal()
errCode = HttpServletResponse.SC_FORBIDDEN;
authenticationEx = ex;
if (log.isDebugEnabled()) {
log.debug("Authentication exception: " + ex.getMessage(), ex);
log.debug(ex, "Authentication exception: " + ex.getMessage());
} else {
log.warn("Authentication exception: " + ex.getMessage());
}
Expand Down Expand Up @@ -439,6 +462,22 @@ private boolean isExcluded(String path)
return false;
}

@Override
public void decorateProxyRequest(
HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Request proxyRequest
)
{
Object cookieToken = clientRequest.getAttribute(SIGNED_TOKEN_ATTRIBUTE);
if (cookieToken != null && cookieToken instanceof String) {
log.debug("Found cookie token will attache it to proxyRequest as cookie");
String authResult = (String) cookieToken;
String existingCookies = proxyRequest.getCookies()
.stream()
.map(HttpCookie::toString)
.collect(Collectors.joining(";"));
proxyRequest.header(HttpHeader.COOKIE, Joiner.on(";").join(authResult, existingCookies));
}
}

/**
* Kerberos context configuration for the JDK GSS library. Copied from hadoop-auth's KerberosAuthenticationHandler.
Expand Down Expand Up @@ -604,4 +643,70 @@ private void initializeKerberosLogin() throws ServletException
throw new ServletException(ex);
}
}

/**
* Creates the Hadoop authentication HTTP cookie.
*
* @param resp the response object.
* @param token authentication token for the cookie.
* @param domain the cookie domain.
* @param path the cookie path.
* @param expires UNIX timestamp that indicates the expire date of the
* cookie. It has no effect if its value &lt; 0.
* @param isSecure is the cookie secure?
* @param isCookiePersistent whether the cookie is persistent or not.
*the following code copy/past from Hadoop 3.0.0 copied to avoid compilation issue due to new signature,
* org.apache.hadoop.security.authentication.server.AuthenticationFilter#createAuthCookie
* (
* javax.servlet.http.HttpServletResponse,
* java.lang.String,
* java.lang.String,
* java.lang.String,
* long, boolean, boolean)
*/
private static void tokenToAuthCookie(
HttpServletResponse resp, String token,
String domain, String path, long expires,
boolean isCookiePersistent,
boolean isSecure
)
{
resp.addHeader("Set-Cookie", tokenToCookieString(token, domain, path, expires, isCookiePersistent, isSecure));
}

private static String tokenToCookieString(
String token,
String domain, String path, long expires,
boolean isCookiePersistent,
boolean isSecure
)
{
StringBuilder sb = new StringBuilder(AuthenticatedURL.AUTH_COOKIE)
.append("=");
if (token != null && token.length() > 0) {
sb.append("\"").append(token).append("\"");
}

if (path != null) {
sb.append("; Path=").append(path);
}

if (domain != null) {
sb.append("; Domain=").append(domain);
}

if (expires >= 0 && isCookiePersistent) {
Date date = new Date(expires);
SimpleDateFormat df = new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss zzz", Locale.ENGLISH);
df.setTimeZone(TimeZone.getTimeZone("GMT"));
sb.append("; Expires=").append(df.format(date));
}

if (isSecure) {
sb.append("; Secure");
}

sb.append("; HttpOnly");
return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.druid.java.util.http.client.HttpClient;
import io.druid.java.util.common.logger.Logger;
import io.druid.java.util.http.client.HttpClient;
import io.druid.server.security.AuthenticationResult;
import io.druid.server.security.Escalator;

Expand Down Expand Up @@ -57,6 +57,9 @@ public HttpClient createEscalatedClient(HttpClient baseClient)
@Override
public AuthenticationResult createEscalatedAuthenticationResult()
{
return new AuthenticationResult(internalClientPrincipal, authorizerName, null);
// if you found your self asking why the authenticatedBy field is set to null please read this:
// https://github.com/druid-io/druid/pull/5706#discussion_r185940889
return new AuthenticationResult(internalClientPrincipal, authorizerName, null, null);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ public Access authorize(AuthenticationResult authenticationResult, Resource reso

public void expectAuthorizationTokenCheck()
{
AuthenticationResult authenticationResult = new AuthenticationResult("druid", "druid", null);

AuthenticationResult authenticationResult = new AuthenticationResult("druid", "druid", null, null);
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).anyTimes();
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT))
.andReturn(authenticationResult)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public void setUp() throws Exception
req = EasyMock.createMock(HttpServletRequest.class);
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).anyTimes();
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null)
new AuthenticationResult("druid", "druid", null, null)
).anyTimes();
req.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().anyTimes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public List<String> getDataSources()
EasyMock.expect(supervisorManager.createOrUpdateAndStartSupervisor(spec)).andReturn(true);
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null)
new AuthenticationResult("druid", "druid", null, null)
).atLeastOnce();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().anyTimes();
Expand Down Expand Up @@ -165,7 +165,7 @@ public List<String> getDataSources()
EasyMock.expect(supervisorManager.getSupervisorSpec("id2")).andReturn(Optional.of(spec2));
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null)
new AuthenticationResult("druid", "druid", null, null)
).atLeastOnce();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().anyTimes();
Expand Down Expand Up @@ -355,7 +355,7 @@ public void testSpecGetAllHistory() throws Exception
EasyMock.expect(supervisorManager.getSupervisorSpec("id2")).andReturn(Optional.of(spec2)).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null)
new AuthenticationResult("druid", "druid", null, null)
).atLeastOnce();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().anyTimes();
Expand Down Expand Up @@ -467,7 +467,7 @@ public void testSpecGetAllHistoryWithAuthFailureFiltering() throws Exception
EasyMock.expect(supervisorManager.getSupervisorSpec("id2")).andReturn(Optional.of(spec2)).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("wronguser", "druid", null)
new AuthenticationResult("wronguser", "druid", null, null)
).atLeastOnce();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().anyTimes();
Expand Down Expand Up @@ -557,7 +557,7 @@ public void testSpecGetHistory() throws Exception
EasyMock.expect(supervisorManager.getSupervisorHistory()).andReturn(history).times(3);
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null)
new AuthenticationResult("druid", "druid", null, null)
).atLeastOnce();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().anyTimes();
Expand Down Expand Up @@ -654,7 +654,7 @@ public void testSpecGetHistoryWithAuthFailure() throws Exception
EasyMock.expect(supervisorManager.getSupervisorHistory()).andReturn(history).times(4);
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("notdruid", "druid", null)
new AuthenticationResult("notdruid", "druid", null, null)
).atLeastOnce();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().anyTimes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
import io.druid.server.router.QueryHostFinder;
import io.druid.server.router.Router;
import io.druid.server.security.AuthConfig;
import io.druid.server.security.AuthenticationResult;
import io.druid.server.security.Authenticator;
import io.druid.server.security.AuthenticatorMapper;
import org.apache.http.client.utils.URIBuilder;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request;
Expand Down Expand Up @@ -112,6 +115,7 @@ private static void handleException(HttpServletResponse response, ObjectMapper o
private final ServiceEmitter emitter;
private final RequestLogger requestLogger;
private final GenericQueryMetricsFactory queryMetricsFactory;
private final AuthenticatorMapper authenticatorMapper;

private HttpClient broadcastClient;

Expand All @@ -125,7 +129,8 @@ public AsyncQueryForwardingServlet(
@Router DruidHttpClientConfig httpClientConfig,
ServiceEmitter emitter,
RequestLogger requestLogger,
GenericQueryMetricsFactory queryMetricsFactory
GenericQueryMetricsFactory queryMetricsFactory,
AuthenticatorMapper authenticatorMapper
)
{
this.warehouse = warehouse;
Expand All @@ -137,6 +142,7 @@ public AsyncQueryForwardingServlet(
this.emitter = emitter;
this.requestLogger = requestLogger;
this.queryMetricsFactory = queryMetricsFactory;
this.authenticatorMapper = authenticatorMapper;
}

@Override
Expand Down Expand Up @@ -313,6 +319,22 @@ protected void sendProxyRequest(
// will log that on the remote node.
clientRequest.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);

// Check if there is an authentication result and use it to decorate the proxy request if needed.
AuthenticationResult authenticationResult = (AuthenticationResult) clientRequest.getAttribute(
AuthConfig.DRUID_AUTHENTICATION_RESULT);
if (authenticationResult != null && authenticationResult.getAuthenticatedBy() != null) {
Authenticator authenticator = authenticatorMapper.getAuthenticatorMap()
.get(authenticationResult.getAuthenticatedBy());
if (authenticator != null) {
authenticator.decorateProxyRequest(
clientRequest,
proxyResponse,
proxyRequest
);
} else {
log.error("Can not find Authenticator with Name [%s]", authenticationResult.getAuthenticatedBy());
}
}
super.sendProxyRequest(
clientRequest,
proxyResponse,
Expand Down

0 comments on commit 8b7a2da

Please sign in to comment.