Skip to content

Commit

Permalink
ISSUE-592 : Security changes to enable Trusted Proxy in SPNEGO authen…
Browse files Browse the repository at this point in the history
…tication (#589)
  • Loading branch information
guruchai authored and raju-saravanan committed Sep 19, 2019
1 parent 2cfba36 commit 01a2f32
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,21 @@

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.hortonworks.registries.auth.PlatformName.IBM_JAVA;
Expand Down Expand Up @@ -138,18 +143,29 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
*/
public static final String KEYTAB = TYPE + ".keytab";

/**
* Constant for the configuration property that indicates the Trusted Proxy setting.
*/
public static final String ENABLE_TRUSTED_PROXY = "enable.trusted.proxy";


/**
* Constant for the configuration property that indicates the Kerberos name
* rules for the Kerberos principals.
*/
public static final String NAME_RULES = TYPE + ".name.rules";

public static final String QUERY_STRING_DELIMITER = "&";
public static final String DOAS_QUERY_STRING = "doAs=";

private String type;
private String keytab;
private GSSManager gssManager;
private Subject serverSubject = new Subject();
private List<LoginContext> loginContexts = new ArrayList<LoginContext>();
private String[] nonBrowserUserAgents;
private boolean trustedProxyEnabled = false;
private ProxyUserAuthorization proxyUserAuthorization;

/**
* Creates a Kerberos SPNEGO authentication handler with the default
Expand Down Expand Up @@ -230,6 +246,12 @@ public void init(Properties config) throws ServletException {
}
loginContexts.add(loginContext);
}

trustedProxyEnabled = Boolean.parseBoolean(config.getProperty(ENABLE_TRUSTED_PROXY));
if (trustedProxyEnabled) {
proxyUserAuthorization = new ProxyUserAuthorization(config);
}

try {
gssManager = Subject.doAs(serverSubject, new PrivilegedExceptionAction<GSSManager>() {

Expand Down Expand Up @@ -379,6 +401,15 @@ public AuthenticationToken run() throws Exception {
String clientPrincipal = gssContext.getSrcName().toString();
KerberosName kerberosName = new KerberosName(clientPrincipal);
String userName = kerberosName.getShortName();
String doAsUser = null;
if (trustedProxyEnabled && (doAsUser = getDoasUser(request)) != null) {
if (!proxyUserAuthorization.authorize(userName, request.getRemoteAddr())) {
LOG.info("{} is not authorized to act as proxy user from {}", userName, request.getRemoteAddr());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return null;
}
clientPrincipal = userName = doAsUser;
}
token = new AuthenticationToken(userName, clientPrincipal, getType());
response.setStatus(HttpServletResponse.SC_OK);
LOG.trace("SPNEGO completed for principal [{}]", clientPrincipal);
Expand Down Expand Up @@ -417,4 +448,70 @@ public boolean shouldAuthenticate(HttpServletRequest request) {
}
return true;
}

protected static String getDoasUser(HttpServletRequest request) {
String doAsUser = null;
String queryString = request.getQueryString();
if (queryString != null) {
String[] pairs = queryString.split(QUERY_STRING_DELIMITER);
try {
for (String pair : pairs) {
if (pair.startsWith(DOAS_QUERY_STRING)) {
doAsUser = URLDecoder.decode(pair.substring(DOAS_QUERY_STRING.length()), "UTF-8").trim();
if (doAsUser.isEmpty()) {
return null;
}
}
break;
}
} catch (UnsupportedEncodingException ex) {
//We are providing "UTF-8". This should not be happening ideally.
LOG.error("Invalid encoding provided.");
}
}
return doAsUser;
}

/**
* Utility class to support proxy user authorizations.
*
* An example config will look like this:
* proxyuser.knox.hosts = 10.222.0.0
* proxyuser.admin.hosts = 10.222.0.0,10.113.221.221
*/
class ProxyUserAuthorization {

/**
* Regex for the configuration property that indicates the Trusted Proxy user against approved host list.
*/
static final String PROXYUSER_REGEX_PATTERN = "proxyuser\\.(.+)\\.hosts";
static final String DELIMITER = "\\s*,\\s*";

private Map<String, List<String>> proxyUserHostsMap = new HashMap<>();

ProxyUserAuthorization(Properties config) {
Pattern pattern = Pattern.compile(PROXYUSER_REGEX_PATTERN);
config.stringPropertyNames().forEach((propertyName -> {
Matcher matcher = pattern.matcher(propertyName);
if (matcher.find()) {
String proxyUser = matcher.group(1);
String hostList = config.getProperty(propertyName).trim();
List<String> hosts = hostList.equals("*") ? new ArrayList<>() : Arrays.asList(hostList.split(DELIMITER));
proxyUserHostsMap.put(proxyUser, hosts);
}
}));
}

public boolean authorize(String proxyUser, String host) {
List<String> proxyHosts = proxyUserHostsMap.get(proxyUser);
if (proxyHosts == null) {
return false;
} else if (proxyHosts.isEmpty()) {
return true;
}

return proxyHosts.contains(host);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
import javax.servlet.http.HttpServletResponse;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.Principal;
import java.util.Properties;
import java.util.Set;
Expand All @@ -48,6 +50,8 @@ public class TestKerberosAuthenticationHandler

protected KerberosAuthenticationHandler handler;

protected Properties kerberosHandlerProps = null;

protected KerberosAuthenticationHandler getNewAuthenticationHandler() {
return new KerberosAuthenticationHandler();
}
Expand All @@ -69,6 +73,21 @@ protected Properties getDefaultProperties() {

@Before
public void setup() throws Exception {
Properties props = loadProperties();
try {
handler.init(props);
} catch (Exception ex) {
handler = null;
throw ex;
}
}

Properties loadProperties() throws Exception {
if (kerberosHandlerProps != null) {
return kerberosHandlerProps;
}

kerberosHandlerProps = getDefaultProperties();
// create keytab
File keytabFile = new File(KerberosTestUtils.getKeytabFile());
String clientPrincipal = KerberosTestUtils.getClientPrincipal();
Expand All @@ -78,13 +97,7 @@ public void setup() throws Exception {
getKdc().createPrincipal(keytabFile, clientPrincipal, serverPrincipal);
// handler
handler = getNewAuthenticationHandler();
Properties props = getDefaultProperties();
try {
handler.init(props);
} catch (Exception ex) {
handler = null;
throw ex;
}
return kerberosHandlerProps;
}

@Test(timeout = 60000)
Expand Down Expand Up @@ -178,6 +191,101 @@ public void testType() throws Exception {
Assert.assertEquals(getExpectedType(), handler.getType());
}

@Test
public void testTrustedProxyNilConfig() {
KerberosAuthenticationHandler handler = new KerberosAuthenticationHandler();
KerberosAuthenticationHandler.ProxyUserAuthorization proxyUserAuthorization = handler.new ProxyUserAuthorization(new Properties());
Assert.assertFalse(proxyUserAuthorization.authorize("knox", "127.0.0.1"));
}

@Test
public void testTrustedProxyConfig() {
KerberosAuthenticationHandler handler = new KerberosAuthenticationHandler();
KerberosAuthenticationHandler.ProxyUserAuthorization proxyUserAuthorization = handler.new ProxyUserAuthorization(new Properties() {{
put("proxyuser.knox.hosts", "127.0.0.1");
put("proxyuser.admin.hosts", "10.222.0.0,10.113.221.221");
put("proxyuser.user1.hosts", "*");
}});
Assert.assertTrue(proxyUserAuthorization.authorize("knox", "127.0.0.1"));
Assert.assertFalse(proxyUserAuthorization.authorize("knox", "10.222.0.0"));

Assert.assertTrue(proxyUserAuthorization.authorize("admin", "10.222.0.0"));
Assert.assertTrue(proxyUserAuthorization.authorize("admin", "10.113.221.221"));
Assert.assertFalse(proxyUserAuthorization.authorize("admin", "127.0.0.1"));

Assert.assertTrue(proxyUserAuthorization.authorize("user1", "10.222.0.0"));
Assert.assertTrue(proxyUserAuthorization.authorize("user1", "10.113.221.221"));
Assert.assertTrue(proxyUserAuthorization.authorize("user1", "127.0.0.1"));
}

@Test
public void testProxyDoAsUser() throws Exception {
Properties props = loadProperties();
props.setProperty(KerberosAuthenticationHandler.ENABLE_TRUSTED_PROXY, Boolean.TRUE.toString());
props.setProperty("proxyuser.client.hosts", "10.222.0.0");

KerberosAuthenticationHandler kerberosAuthHandler = new KerberosAuthenticationHandler();
kerberosAuthHandler.init(props);

//Request comes from proxyuser from whitelisted host
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
String token = generateClientToken();
Mockito.when(request.getHeader(KerberosAuthenticator.AUTHORIZATION))
.thenReturn(KerberosAuthenticator.NEGOTIATE + " " + token);
Mockito.when(request.getServerName()).thenReturn("localhost");
Mockito.when(request.getRemoteAddr()).thenReturn("10.222.0.0");
Mockito.when(request.getQueryString()).thenReturn("doAs=user1");

AuthenticationToken authToken = kerberosAuthHandler.authenticate(request, response);
Assert.assertEquals("user1", authToken.getName());
Assert.assertEquals("user1", authToken.getUserName());

//Request comes from proxyuser from non-whitelisted host with a doAsUser
HttpServletRequest request2 = Mockito.mock(HttpServletRequest.class);
HttpServletResponse response2 = Mockito.mock(HttpServletResponse.class);
String token2 = generateClientToken();
Mockito.when(request.getHeader(KerberosAuthenticator.AUTHORIZATION))
.thenReturn(KerberosAuthenticator.NEGOTIATE + " " + token2);
Mockito.when(request2.getServerName()).thenReturn("localhost");
Mockito.when(request2.getRemoteAddr()).thenReturn("10.222.0.1");
Mockito.when(request2.getQueryString()).thenReturn("doAs=user1");

AuthenticationToken authToken2 = kerberosAuthHandler.authenticate(request2, response2);
Assert.assertNull(authToken2);

//Request comes from proxyuser from non-whitelisted host without a doAsUser
HttpServletRequest request3 = Mockito.mock(HttpServletRequest.class);
HttpServletResponse response3 = Mockito.mock(HttpServletResponse.class);
String token3 = generateClientToken();
Mockito.when(request3.getHeader(KerberosAuthenticator.AUTHORIZATION))
.thenReturn(KerberosAuthenticator.NEGOTIATE + " " + token3);
Mockito.when(request3.getServerName()).thenReturn("localhost");
Mockito.when(request3.getRemoteAddr()).thenReturn("10.222.0.0");

AuthenticationToken authToken3 = kerberosAuthHandler.authenticate(request3, response3);
Assert.assertEquals("client@EXAMPLE.COM", authToken3.getName());
Assert.assertEquals("client", authToken3.getUserName());

kerberosAuthHandler.destroy();
}

@Test
public void testGetDoAsUser() throws UnsupportedEncodingException {
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
Mockito.when(request.getQueryString()).thenReturn("doAs=user1");
Assert.assertEquals("user1", KerberosAuthenticationHandler.getDoasUser(request));

Mockito.when(request.getQueryString()).thenReturn("x=y&z=a");
Assert.assertNull(KerberosAuthenticationHandler.getDoasUser(request));

Mockito.when(request.getQueryString()).thenReturn("x=y&doAs=");
Assert.assertNull(KerberosAuthenticationHandler.getDoasUser(request));

Mockito.when(request.getQueryString()).thenReturn("x=y&doAs=%20");
Assert.assertNull(KerberosAuthenticationHandler.getDoasUser(request));
}

public void testRequestWithoutAuthorization() throws Exception {
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
Expand Down Expand Up @@ -215,34 +323,7 @@ public void testRequestWithIncompleteAuthorization() throws Exception {
}

public void testRequestWithAuthorization() throws Exception {
String token = KerberosTestUtils.doAsClient(new Callable<String>() {
@Override
public String call() throws Exception {
GSSManager gssManager = GSSManager.getInstance();
GSSContext gssContext = null;
try {
String servicePrincipal = KerberosTestUtils.getServerPrincipal();
Oid oid = KerberosUtil.getOidInstance("NT_GSS_KRB5_PRINCIPAL");
GSSName serviceName = gssManager.createName(servicePrincipal,
oid);
oid = KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID");
gssContext = gssManager.createContext(serviceName, oid, null,
GSSContext.DEFAULT_LIFETIME);
gssContext.requestCredDeleg(true);
gssContext.requestMutualAuth(true);

byte[] inToken = new byte[0];
byte[] outToken = gssContext.initSecContext(inToken, 0, inToken.length);
Base64 base64 = new Base64(0);
return base64.encodeToString(outToken);

} finally {
if (gssContext != null) {
gssContext.dispose();
}
}
}
});
String token = generateClientToken();

HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
Expand Down Expand Up @@ -288,6 +369,38 @@ public void testRequestWithInvalidKerberosAuthorization() throws Exception {
}
}

String generateClientToken() throws Exception {
String token = KerberosTestUtils.doAsClient(new Callable<String>() {
@Override
public String call() throws Exception {
GSSManager gssManager = GSSManager.getInstance();
GSSContext gssContext = null;
try {
String servicePrincipal = KerberosTestUtils.getServerPrincipal();
Oid oid = KerberosUtil.getOidInstance("NT_GSS_KRB5_PRINCIPAL");
GSSName serviceName = gssManager.createName(servicePrincipal,
oid);
oid = KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID");
gssContext = gssManager.createContext(serviceName, oid, null,
GSSContext.DEFAULT_LIFETIME);
gssContext.requestCredDeleg(true);
gssContext.requestMutualAuth(true);

byte[] inToken = new byte[0];
byte[] outToken = gssContext.initSecContext(inToken, 0, inToken.length);
Base64 base64 = new Base64(0);
return base64.encodeToString(outToken);

} finally {
if (gssContext != null) {
gssContext.dispose();
}
}
}
});
return token;
}

@After
public void tearDown() throws Exception {
if (handler != null) {
Expand Down
2 changes: 2 additions & 0 deletions conf/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ servletFilters:
# type: "kerberos"
# kerberos.principal: "HTTP/streamline-ui-host.com"
# kerberos.keytab: "/vagrant/keytabs/http.keytab"
# enable.trusted.proxy: true
# proxyuser.knox.hosts: 172.22.64.70
# login.enabled: "true"
# spnego.enabled: "true"
# kerberos.name.rules: "RULE:[2:$1@$0]([jt]t@.*EXAMPLE.COM)s/.*/$MAPRED_USER/ RULE:[2:$1@$0]([nd]n@.*EXAMPLE.COM)s/.*/$HDFS_USER/DEFAULT"
Expand Down
Loading

0 comments on commit 01a2f32

Please sign in to comment.