HADOOP-11187 NameNode - KMS communication fails after a long period of inactivity. Contributed by Arun Suresh.

This commit is contained in:
Aaron T. Myers 2014-11-05 18:15:12 -08:00
parent 86eb27ba1d
commit ef5af4f8de
10 changed files with 49 additions and 32 deletions

View File

@ -17,6 +17,7 @@
import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.security.authentication.client.AuthenticatedURL; import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.apache.hadoop.security.authentication.client.AuthenticationException;
import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
import org.apache.hadoop.security.authentication.util.Signer; import org.apache.hadoop.security.authentication.util.Signer;
import org.apache.hadoop.security.authentication.util.SignerException; import org.apache.hadoop.security.authentication.util.SignerException;
import org.apache.hadoop.security.authentication.util.RandomSignerSecretProvider; import org.apache.hadoop.security.authentication.util.RandomSignerSecretProvider;
@ -36,6 +37,7 @@
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.security.Principal; import java.security.Principal;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -565,6 +567,13 @@ public Principal getUserPrincipal() {
if (!httpResponse.isCommitted()) { if (!httpResponse.isCommitted()) {
createAuthCookie(httpResponse, "", getCookieDomain(), createAuthCookie(httpResponse, "", getCookieDomain(),
getCookiePath(), 0, isHttps); getCookiePath(), 0, isHttps);
// If response code is 401. Then WWW-Authenticate Header should be
// present.. reset to 403 if not found..
if ((errCode == HttpServletResponse.SC_UNAUTHORIZED)
&& (!httpResponse.containsHeader(
KerberosAuthenticator.WWW_AUTHENTICATE))) {
errCode = HttpServletResponse.SC_FORBIDDEN;
}
if (authenticationEx == null) { if (authenticationEx == null) {
httpResponse.sendError(errCode, "Authentication required"); httpResponse.sendError(errCode, "Authentication required");
} else { } else {

View File

@ -18,6 +18,7 @@
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Properties; import java.util.Properties;
@ -30,6 +31,8 @@
*/ */
public interface AuthenticationHandler { public interface AuthenticationHandler {
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
/** /**
* Returns the authentication type of the authentication handler. * Returns the authentication type of the authentication handler.
* <p/> * <p/>

View File

@ -331,7 +331,7 @@ public AuthenticationToken authenticate(HttpServletRequest request, final HttpSe
String authorization = request.getHeader(KerberosAuthenticator.AUTHORIZATION); String authorization = request.getHeader(KerberosAuthenticator.AUTHORIZATION);
if (authorization == null || !authorization.startsWith(KerberosAuthenticator.NEGOTIATE)) { if (authorization == null || !authorization.startsWith(KerberosAuthenticator.NEGOTIATE)) {
response.setHeader(KerberosAuthenticator.WWW_AUTHENTICATE, KerberosAuthenticator.NEGOTIATE); response.setHeader(WWW_AUTHENTICATE, KerberosAuthenticator.NEGOTIATE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
if (authorization == null) { if (authorization == null) {
LOG.trace("SPNEGO starting"); LOG.trace("SPNEGO starting");

View File

@ -15,13 +15,13 @@
import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.apache.hadoop.security.authentication.client.AuthenticationException;
import org.apache.hadoop.security.authentication.client.PseudoAuthenticator; import org.apache.hadoop.security.authentication.client.PseudoAuthenticator;
import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List; import java.util.List;
@ -54,6 +54,9 @@ public class PseudoAuthenticationHandler implements AuthenticationHandler {
public static final String ANONYMOUS_ALLOWED = TYPE + ".anonymous.allowed"; public static final String ANONYMOUS_ALLOWED = TYPE + ".anonymous.allowed";
private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); private static final Charset UTF8_CHARSET = Charset.forName("UTF-8");
private static final String PSEUDO_AUTH = "PseudoAuth";
private boolean acceptAnonymous; private boolean acceptAnonymous;
private String type; private String type;
@ -181,7 +184,9 @@ public AuthenticationToken authenticate(HttpServletRequest request, HttpServletR
if (getAcceptAnonymous()) { if (getAcceptAnonymous()) {
token = AuthenticationToken.ANONYMOUS; token = AuthenticationToken.ANONYMOUS;
} else { } else {
throw new AuthenticationException("Anonymous requests are disallowed"); response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setHeader(WWW_AUTHENTICATE, PSEUDO_AUTH);
token = null;
} }
} else { } else {
token = new AuthenticationToken(userName, userName, getType()); token = new AuthenticationToken(userName, userName, getType());

View File

@ -63,8 +63,9 @@ public void testAnonymousDisallowed() throws Exception {
URL url = new URL(auth.getBaseURL()); URL url = new URL(auth.getBaseURL());
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.connect(); conn.connect();
Assert.assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode()); Assert.assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode());
Assert.assertEquals("Anonymous requests are disallowed", conn.getResponseMessage()); Assert.assertTrue(conn.getHeaderFields().containsKey("WWW-Authenticate"));
Assert.assertEquals("Authentication required", conn.getResponseMessage());
} finally { } finally {
auth.stop(); auth.stop();
} }

View File

@ -537,11 +537,11 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
} }
).when(chain).doFilter(Mockito.<ServletRequest>anyObject(), Mockito.<ServletResponse>anyObject()); ).when(chain).doFilter(Mockito.<ServletRequest>anyObject(), Mockito.<ServletResponse>anyObject());
Mockito.when(response.containsHeader("WWW-Authenticate")).thenReturn(true);
filter.doFilter(request, response, chain); filter.doFilter(request, response, chain);
Mockito.verify(response).sendError( Mockito.verify(response).sendError(
HttpServletResponse.SC_UNAUTHORIZED, "Authentication required"); HttpServletResponse.SC_UNAUTHORIZED, "Authentication required");
Mockito.verify(response).setHeader("WWW-Authenticate", "dummyauth");
} finally { } finally {
filter.destroy(); filter.destroy();
} }
@ -852,6 +852,7 @@ public void testDoFilterAuthenticatedExpired() throws Exception {
Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie}); Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie});
HttpServletResponse response = Mockito.mock(HttpServletResponse.class); HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
Mockito.when(response.containsHeader("WWW-Authenticate")).thenReturn(true);
FilterChain chain = Mockito.mock(FilterChain.class); FilterChain chain = Mockito.mock(FilterChain.class);
verifyUnauthorized(filter, request, response, chain); verifyUnauthorized(filter, request, response, chain);
@ -930,6 +931,7 @@ public void testDoFilterAuthenticatedInvalidType() throws Exception {
Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie}); Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie});
HttpServletResponse response = Mockito.mock(HttpServletResponse.class); HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
Mockito.when(response.containsHeader("WWW-Authenticate")).thenReturn(true);
FilterChain chain = Mockito.mock(FilterChain.class); FilterChain chain = Mockito.mock(FilterChain.class);
verifyUnauthorized(filter, request, response, chain); verifyUnauthorized(filter, request, response, chain);

View File

@ -21,6 +21,7 @@
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.Properties; import java.util.Properties;
public class TestPseudoAuthenticationHandler { public class TestPseudoAuthenticationHandler {
@ -74,12 +75,8 @@ public void testAnonymousOff() throws Exception {
HttpServletRequest request = Mockito.mock(HttpServletRequest.class); HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
HttpServletResponse response = Mockito.mock(HttpServletResponse.class); HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
handler.authenticate(request, response); AuthenticationToken token = handler.authenticate(request, response);
Assert.fail(); Assert.assertNull(token);
} catch (AuthenticationException ex) {
// Expected
} catch (Exception ex) {
Assert.fail();
} finally { } finally {
handler.destroy(); handler.destroy();
} }

View File

@ -412,6 +412,9 @@ Release 2.7.0 - UNRELEASED
HADOOP-11272. Allow ZKSignerSecretProvider and HADOOP-11272. Allow ZKSignerSecretProvider and
ZKDelegationTokenSecretManager to use the same curator client. (Arun Suresh via atm) ZKDelegationTokenSecretManager to use the same curator client. (Arun Suresh via atm)
HADOOP-11187 NameNode - KMS communication fails after a long period of
inactivity. (Arun Suresh via atm)
Release 2.6.0 - UNRELEASED Release 2.6.0 - UNRELEASED
INCOMPATIBLE CHANGES INCOMPATIBLE CHANGES

View File

@ -81,6 +81,8 @@
public class KMSClientProvider extends KeyProvider implements CryptoExtension, public class KMSClientProvider extends KeyProvider implements CryptoExtension,
KeyProviderDelegationTokenExtension.DelegationTokenExtension { KeyProviderDelegationTokenExtension.DelegationTokenExtension {
private static final String INVALID_SIGNATURE = "Invalid signature";
private static final String ANONYMOUS_REQUESTS_DISALLOWED = "Anonymous requests are disallowed"; private static final String ANONYMOUS_REQUESTS_DISALLOWED = "Anonymous requests are disallowed";
public static final String TOKEN_KIND = "kms-dt"; public static final String TOKEN_KIND = "kms-dt";
@ -453,7 +455,8 @@ private <T> T call(HttpURLConnection conn, Map jsonOutput,
throw ex; throw ex;
} }
if ((conn.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN if ((conn.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN
&& conn.getResponseMessage().equals(ANONYMOUS_REQUESTS_DISALLOWED)) && (conn.getResponseMessage().equals(ANONYMOUS_REQUESTS_DISALLOWED) ||
conn.getResponseMessage().contains(INVALID_SIGNATURE)))
|| conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { || conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
// Ideally, this should happen only when there is an Authentication // Ideally, this should happen only when there is an Authentication
// failure. Unfortunately, the AuthenticationFilter returns 403 when it // failure. Unfortunately, the AuthenticationFilter returns 403 when it

View File

@ -900,6 +900,7 @@ public void testKMSAuthFailureRetry() throws Exception {
keytab.getAbsolutePath()); keytab.getAbsolutePath());
conf.set("hadoop.kms.authentication.kerberos.principal", "HTTP/localhost"); conf.set("hadoop.kms.authentication.kerberos.principal", "HTTP/localhost");
conf.set("hadoop.kms.authentication.kerberos.name.rules", "DEFAULT"); conf.set("hadoop.kms.authentication.kerberos.name.rules", "DEFAULT");
conf.set("hadoop.kms.authentication.token.validity", "1");
for (KMSACLs.Type type : KMSACLs.Type.values()) { for (KMSACLs.Type type : KMSACLs.Type.values()) {
conf.set(type.getAclConfigKey(), type.toString()); conf.set(type.getAclConfigKey(), type.toString());
@ -930,11 +931,16 @@ public Void call() throws Exception {
@Override @Override
public Void run() throws Exception { public Void run() throws Exception {
KMSClientProvider kp = new KMSClientProvider(uri, conf); KMSClientProvider kp = new KMSClientProvider(uri, conf);
kp.createKey("k0", new byte[16],
new KeyProvider.Options(conf));
// This happens before rollover
kp.createKey("k1", new byte[16], kp.createKey("k1", new byte[16],
new KeyProvider.Options(conf)); new KeyProvider.Options(conf));
makeAuthTokenStale(kp); // Atleast 2 rollovers.. so should induce signer Exception
Thread.sleep(3500);
kp.createKey("k2", new byte[16], kp.createKey("k2", new byte[16],
new KeyProvider.Options(conf)); new KeyProvider.Options(conf));
return null; return null;
} }
}); });
@ -958,15 +964,16 @@ public Void run() throws Exception {
KMSClientProvider kp = new KMSClientProvider(uri, conf); KMSClientProvider kp = new KMSClientProvider(uri, conf);
kp.createKey("k3", new byte[16], kp.createKey("k3", new byte[16],
new KeyProvider.Options(conf)); new KeyProvider.Options(conf));
makeAuthTokenStale(kp); // Atleast 2 rollovers.. so should induce signer Exception
Thread.sleep(3500);
try { try {
kp.createKey("k4", new byte[16], kp.createKey("k4", new byte[16],
new KeyProvider.Options(conf)); new KeyProvider.Options(conf));
Assert.fail("Shoud fail since retry count == 0"); Assert.fail("This should not succeed..");
} catch (IOException e) { } catch (IOException e) {
Assert.assertTrue( Assert.assertTrue(
"HTTP exception must be a 403 : " + e.getMessage(), e "HTTP exception must be a 401 : " + e.getMessage(), e
.getMessage().contains("403")); .getMessage().contains("401"));
} }
return null; return null;
} }
@ -976,19 +983,6 @@ public Void run() throws Exception {
}); });
} }
private void makeAuthTokenStale(KMSClientProvider kp) throws Exception {
Field tokF = KMSClientProvider.class.getDeclaredField("authToken");
tokF.setAccessible(true);
DelegationTokenAuthenticatedURL.Token delToken =
(DelegationTokenAuthenticatedURL.Token) tokF.get(kp);
String oldTokStr = delToken.toString();
Method setM =
AuthenticatedURL.Token.class.getDeclaredMethod("set", String.class);
setM.setAccessible(true);
String newTokStr = oldTokStr.replaceAll("e=[^&]*", "e=1000");
setM.invoke(((AuthenticatedURL.Token)delToken), newTokStr);
}
@Test @Test
public void testACLs() throws Exception { public void testACLs() throws Exception {
Configuration conf = new Configuration(); Configuration conf = new Configuration();