Skip to content

Commit ea6cea8

Browse files
risdenkjoshelser
authored andcommitted
HBASE-23312 HBase Thrift SPNEGO configs (HBASE-19852) should be backwards compatible
HBase Thrift SPNEGO configs should not be required. The `hbase.thrift.spnego.keytab.file` and `hbase.thrift.spnego.principal` configs should fall back to the `hbase.thrift.keytab.file` and `hbase.thrift.kerberos.principal` configs. This will avoid any issues during upgrades. Signed-off-by: Josh Elser <[email protected]> Amending-author: Josh Elser <[email protected]> Closes #850
1 parent dbbba79 commit ea6cea8

File tree

4 files changed

+324
-73
lines changed

4 files changed

+324
-73
lines changed

hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftHttpServlet.java

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
1818

1919
package org.apache.hadoop.hbase.thrift;
2020

21-
import static org.apache.hadoop.hbase.thrift.Constants.THRIFT_SPNEGO_KEYTAB_FILE_KEY;
22-
import static org.apache.hadoop.hbase.thrift.Constants.THRIFT_SPNEGO_PRINCIPAL_KEY;
23-
2421
import java.io.IOException;
2522
import java.security.PrivilegedExceptionAction;
2623
import java.util.Base64;
@@ -29,7 +26,6 @@
2926
import javax.servlet.http.HttpServletRequest;
3027
import javax.servlet.http.HttpServletResponse;
3128

32-
import org.apache.hadoop.conf.Configuration;
3329
import org.apache.hadoop.hbase.security.SecurityUtil;
3430
import org.apache.hadoop.security.UserGroupInformation;
3531
import org.apache.hadoop.security.authorize.AuthorizationException;
@@ -66,25 +62,14 @@ public class ThriftHttpServlet extends TServlet {
6662
public static final String NEGOTIATE = "Negotiate";
6763

6864
public ThriftHttpServlet(TProcessor processor, TProtocolFactory protocolFactory,
69-
UserGroupInformation serviceUGI, Configuration conf,
70-
HBaseServiceHandler handler, boolean securityEnabled, boolean doAsEnabled)
71-
throws IOException {
65+
UserGroupInformation serviceUGI, UserGroupInformation httpUGI,
66+
HBaseServiceHandler handler, boolean securityEnabled, boolean doAsEnabled) {
7267
super(processor, protocolFactory);
7368
this.serviceUGI = serviceUGI;
69+
this.httpUGI = httpUGI;
7470
this.handler = handler;
7571
this.securityEnabled = securityEnabled;
7672
this.doAsEnabled = doAsEnabled;
77-
78-
if (securityEnabled) {
79-
// login the spnego principal
80-
UserGroupInformation.setConfiguration(conf);
81-
this.httpUGI = UserGroupInformation.loginUserFromKeytabAndReturnUGI(
82-
conf.get(THRIFT_SPNEGO_PRINCIPAL_KEY),
83-
conf.get(THRIFT_SPNEGO_KEYTAB_FILE_KEY)
84-
);
85-
} else {
86-
this.httpUGI = null;
87-
}
8873
}
8974

9075
@Override

hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftServer.java

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
import static org.apache.hadoop.hbase.thrift.Constants.THRIFT_SELECTOR_NUM;
6262
import static org.apache.hadoop.hbase.thrift.Constants.THRIFT_SERVER_SOCKET_READ_TIMEOUT_DEFAULT;
6363
import static org.apache.hadoop.hbase.thrift.Constants.THRIFT_SERVER_SOCKET_READ_TIMEOUT_KEY;
64+
import static org.apache.hadoop.hbase.thrift.Constants.THRIFT_SPNEGO_KEYTAB_FILE_KEY;
65+
import static org.apache.hadoop.hbase.thrift.Constants.THRIFT_SPNEGO_PRINCIPAL_KEY;
6466
import static org.apache.hadoop.hbase.thrift.Constants.THRIFT_SSL_ENABLED_KEY;
6567
import static org.apache.hadoop.hbase.thrift.Constants.THRIFT_SSL_EXCLUDE_CIPHER_SUITES_KEY;
6668
import static org.apache.hadoop.hbase.thrift.Constants.THRIFT_SSL_EXCLUDE_PROTOCOLS_KEY;
@@ -172,6 +174,7 @@ public class ThriftServer extends Configured implements Tool {
172174
protected ThriftMetrics metrics;
173175
protected HBaseServiceHandler hbaseServiceHandler;
174176
protected UserGroupInformation serviceUGI;
177+
protected UserGroupInformation httpUGI;
175178
protected boolean httpEnabled;
176179

177180
protected SaslUtil.QualityOfProtection qop;
@@ -210,8 +213,19 @@ protected void setupParamters() throws IOException {
210213
conf.get(THRIFT_DNS_INTERFACE_KEY, "default"),
211214
conf.get(THRIFT_DNS_NAMESERVER_KEY, "default")));
212215
userProvider.login(THRIFT_KEYTAB_FILE_KEY, THRIFT_KERBEROS_PRINCIPAL_KEY, host);
216+
217+
// Setup the SPNEGO user for HTTP if configured
218+
String spnegoPrincipal = getSpengoPrincipal(conf, host);
219+
String spnegoKeytab = getSpnegoKeytab(conf);
220+
UserGroupInformation.setConfiguration(conf);
221+
// login the SPNEGO principal using UGI to avoid polluting the login user
222+
this.httpUGI = UserGroupInformation.loginUserFromKeytabAndReturnUGI(spnegoPrincipal,
223+
spnegoKeytab);
213224
}
214225
this.serviceUGI = userProvider.getCurrent().getUGI();
226+
if (httpUGI == null) {
227+
this.httpUGI = serviceUGI;
228+
}
215229

216230
this.listenPort = conf.getInt(PORT_CONF_KEY, DEFAULT_LISTEN_PORT);
217231
this.metrics = createThriftMetrics(conf);
@@ -249,6 +263,37 @@ protected void setupParamters() throws IOException {
249263
pauseMonitor.start();
250264
}
251265

266+
private String getSpengoPrincipal(Configuration conf, String host) throws IOException {
267+
String principal = conf.get(THRIFT_SPNEGO_PRINCIPAL_KEY);
268+
if (principal == null) {
269+
// We cannot use the Hadoop configuration deprecation handling here since
270+
// the THRIFT_KERBEROS_PRINCIPAL_KEY config is still valid for regular Kerberos
271+
// communication. The preference should be to use the THRIFT_SPNEGO_PRINCIPAL_KEY
272+
// config so that THRIFT_KERBEROS_PRINCIPAL_KEY doesn't control both backend
273+
// Kerberos principal and SPNEGO principal.
274+
LOG.info("Using deprecated {} config for SPNEGO principal. Use {} instead.",
275+
THRIFT_KERBEROS_PRINCIPAL_KEY, THRIFT_SPNEGO_PRINCIPAL_KEY);
276+
principal = conf.get(THRIFT_KERBEROS_PRINCIPAL_KEY);
277+
}
278+
// Handle _HOST in principal value
279+
return org.apache.hadoop.security.SecurityUtil.getServerPrincipal(principal, host);
280+
}
281+
282+
private String getSpnegoKeytab(Configuration conf) {
283+
String keytab = conf.get(THRIFT_SPNEGO_KEYTAB_FILE_KEY);
284+
if (keytab == null) {
285+
// We cannot use the Hadoop configuration deprecation handling here since
286+
// the THRIFT_KEYTAB_FILE_KEY config is still valid for regular Kerberos
287+
// communication. The preference should be to use the THRIFT_SPNEGO_KEYTAB_FILE_KEY
288+
// config so that THRIFT_KEYTAB_FILE_KEY doesn't control both backend
289+
// Kerberos keytab and SPNEGO keytab.
290+
LOG.info("Using deprecated {} config for SPNEGO keytab. Use {} instead.",
291+
THRIFT_KEYTAB_FILE_KEY, THRIFT_SPNEGO_KEYTAB_FILE_KEY);
292+
keytab = conf.get(THRIFT_KEYTAB_FILE_KEY);
293+
}
294+
return keytab;
295+
}
296+
252297
protected void startInfoServer() throws IOException {
253298
// Put up info server.
254299
int port = conf.getInt(THRIFT_INFO_SERVER_PORT , THRIFT_INFO_SERVER_PORT_DEFAULT);
@@ -316,11 +361,10 @@ protected void printUsageAndExit(Options options, int exitCode)
316361
* Create a Servlet for the http server
317362
* @param protocolFactory protocolFactory
318363
* @return the servlet
319-
* @throws IOException IOException
320364
*/
321-
protected TServlet createTServlet(TProtocolFactory protocolFactory) throws IOException {
322-
return new ThriftHttpServlet(processor, protocolFactory, serviceUGI,
323-
conf, hbaseServiceHandler, securityEnabled, doAsEnabled);
365+
protected TServlet createTServlet(TProtocolFactory protocolFactory) {
366+
return new ThriftHttpServlet(processor, protocolFactory, serviceUGI, httpUGI,
367+
hbaseServiceHandler, securityEnabled, doAsEnabled);
324368
}
325369

326370
/**
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.thrift;
19+
20+
import static org.apache.hadoop.hbase.thrift.Constants.THRIFT_SUPPORT_PROXYUSER_KEY;
21+
import static org.junit.Assert.assertFalse;
22+
import static org.junit.Assert.assertNotNull;
23+
24+
import java.io.File;
25+
import java.nio.file.Paths;
26+
import java.security.Principal;
27+
import java.security.PrivilegedExceptionAction;
28+
import java.util.Set;
29+
30+
import javax.security.auth.Subject;
31+
import javax.security.auth.kerberos.KerberosTicket;
32+
33+
import org.apache.hadoop.conf.Configuration;
34+
import org.apache.hadoop.hbase.HBaseClassTestRule;
35+
import org.apache.hadoop.hbase.HBaseTestingUtility;
36+
import org.apache.hadoop.hbase.HConstants;
37+
import org.apache.hadoop.hbase.security.HBaseKerberosUtils;
38+
import org.apache.hadoop.hbase.testclassification.ClientTests;
39+
import org.apache.hadoop.hbase.testclassification.LargeTests;
40+
import org.apache.hadoop.hbase.thrift.generated.Hbase;
41+
import org.apache.hadoop.hbase.util.TableDescriptorChecker;
42+
import org.apache.hadoop.security.authentication.util.KerberosName;
43+
import org.apache.http.HttpHeaders;
44+
import org.apache.http.auth.AuthSchemeProvider;
45+
import org.apache.http.auth.AuthScope;
46+
import org.apache.http.auth.KerberosCredentials;
47+
import org.apache.http.client.config.AuthSchemes;
48+
import org.apache.http.config.Lookup;
49+
import org.apache.http.config.RegistryBuilder;
50+
import org.apache.http.impl.auth.SPNegoSchemeFactory;
51+
import org.apache.http.impl.client.BasicCredentialsProvider;
52+
import org.apache.http.impl.client.CloseableHttpClient;
53+
import org.apache.http.impl.client.HttpClients;
54+
import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil;
55+
import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
56+
import org.apache.thrift.protocol.TBinaryProtocol;
57+
import org.apache.thrift.protocol.TProtocol;
58+
import org.apache.thrift.transport.THttpClient;
59+
import org.ietf.jgss.GSSCredential;
60+
import org.ietf.jgss.GSSManager;
61+
import org.ietf.jgss.GSSName;
62+
import org.ietf.jgss.Oid;
63+
import org.junit.AfterClass;
64+
import org.junit.BeforeClass;
65+
import org.junit.ClassRule;
66+
import org.junit.experimental.categories.Category;
67+
import org.slf4j.Logger;
68+
import org.slf4j.LoggerFactory;
69+
70+
/**
71+
* Start the HBase Thrift HTTP server on a random port through the command-line
72+
* interface and talk to it from client side with SPNEGO security enabled.
73+
*
74+
* Supplemental test to TestThriftSpnegoHttpServer which falls back to the original
75+
* Kerberos principal and keytab configuration properties, not the separate
76+
* SPNEGO-specific properties.
77+
*/
78+
@Category({ClientTests.class, LargeTests.class})
79+
public class TestThriftSpnegoHttpFallbackServer extends TestThriftHttpServer {
80+
@ClassRule
81+
public static final HBaseClassTestRule CLASS_RULE =
82+
HBaseClassTestRule.forClass(TestThriftSpnegoHttpFallbackServer.class);
83+
84+
private static final Logger LOG =
85+
LoggerFactory.getLogger(TestThriftSpnegoHttpFallbackServer.class);
86+
87+
private static SimpleKdcServer kdc;
88+
private static File serverKeytab;
89+
private static File clientKeytab;
90+
91+
private static String clientPrincipal;
92+
private static String serverPrincipal;
93+
private static String spnegoServerPrincipal;
94+
95+
private static SimpleKdcServer buildMiniKdc() throws Exception {
96+
SimpleKdcServer kdc = new SimpleKdcServer();
97+
98+
File kdcDir = Paths.get(TEST_UTIL.getRandomDir().toString()).toAbsolutePath().toFile();
99+
kdcDir.mkdirs();
100+
kdc.setWorkDir(kdcDir);
101+
102+
kdc.setKdcHost(HConstants.LOCALHOST);
103+
int kdcPort = HBaseTestingUtility.randomFreePort();
104+
kdc.setAllowTcp(true);
105+
kdc.setAllowUdp(false);
106+
kdc.setKdcTcpPort(kdcPort);
107+
108+
LOG.info("Starting KDC server at " + HConstants.LOCALHOST + ":" + kdcPort);
109+
110+
kdc.init();
111+
112+
return kdc;
113+
}
114+
115+
private static void addSecurityConfigurations(Configuration conf) {
116+
KerberosName.setRules("DEFAULT");
117+
118+
HBaseKerberosUtils.setKeytabFileForTesting(serverKeytab.getAbsolutePath());
119+
120+
conf.setBoolean(THRIFT_SUPPORT_PROXYUSER_KEY, true);
121+
conf.setBoolean(Constants.USE_HTTP_CONF_KEY, true);
122+
123+
conf.set(Constants.THRIFT_KERBEROS_PRINCIPAL_KEY, serverPrincipal);
124+
conf.set(Constants.THRIFT_KEYTAB_FILE_KEY, serverKeytab.getAbsolutePath());
125+
126+
HBaseKerberosUtils.setSecuredConfiguration(conf, spnegoServerPrincipal,
127+
spnegoServerPrincipal);
128+
conf.set("hadoop.proxyuser.HTTP.hosts", "*");
129+
conf.set("hadoop.proxyuser.HTTP.groups", "*");
130+
conf.set(Constants.THRIFT_KERBEROS_PRINCIPAL_KEY, spnegoServerPrincipal);
131+
}
132+
133+
@BeforeClass
134+
public static void setUpBeforeClass() throws Exception {
135+
kdc = buildMiniKdc();
136+
kdc.start();
137+
138+
File keytabDir = Paths.get(TEST_UTIL.getRandomDir().toString()).toAbsolutePath().toFile();
139+
keytabDir.mkdirs();
140+
141+
clientPrincipal = "client@" + kdc.getKdcConfig().getKdcRealm();
142+
clientKeytab = new File(keytabDir, clientPrincipal + ".keytab");
143+
kdc.createAndExportPrincipals(clientKeytab, clientPrincipal);
144+
145+
serverPrincipal = "hbase/" + HConstants.LOCALHOST + "@" + kdc.getKdcConfig().getKdcRealm();
146+
serverKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') + ".keytab");
147+
148+
spnegoServerPrincipal = "HTTP/" + HConstants.LOCALHOST + "@" + kdc.getKdcConfig().getKdcRealm();
149+
// Add SPNEGO principal to server keytab
150+
kdc.createAndExportPrincipals(serverKeytab, serverPrincipal, spnegoServerPrincipal);
151+
152+
TEST_UTIL.getConfiguration().setBoolean(Constants.USE_HTTP_CONF_KEY, true);
153+
addSecurityConfigurations(TEST_UTIL.getConfiguration());
154+
155+
TestThriftHttpServer.setUpBeforeClass();
156+
}
157+
158+
@AfterClass
159+
public static void tearDownAfterClass() throws Exception {
160+
TestThriftHttpServer.tearDownAfterClass();
161+
162+
try {
163+
if (null != kdc) {
164+
kdc.stop();
165+
kdc = null;
166+
}
167+
} catch (Exception e) {
168+
LOG.info("Failed to stop mini KDC", e);
169+
}
170+
}
171+
172+
@Override
173+
protected void talkToThriftServer(String url, int customHeaderSize) throws Exception {
174+
// Close httpClient and THttpClient automatically on any failures
175+
try (
176+
CloseableHttpClient httpClient = createHttpClient();
177+
THttpClient tHttpClient = new THttpClient(url, httpClient)
178+
) {
179+
tHttpClient.open();
180+
if (customHeaderSize > 0) {
181+
StringBuilder sb = new StringBuilder();
182+
for (int i = 0; i < customHeaderSize; i++) {
183+
sb.append("a");
184+
}
185+
tHttpClient.setCustomHeader(HttpHeaders.USER_AGENT, sb.toString());
186+
}
187+
188+
TProtocol prot = new TBinaryProtocol(tHttpClient);
189+
Hbase.Client client = new Hbase.Client(prot);
190+
TestThriftServer.createTestTables(client);
191+
TestThriftServer.checkTableList(client);
192+
TestThriftServer.dropTestTables(client);
193+
}
194+
}
195+
196+
private CloseableHttpClient createHttpClient() throws Exception {
197+
final Subject clientSubject = JaasKrbUtil.loginUsingKeytab(clientPrincipal, clientKeytab);
198+
final Set<Principal> clientPrincipals = clientSubject.getPrincipals();
199+
// Make sure the subject has a principal
200+
assertFalse("Found no client principals in the clientSubject.",
201+
clientPrincipals.isEmpty());
202+
203+
// Get a TGT for the subject (might have many, different encryption types). The first should
204+
// be the default encryption type.
205+
Set<KerberosTicket> privateCredentials =
206+
clientSubject.getPrivateCredentials(KerberosTicket.class);
207+
assertFalse("Found no private credentials in the clientSubject.",
208+
privateCredentials.isEmpty());
209+
KerberosTicket tgt = privateCredentials.iterator().next();
210+
assertNotNull("No kerberos ticket found.", tgt);
211+
212+
// The name of the principal
213+
final String clientPrincipalName = clientPrincipals.iterator().next().getName();
214+
215+
return Subject.doAs(clientSubject, new PrivilegedExceptionAction<CloseableHttpClient>() {
216+
@Override
217+
public CloseableHttpClient run() throws Exception {
218+
// Logs in with Kerberos via GSS
219+
GSSManager gssManager = GSSManager.getInstance();
220+
// jGSS Kerberos login constant
221+
Oid oid = new Oid("1.2.840.113554.1.2.2");
222+
GSSName gssClient = gssManager.createName(clientPrincipalName, GSSName.NT_USER_NAME);
223+
GSSCredential credential = gssManager.createCredential(gssClient,
224+
GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
225+
226+
Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider>create()
227+
.register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true))
228+
.build();
229+
230+
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
231+
credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential));
232+
233+
return HttpClients.custom()
234+
.setDefaultAuthSchemeRegistry(authRegistry)
235+
.setDefaultCredentialsProvider(credentialsProvider)
236+
.build();
237+
}
238+
});
239+
}
240+
}

0 commit comments

Comments
 (0)