HBASE-24268 REST and Thrift server do not handle the "doAs" parameter case insensitively

Closes #1843

Signed-off-by: Josh Elser <elserj@apache.org>
Signed-off-by: Sean Busbey <busbey@apache.org>
This commit is contained in:
Richard Antal 2020-11-18 11:19:42 +01:00 committed by Josh Elser
parent 31917b0a8a
commit 5ce3e3e12c
4 changed files with 80 additions and 19 deletions

View File

@ -150,6 +150,25 @@ public class ProxyUserAuthenticationFilter extends AuthenticationFilter {
return false;
}
/**
* The purpose of this function is to get the doAs parameter of a http request
* case insensitively
* @param request
* @return doAs parameter if exists or null otherwise
*/
public static String getDoasFromHeader(final HttpServletRequest request) {
String doas = null;
final Enumeration<String> headers = request.getHeaderNames();
while (headers.hasMoreElements()){
String header = headers.nextElement();
if (header.toLowerCase().equals("doas")){
doas = request.getHeader(header);
break;
}
}
return doas;
}
public static HttpServletRequest toLowerCase(
final HttpServletRequest request) {
@SuppressWarnings("unchecked")

View File

@ -22,6 +22,7 @@ import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authorize.AuthorizationException;
@ -30,6 +31,7 @@ import org.apache.yetus.audience.InterfaceAudience;
import org.apache.hbase.thirdparty.org.glassfish.jersey.server.ResourceConfig;
import org.apache.hbase.thirdparty.org.glassfish.jersey.servlet.ServletContainer;
import static org.apache.hadoop.hbase.http.ProxyUserAuthenticationFilter.toLowerCase;
/**
* REST servlet container. It is used to get the remote request user
@ -51,7 +53,8 @@ public class RESTServletContainer extends ServletContainer {
@Override
public void service(final HttpServletRequest request,
final HttpServletResponse response) throws ServletException, IOException {
final String doAsUserFromQuery = request.getParameter("doAs");
final HttpServletRequest lowerCaseRequest = toLowerCase(request);
final String doAsUserFromQuery = lowerCaseRequest.getParameter("doas");
RESTServlet servlet = RESTServlet.getInstance();
if (doAsUserFromQuery != null) {
Configuration conf = servlet.getConfiguration();

View File

@ -17,6 +17,7 @@
*/
package org.apache.hadoop.hbase.rest;
import static org.apache.hadoop.hbase.rest.RESTServlet.HBASE_REST_SUPPORT_PROXYUSER;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@ -24,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.Principal;
@ -115,6 +117,7 @@ public class TestSecureRESTServer {
private static final String HOSTNAME = "localhost";
private static final String CLIENT_PRINCIPAL = "client";
private static final String WHEEL_PRINCIPAL = "wheel";
// The principal for accepting SPNEGO authn'ed requests (*must* be HTTP/fqdn)
private static final String SPNEGO_SERVICE_PRINCIPAL = "HTTP/" + HOSTNAME;
// The principal we use to connect to HBase
@ -126,6 +129,7 @@ public class TestSecureRESTServer {
private static RESTServer server;
private static File restServerKeytab;
private static File clientKeytab;
private static File wheelKeytab;
private static File serviceKeytab;
@BeforeClass
@ -148,6 +152,8 @@ public class TestSecureRESTServer {
restServerKeytab = new File(keytabDir, "spnego.keytab");
// Keytab for the client
clientKeytab = new File(keytabDir, CLIENT_PRINCIPAL + ".keytab");
// Keytab for wheel
wheelKeytab = new File(keytabDir, WHEEL_PRINCIPAL + ".keytab");
/*
* Update UGI
@ -159,6 +165,7 @@ public class TestSecureRESTServer {
*/
KDC = TEST_UTIL.setupMiniKdc(serviceKeytab);
KDC.createPrincipal(clientKeytab, CLIENT_PRINCIPAL);
KDC.createPrincipal(wheelKeytab, WHEEL_PRINCIPAL);
KDC.createPrincipal(serviceKeytab, SERVICE_PRINCIPAL);
// REST server's keytab contains keys for both principals REST uses
KDC.createPrincipal(restServerKeytab, SPNEGO_SERVICE_PRINCIPAL, REST_SERVER_PRINCIPAL);
@ -184,6 +191,8 @@ public class TestSecureRESTServer {
conf.set("hbase.superuser", "hbase");
conf.set("hadoop.proxyuser.rest.hosts", "*");
conf.set("hadoop.proxyuser.rest.users", "*");
conf.set("hadoop.proxyuser.wheel.hosts", "*");
conf.set("hadoop.proxyuser.wheel.users", "*");
UserGroupInformation.setConfiguration(conf);
updateKerberosConfiguration(conf, REST_SERVER_PRINCIPAL, SPNEGO_SERVICE_PRINCIPAL,
@ -230,6 +239,7 @@ public class TestSecureRESTServer {
return null;
}
});
instertData();
}
@AfterClass
@ -299,21 +309,21 @@ public class TestSecureRESTServer {
// Keytab for both principals above
conf.set(RESTServer.REST_KEYTAB_FILE, serverKeytab.getAbsolutePath());
conf.set("hbase.rest.authentication.kerberos.keytab", serverKeytab.getAbsolutePath());
conf.set(HBASE_REST_SUPPORT_PROXYUSER, "true");
}
@Test
public void testPositiveAuthorization() throws Exception {
private static void instertData() throws IOException, InterruptedException {
// Create a table, write a row to it, grant read perms to the client
UserGroupInformation superuser = UserGroupInformation.loginUserFromKeytabAndReturnUGI(
SERVICE_PRINCIPAL, serviceKeytab.getAbsolutePath());
SERVICE_PRINCIPAL, serviceKeytab.getAbsolutePath());
final TableName table = TableName.valueOf("publicTable");
superuser.doAs(new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws Exception {
try (Connection conn = ConnectionFactory.createConnection(TEST_UTIL.getConfiguration())) {
TableDescriptor desc = TableDescriptorBuilder.newBuilder(table)
.setColumnFamily(ColumnFamilyDescriptorBuilder.of("f1"))
.build();
.setColumnFamily(ColumnFamilyDescriptorBuilder.of("f1"))
.build();
conn.getAdmin().createTable(desc);
try (Table t = conn.getTable(table)) {
Put p = new Put(Bytes.toBytes("a"));
@ -331,6 +341,12 @@ public class TestSecureRESTServer {
return null;
}
});
}
public void testProxy(String extraArgs, String PRINCIPAL, File keytab, int responseCode) throws Exception{
UserGroupInformation superuser = UserGroupInformation.loginUserFromKeytabAndReturnUGI(
SERVICE_PRINCIPAL, serviceKeytab.getAbsolutePath());
final TableName table = TableName.valueOf("publicTable");
// Read that row as the client
Pair<CloseableHttpClient,HttpClientContext> pair = getClient();
@ -338,32 +354,54 @@ public class TestSecureRESTServer {
HttpClientContext context = pair.getSecond();
HttpGet get = new HttpGet(new URL("http://localhost:"+ REST_TEST.getServletPort()).toURI()
+ "/" + table + "/a");
+ "/" + table + "/a" + extraArgs);
get.addHeader("Accept", "application/json");
UserGroupInformation user = UserGroupInformation.loginUserFromKeytabAndReturnUGI(
CLIENT_PRINCIPAL, clientKeytab.getAbsolutePath());
PRINCIPAL, keytab.getAbsolutePath());
String jsonResponse = user.doAs(new PrivilegedExceptionAction<String>() {
@Override
public String run() throws Exception {
try (CloseableHttpResponse response = client.execute(get, context)) {
final int statusCode = response.getStatusLine().getStatusCode();
assertEquals(response.getStatusLine().toString(), HttpURLConnection.HTTP_OK, statusCode);
assertEquals(response.getStatusLine().toString(), responseCode, statusCode);
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity);
}
}
});
ObjectMapper mapper = new JacksonJaxbJsonProvider()
.locateMapper(CellSetModel.class, MediaType.APPLICATION_JSON_TYPE);
CellSetModel model = mapper.readValue(jsonResponse, CellSetModel.class);
assertEquals(1, model.getRows().size());
RowModel row = model.getRows().get(0);
assertEquals("a", Bytes.toString(row.getKey()));
assertEquals(1, row.getCells().size());
CellModel cell = row.getCells().get(0);
assertEquals("1", Bytes.toString(cell.getValue()));
if(responseCode == HttpURLConnection.HTTP_OK) {
ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class, MediaType.APPLICATION_JSON_TYPE);
CellSetModel model = mapper.readValue(jsonResponse, CellSetModel.class);
assertEquals(1, model.getRows().size());
RowModel row = model.getRows().get(0);
assertEquals("a", Bytes.toString(row.getKey()));
assertEquals(1, row.getCells().size());
CellModel cell = row.getCells().get(0);
assertEquals("1", Bytes.toString(cell.getValue()));
}
}
@Test
public void testPositiveAuthorization() throws Exception {
testProxy("", CLIENT_PRINCIPAL, clientKeytab, HttpURLConnection.HTTP_OK);
}
@Test
public void testDoAs() throws Exception {
testProxy("?doAs="+CLIENT_PRINCIPAL, WHEEL_PRINCIPAL, wheelKeytab, HttpURLConnection.HTTP_OK);
}
@Test
public void testDoas() throws Exception {
testProxy("?doas="+CLIENT_PRINCIPAL, WHEEL_PRINCIPAL, wheelKeytab, HttpURLConnection.HTTP_OK);
}
@Test
public void testWithoutDoAs() throws Exception {
testProxy("", WHEEL_PRINCIPAL, wheelKeytab, HttpURLConnection.HTTP_FORBIDDEN);
}
@Test
public void testNegativeAuthorization() throws Exception {
Pair<CloseableHttpClient,HttpClientContext> pair = getClient();

View File

@ -43,6 +43,7 @@ import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.hadoop.hbase.http.ProxyUserAuthenticationFilter.getDoasFromHeader;
/**
* Thrift Http Servlet is used for performing Kerberos authentication if security is enabled and
@ -112,7 +113,7 @@ public class ThriftHttpServlet extends TServlet {
effectiveUser = serviceUGI.getShortUserName();
}
String doAsUserFromQuery = request.getHeader("doAs");
String doAsUserFromQuery = getDoasFromHeader(request);
if (doAsUserFromQuery != null) {
if (!doAsEnabled) {
throw new ServletException("Support for proxyuser is not configured");