From b997727c69096d4c662f4cb6a30aab0882f48520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Rzepi=C5=84ski?= Date: Tue, 26 May 2026 16:57:47 +0200 Subject: [PATCH 1/3] fix: Treat empty string in hadoop.auth cookie as no cookie --- extensions-core/druid-kerberos/pom.xml | 5 + .../kerberos/KerberosAuthenticator.java | 41 ++-- .../kerberos/KerberosAuthenticatorTest.java | 184 ++++++++++++++++-- 3 files changed, 196 insertions(+), 34 deletions(-) diff --git a/extensions-core/druid-kerberos/pom.xml b/extensions-core/druid-kerberos/pom.xml index f1843314434d..f75ddd24313e 100644 --- a/extensions-core/druid-kerberos/pom.xml +++ b/extensions-core/druid-kerberos/pom.xml @@ -370,6 +370,11 @@ + + org.mockito + mockito-core + test + org.apache.druid druid-processing diff --git a/extensions-core/druid-kerberos/src/main/java/org/apache/druid/security/kerberos/KerberosAuthenticator.java b/extensions-core/druid-kerberos/src/main/java/org/apache/druid/security/kerberos/KerberosAuthenticator.java index 6ef391610c31..6c2e9afec390 100644 --- a/extensions-core/druid-kerberos/src/main/java/org/apache/druid/security/kerberos/KerberosAuthenticator.java +++ b/extensions-core/druid-kerberos/src/main/java/org/apache/druid/security/kerberos/KerberosAuthenticator.java @@ -44,6 +44,7 @@ import org.eclipse.jetty.client.Request; import org.eclipse.jetty.http.HttpCookie; +import javax.annotation.Nullable; import javax.security.auth.Subject; import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.login.AppConfigurationEntry; @@ -188,11 +189,15 @@ protected AuthenticationToken getToken(HttpServletRequest request) throws Authen for (Cookie cookie : cookies) { if (cookie.getName().equals(AuthenticatedURL.AUTH_COOKIE)) { tokenStr = cookie.getValue(); - try { - tokenStr = mySigner.verifyAndExtract(tokenStr); - } - catch (SignerException ex) { - throw new AuthenticationException(ex); + if (tokenStr == null || tokenStr.isEmpty()) { + tokenStr = null; + } else { + try { + tokenStr = mySigner.verifyAndExtract(tokenStr); + } + catch (SignerException ex) { + throw new AuthenticationException(ex); + } } break; } @@ -266,7 +271,7 @@ private void doFilterSuper(ServletRequest request, ServletResponse response, Fil } token = getAuthenticationHandler().authenticate(httpRequest, httpResponse); if (token != null && token.getExpires() != 0 && - token != AuthenticationToken.ANONYMOUS) { + !AuthenticationToken.ANONYMOUS.equals(token)) { token.setExpires(System.currentTimeMillis() + getValidity() * 1000); } newToken = true; @@ -293,12 +298,13 @@ public String getRemoteUser() } @Override + @Nullable public Principal getUserPrincipal() { - return (authToken != AuthenticationToken.ANONYMOUS) ? authToken : null; + return (!AuthenticationToken.ANONYMOUS.equals(authToken)) ? authToken : null; } }; - if (newToken && !token.isExpired() && token != AuthenticationToken.ANONYMOUS) { + if (newToken && !token.isExpired() && !AuthenticationToken.ANONYMOUS.equals(token)) { String signedToken = mySigner.sign(token.toString()); tokenToAuthCookie( httpResponse, @@ -374,6 +380,7 @@ public Principal getUserPrincipal() } @Override + @Nullable public Class getFilterClass() { return null; @@ -398,6 +405,7 @@ public String getPath() } @Override + @Nullable public EnumSet getDispatcherType() { return null; @@ -423,9 +431,8 @@ public void decorateProxyRequest( ) { Object cookieToken = clientRequest.getAttribute(SIGNED_TOKEN_ATTRIBUTE); - if (cookieToken != null && cookieToken instanceof String) { + if (cookieToken instanceof String authResult) { log.debug("Found cookie token will attache it to proxyRequest as cookie"); - String authResult = (String) cookieToken; proxyRequest.cookie(HttpCookie.from(SIGNED_TOKEN_ATTRIBUTE, authResult)); } } @@ -435,8 +442,8 @@ public void decorateProxyRequest( */ public static class DruidKerberosConfiguration extends Configuration { - private String keytab; - private String principal; + private final String keytab; + private final String principal; public DruidKerberosConfiguration(String keytab, String principal) { @@ -497,11 +504,11 @@ private void initializeKerberosLogin() throws ServletException String keytab; try { - if (serverPrincipal == null || serverPrincipal.trim().length() == 0) { + if (serverPrincipal == null || serverPrincipal.trim().isEmpty()) { throw new ServletException("Principal not defined in configuration"); } keytab = serverKeytab; - if (keytab == null || keytab.trim().length() == 0) { + if (keytab == null || keytab.trim().isEmpty()) { throw new ServletException("Keytab not defined in configuration"); } if (!new File(keytab).exists()) { @@ -568,7 +575,7 @@ private static String tokenToCookieString( { StringBuilder sb = new StringBuilder(AuthenticatedURL.AUTH_COOKIE) .append("="); - if (token != null && token.length() > 0) { + if (token != null && !token.isEmpty()) { sb.append("\"").append(token).append("\""); } @@ -580,7 +587,9 @@ private static String tokenToCookieString( sb.append("; Domain=").append(domain); } - if (expires >= 0 && isCookiePersistent) { + if (expires == 0) { + sb.append("; Max-Age=0"); + } else 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")); diff --git a/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java b/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java index 406e34557f81..4b68f5fa1645 100644 --- a/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java +++ b/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java @@ -21,8 +21,23 @@ import org.apache.druid.error.DruidException; import org.apache.druid.server.DruidNode; +import org.apache.hadoop.security.authentication.client.AuthenticatedURL; +import org.apache.hadoop.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.security.authentication.server.AuthenticationToken; +import org.apache.hadoop.security.authentication.util.Signer; +import org.apache.hadoop.security.authentication.util.SignerSecretProvider; import org.junit.Assert; import org.junit.Test; +import org.mockito.Mockito; + +import javax.servlet.Filter; +import javax.servlet.ServletContext; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.Properties; public class KerberosAuthenticatorTest { @@ -39,6 +54,91 @@ private DruidNode createTestNode() } + /** + * Verifies that an empty hadoop.auth cookie value is treated as "no cookie" rather than + * causing a SignerException. An empty cookie results from a prior session expiry where + * Druid cleared the cookie. Without this fix, the empty value would be passed to + * Signer.verifyAndExtract("") which throws SignerException, setting authenticationEx + * and causing the entire auth chain to short-circuit with a 403. + */ + @Test + public void testGetTokenWithEmptyCookieReturnsNull() throws Exception + { + final Filter filter = createFilterWithSigner(); + final Method getToken = findGetTokenMethod(); + + // Empty cookie value - the real-world scenario after session expiry clears the cookie. + // Without the fix, Signer.verifyAndExtract("") throws SignerException. + final HttpServletRequest requestWithEmptyCookie = mockRequestWithEmptyCookie(); + final AuthenticationToken token = (AuthenticationToken) getToken.invoke(filter, requestWithEmptyCookie); + Assert.assertNull("Empty hadoop.auth cookie should be treated as no cookie", token); + + // No cookie at all - baseline, should return null + final HttpServletRequest requestWithNoCookie = Mockito.mock(HttpServletRequest.class); + Mockito.when(requestWithNoCookie.getCookies()).thenReturn(null); + final AuthenticationToken tokenForNoCookie = (AuthenticationToken) getToken.invoke(filter, requestWithNoCookie); + Assert.assertNull("Missing hadoop.auth cookie should return null", tokenForNoCookie); + } + + private Filter createFilterWithSigner() throws Exception + { + final Filter filter = new KerberosAuthenticator( + TEST_SERVER_PRINCIPAL, + TEST_SERVER_KEYTAB, + TEST_AUTH_TO_LOCAL, + TEST_COOKIE_SECRET, + TEST_AUTHORIZER_NAME, + TEST_NAME, + createTestNode() + ).getFilter(); + + final SignerSecretProvider secretProvider = new SignerSecretProvider() + { + @Override + public void init(Properties config, ServletContext servletContext, long tokenValidity) + { + } + + @Override + public byte[] getCurrentSecret() + { + return TEST_COOKIE_SECRET.getBytes(StandardCharsets.UTF_8); + } + + @Override + public byte[][] getAllSecrets() + { + return new byte[][]{TEST_COOKIE_SECRET.getBytes(StandardCharsets.UTF_8)}; + } + }; + final Signer signer = new Signer(secretProvider); + + // Inject mySigner into the anonymous AuthenticationFilter subclass via reflection + for (Field field : filter.getClass().getDeclaredFields()) { + if (field.getType().equals(Signer.class)) { + field.setAccessible(true); + field.set(filter, signer); + break; + } + } + return filter; + } + + private Method findGetTokenMethod() throws Exception + { + final Method method = AuthenticationFilter.class.getDeclaredMethod("getToken", HttpServletRequest.class); + method.setAccessible(true); + return method; + } + + private HttpServletRequest mockRequestWithEmptyCookie() + { + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, ""); + Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + return request; + } + @Test public void testConstructorWithNullCookieSignatureSecret() { @@ -46,15 +146,18 @@ public void testConstructorWithNullCookieSignatureSecret() DruidException exception = Assert.assertThrows( DruidException.class, - () -> new KerberosAuthenticator( - TEST_SERVER_PRINCIPAL, - TEST_SERVER_KEYTAB, - TEST_AUTH_TO_LOCAL, - null, // null cookie signature secret - TEST_AUTHORIZER_NAME, - TEST_NAME, - node - ) + () -> { + @SuppressWarnings("unused") + KerberosAuthenticator authenticator = new KerberosAuthenticator( + TEST_SERVER_PRINCIPAL, + TEST_SERVER_KEYTAB, + TEST_AUTH_TO_LOCAL, + null, // null cookie signature secret + TEST_AUTHORIZER_NAME, + TEST_NAME, + node + ); + } ); Assert.assertEquals(DruidException.Persona.OPERATOR, exception.getTargetPersona()); @@ -76,15 +179,18 @@ public void testConstructorWithEmptyCookieSignatureSecret() DruidException exception = Assert.assertThrows( DruidException.class, - () -> new KerberosAuthenticator( - TEST_SERVER_PRINCIPAL, - TEST_SERVER_KEYTAB, - TEST_AUTH_TO_LOCAL, - "", // empty cookie signature secret - TEST_AUTHORIZER_NAME, - TEST_NAME, - node - ) + () -> { + @SuppressWarnings("unused") + KerberosAuthenticator authenticator = new KerberosAuthenticator( + TEST_SERVER_PRINCIPAL, + TEST_SERVER_KEYTAB, + TEST_AUTH_TO_LOCAL, + "", // empty cookie signature secret + TEST_AUTHORIZER_NAME, + TEST_NAME, + node + ); + } ); Assert.assertEquals(DruidException.Persona.OPERATOR, exception.getTargetPersona()); @@ -98,4 +204,46 @@ public void testConstructorWithEmptyCookieSignatureSecret() exception.getMessage().contains("is not set") ); } + + @Test + public void testTokenToCookieStringWithZeroExpiresIncludesMaxAge() throws Exception + { + final Method method = KerberosAuthenticator.class.getDeclaredMethod( + "tokenToCookieString", + String.class, + String.class, + String.class, + long.class, + boolean.class, + boolean.class + ); + method.setAccessible(true); + + // Test case: expires = 0 (intended for cookie deletion) + final String cookieString = (String) method.invoke( + null, + "", // token + "localhost", // domain + "/", // path + 0, // expires + false, // isCookiePersistent + false // isSecure + ); + + Assert.assertTrue("Cookie string should contain 'Max-Age=0'", cookieString.contains("Max-Age=0")); + Assert.assertFalse("Cookie string should not contain 'Expires=' when expires is 0", cookieString.contains("Expires=")); + + // Test case: expires > 0 and persistent + final String persistentCookieString = (String) method.invoke( + null, + "some-token", + "localhost", + "/", + System.currentTimeMillis() + 3600, + true, + false + ); + Assert.assertTrue("Persistent cookie should contain 'Expires='", persistentCookieString.contains("Expires=")); + Assert.assertFalse("Persistent cookie should not contain 'Max-Age=0'", persistentCookieString.contains("Max-Age=0")); + } } From 4d92fd7e023ad1103d3a18e904d61994c3e002ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Rzepi=C5=84ski?= Date: Thu, 28 May 2026 11:39:34 +0200 Subject: [PATCH 2/3] fix: Improve test coverage for KerberosAuthenticator --- .../kerberos/KerberosAuthenticatorTest.java | 217 ++++++++++++++++-- 1 file changed, 193 insertions(+), 24 deletions(-) diff --git a/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java b/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java index 4b68f5fa1645..a79e0fc41f66 100644 --- a/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java +++ b/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java @@ -21,19 +21,26 @@ import org.apache.druid.error.DruidException; import org.apache.druid.server.DruidNode; +import org.apache.druid.server.security.AuthConfig; import org.apache.hadoop.security.authentication.client.AuthenticatedURL; import org.apache.hadoop.security.authentication.server.AuthenticationFilter; import org.apache.hadoop.security.authentication.server.AuthenticationToken; import org.apache.hadoop.security.authentication.util.Signer; import org.apache.hadoop.security.authentication.util.SignerSecretProvider; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.http.HttpCookie; import org.junit.Assert; import org.junit.Test; +import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import javax.servlet.Filter; +import javax.servlet.FilterChain; import javax.servlet.ServletContext; +import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; @@ -146,18 +153,15 @@ public void testConstructorWithNullCookieSignatureSecret() DruidException exception = Assert.assertThrows( DruidException.class, - () -> { - @SuppressWarnings("unused") - KerberosAuthenticator authenticator = new KerberosAuthenticator( - TEST_SERVER_PRINCIPAL, - TEST_SERVER_KEYTAB, - TEST_AUTH_TO_LOCAL, - null, // null cookie signature secret - TEST_AUTHORIZER_NAME, - TEST_NAME, - node - ); - } + () -> new KerberosAuthenticator( + TEST_SERVER_PRINCIPAL, + TEST_SERVER_KEYTAB, + TEST_AUTH_TO_LOCAL, + null, // null cookie signature secret + TEST_AUTHORIZER_NAME, + TEST_NAME, + node + ) ); Assert.assertEquals(DruidException.Persona.OPERATOR, exception.getTargetPersona()); @@ -179,18 +183,15 @@ public void testConstructorWithEmptyCookieSignatureSecret() DruidException exception = Assert.assertThrows( DruidException.class, - () -> { - @SuppressWarnings("unused") - KerberosAuthenticator authenticator = new KerberosAuthenticator( - TEST_SERVER_PRINCIPAL, - TEST_SERVER_KEYTAB, - TEST_AUTH_TO_LOCAL, - "", // empty cookie signature secret - TEST_AUTHORIZER_NAME, - TEST_NAME, - node - ); - } + () -> new KerberosAuthenticator( + TEST_SERVER_PRINCIPAL, + TEST_SERVER_KEYTAB, + TEST_AUTH_TO_LOCAL, + "", // empty cookie signature secret + TEST_AUTHORIZER_NAME, + TEST_NAME, + node + ) ); Assert.assertEquals(DruidException.Persona.OPERATOR, exception.getTargetPersona()); @@ -246,4 +247,172 @@ public void testTokenToCookieStringWithZeroExpiresIncludesMaxAge() throws Except Assert.assertTrue("Persistent cookie should contain 'Expires='", persistentCookieString.contains("Expires=")); Assert.assertFalse("Persistent cookie should not contain 'Max-Age=0'", persistentCookieString.contains("Max-Age=0")); } + + @Test + public void testTokenToCookieStringWithNullToken() throws Exception + { + final Method method = KerberosAuthenticator.class.getDeclaredMethod( + "tokenToCookieString", + String.class, + String.class, + String.class, + long.class, + boolean.class, + boolean.class + ); + method.setAccessible(true); + + final String cookieString = (String) method.invoke(null, null, "localhost", "/", 0, false, false); + + Assert.assertFalse("Null token should not add quoted value", cookieString.contains("\"")); + Assert.assertTrue("Cookie string should contain Max-Age=0", cookieString.contains("Max-Age=0")); + } + + @Test + public void testTokenToCookieStringWithNonPersistentPositiveExpires() throws Exception + { + final Method method = KerberosAuthenticator.class.getDeclaredMethod( + "tokenToCookieString", + String.class, + String.class, + String.class, + long.class, + boolean.class, + boolean.class + ); + method.setAccessible(true); + + final String cookieString = (String) method.invoke( + null, + "some-token", + "localhost", + "/", + System.currentTimeMillis() + 3600, + false, // isCookiePersistent = false + false + ); + + Assert.assertFalse("Non-persistent cookie should not contain 'Expires='", cookieString.contains("Expires=")); + Assert.assertFalse("Non-persistent cookie should not contain 'Max-Age=0'", cookieString.contains("Max-Age=0")); + } + + @Test + public void testDecorateProxyRequestWithStringToken() + { + final KerberosAuthenticator authenticator = new KerberosAuthenticator( + TEST_SERVER_PRINCIPAL, + TEST_SERVER_KEYTAB, + TEST_AUTH_TO_LOCAL, + TEST_COOKIE_SECRET, + TEST_AUTHORIZER_NAME, + TEST_NAME, + createTestNode() + ); + final HttpServletRequest clientRequest = Mockito.mock(HttpServletRequest.class); + final HttpServletResponse proxyResponse = Mockito.mock(HttpServletResponse.class); + final Request proxyRequest = Mockito.mock(Request.class); + Mockito.when(clientRequest.getAttribute(KerberosAuthenticator.SIGNED_TOKEN_ATTRIBUTE)).thenReturn("signed-token-value"); + + authenticator.decorateProxyRequest(clientRequest, proxyResponse, proxyRequest); + + Mockito.verify(proxyRequest).cookie(ArgumentMatchers.any(HttpCookie.class)); + } + + @Test + public void testDecorateProxyRequestWithoutToken() + { + final KerberosAuthenticator authenticator = new KerberosAuthenticator( + TEST_SERVER_PRINCIPAL, + TEST_SERVER_KEYTAB, + TEST_AUTH_TO_LOCAL, + TEST_COOKIE_SECRET, + TEST_AUTHORIZER_NAME, + TEST_NAME, + createTestNode() + ); + final HttpServletRequest clientRequest = Mockito.mock(HttpServletRequest.class); + final HttpServletResponse proxyResponse = Mockito.mock(HttpServletResponse.class); + final Request proxyRequest = Mockito.mock(Request.class); + Mockito.when(clientRequest.getAttribute(KerberosAuthenticator.SIGNED_TOKEN_ATTRIBUTE)).thenReturn(null); + + authenticator.decorateProxyRequest(clientRequest, proxyResponse, proxyRequest); + + Mockito.verify(proxyRequest, Mockito.never()).cookie(ArgumentMatchers.any()); + } + + @Test + public void testDoFilterWithNullPrincipalThrowsServletException() + { + // null serverPrincipal causes initializeKerberosLogin to throw "Principal not defined" + final KerberosAuthenticator authenticator = new KerberosAuthenticator( + null, + TEST_SERVER_KEYTAB, + TEST_AUTH_TO_LOCAL, + TEST_COOKIE_SECRET, + TEST_AUTHORIZER_NAME, + TEST_NAME, + createTestNode() + ); + final Filter filter = authenticator.getFilter(); + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + final FilterChain chain = Mockito.mock(FilterChain.class); + Mockito.when(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).thenReturn(null); + + final ServletException ex = Assert.assertThrows( + ServletException.class, + () -> filter.doFilter(request, response, chain) + ); + Assert.assertTrue(ex.getMessage().contains("Principal not defined")); + } + + @Test + public void testDoFilterWithNullKeytabThrowsServletException() + { + final KerberosAuthenticator authenticator = new KerberosAuthenticator( + TEST_SERVER_PRINCIPAL, + null, // null keytab + TEST_AUTH_TO_LOCAL, + TEST_COOKIE_SECRET, + TEST_AUTHORIZER_NAME, + TEST_NAME, + createTestNode() + ); + final Filter filter = authenticator.getFilter(); + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + final FilterChain chain = Mockito.mock(FilterChain.class); + Mockito.when(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).thenReturn(null); + + final ServletException ex = Assert.assertThrows( + ServletException.class, + () -> filter.doFilter(request, response, chain) + ); + Assert.assertTrue(ex.getMessage().contains("Keytab not defined")); + } + + @Test + public void testDoFilterWithEmptyKeytabThrowsServletException() + { + final KerberosAuthenticator authenticator = new KerberosAuthenticator( + TEST_SERVER_PRINCIPAL, + "", // empty keytab + TEST_AUTH_TO_LOCAL, + TEST_COOKIE_SECRET, + TEST_AUTHORIZER_NAME, + TEST_NAME, + createTestNode() + ); + final Filter filter = authenticator.getFilter(); + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + final FilterChain chain = Mockito.mock(FilterChain.class); + Mockito.when(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).thenReturn(null); + + final ServletException ex = Assert.assertThrows( + ServletException.class, + () -> filter.doFilter(request, response, chain) + ); + Assert.assertTrue(ex.getMessage().contains("Keytab not defined")); + } } From 11e4790e1256e07898f71b4b24280ac2b9e85225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Rzepi=C5=84ski?= Date: Sat, 30 May 2026 08:06:03 +0200 Subject: [PATCH 3/3] fix: Missing dependency declaration --- extensions-core/druid-kerberos/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extensions-core/druid-kerberos/pom.xml b/extensions-core/druid-kerberos/pom.xml index f75ddd24313e..0c019041bc8a 100644 --- a/extensions-core/druid-kerberos/pom.xml +++ b/extensions-core/druid-kerberos/pom.xml @@ -46,6 +46,11 @@ ${project.parent.version} provided + + com.google.code.findbugs + jsr305 + provided + org.apache.hadoop hadoop-common