001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.security;
019
020import static org.apache.hadoop.hbase.ipc.TestProtobufRpcServiceImpl.SERVICE;
021import static org.apache.hadoop.hbase.ipc.TestProtobufRpcServiceImpl.newBlockingStub;
022import static org.apache.hadoop.hbase.security.HBaseKerberosUtils.getKeytabFileForTesting;
023import static org.apache.hadoop.hbase.security.HBaseKerberosUtils.getPrincipalForTesting;
024import static org.apache.hadoop.hbase.security.HBaseKerberosUtils.loginKerberosPrincipal;
025import static org.apache.hadoop.hbase.security.HBaseKerberosUtils.setSecuredConfiguration;
026import static org.hamcrest.MatcherAssert.assertThat;
027import static org.hamcrest.Matchers.either;
028import static org.hamcrest.Matchers.instanceOf;
029import static org.junit.Assert.assertEquals;
030import static org.junit.Assert.assertNotEquals;
031import static org.junit.Assert.assertNotSame;
032import static org.junit.Assert.assertSame;
033import static org.junit.Assert.assertThrows;
034import static org.junit.Assert.fail;
035
036import java.io.EOFException;
037import java.io.File;
038import java.io.IOException;
039import java.lang.reflect.Field;
040import java.net.InetAddress;
041import java.net.InetSocketAddress;
042import java.net.SocketException;
043import java.security.PrivilegedExceptionAction;
044import java.util.ArrayList;
045import java.util.Collections;
046import javax.security.sasl.SaslException;
047import org.apache.commons.lang3.RandomStringUtils;
048import org.apache.hadoop.conf.Configuration;
049import org.apache.hadoop.hbase.HBaseTestingUtil;
050import org.apache.hadoop.hbase.HConstants;
051import org.apache.hadoop.hbase.exceptions.ConnectionClosedException;
052import org.apache.hadoop.hbase.ipc.FallbackDisallowedException;
053import org.apache.hadoop.hbase.ipc.FifoRpcScheduler;
054import org.apache.hadoop.hbase.ipc.RpcClient;
055import org.apache.hadoop.hbase.ipc.RpcClientFactory;
056import org.apache.hadoop.hbase.ipc.RpcServer;
057import org.apache.hadoop.hbase.ipc.RpcServerFactory;
058import org.apache.hadoop.minikdc.MiniKdc;
059import org.apache.hadoop.security.UserGroupInformation;
060import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
061import org.junit.jupiter.api.TestTemplate;
062import org.mockito.Mockito;
063
064import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
065import org.apache.hbase.thirdparty.com.google.protobuf.BlockingService;
066
067import org.apache.hadoop.hbase.shaded.ipc.protobuf.generated.TestProtos;
068import org.apache.hadoop.hbase.shaded.ipc.protobuf.generated.TestRpcServiceProtos.TestProtobufRpcProto.BlockingInterface;
069
070public class AbstractTestSecureIPC {
071
072  protected static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
073
074  protected static final File KEYTAB_FILE =
075    new File(TEST_UTIL.getDataTestDir("keytab").toUri().getPath());
076
077  protected static MiniKdc KDC;
078  protected static String HOST = "localhost";
079  protected static String PRINCIPAL;
080
081  protected String krbKeytab;
082  protected String krbPrincipal;
083  protected UserGroupInformation ugi;
084  protected Configuration clientConf;
085  protected Configuration serverConf;
086
087  protected static void initKDCAndConf() throws Exception {
088    KDC = TEST_UTIL.setupMiniKdc(KEYTAB_FILE);
089    PRINCIPAL = "hbase/" + HOST;
090    KDC.createPrincipal(KEYTAB_FILE, PRINCIPAL);
091    HBaseKerberosUtils.setPrincipalForTesting(PRINCIPAL + "@" + KDC.getRealm());
092    // set a smaller timeout and retry to speed up tests
093    TEST_UTIL.getConfiguration().setInt(RpcClient.SOCKET_TIMEOUT_READ, 2000);
094    TEST_UTIL.getConfiguration().setInt("hbase.security.relogin.maxretries", 1);
095    TEST_UTIL.getConfiguration().setInt("hbase.security.relogin.maxbackoff", 100);
096  }
097
098  protected static void stopKDC() {
099    if (KDC != null) {
100      KDC.stop();
101    }
102  }
103
104  protected final void setUpPrincipalAndConf() throws Exception {
105    krbKeytab = getKeytabFileForTesting();
106    krbPrincipal = getPrincipalForTesting();
107    ugi = loginKerberosPrincipal(krbKeytab, krbPrincipal);
108    clientConf = new Configuration(TEST_UTIL.getConfiguration());
109    setSecuredConfiguration(clientConf);
110    serverConf = new Configuration(TEST_UTIL.getConfiguration());
111    setSecuredConfiguration(serverConf);
112  }
113
114  @TestTemplate
115  public void testRpcCallWithEnabledKerberosSaslAuth() throws Exception {
116    UserGroupInformation ugi2 = UserGroupInformation.getCurrentUser();
117
118    // check that the login user is okay:
119    assertSame(ugi2, ugi);
120    assertEquals(AuthenticationMethod.KERBEROS, ugi.getAuthenticationMethod());
121    assertEquals(krbPrincipal, ugi.getUserName());
122
123    callRpcService(User.create(ugi2));
124  }
125
126  private static void setCanonicalHostName(InetAddress addr, String canonicalHostName)
127    throws Exception {
128    final Field field = InetAddress.class.getDeclaredField("canonicalHostName");
129    field.setAccessible(true);
130    field.set(addr, canonicalHostName);
131
132  }
133
134  @TestTemplate
135  public void testRpcCallWithKerberosSaslAuthCanonicalHostname() throws Exception {
136    UserGroupInformation ugi2 = UserGroupInformation.getCurrentUser();
137
138    // check that the login user is okay:
139    assertSame(ugi2, ugi);
140    assertEquals(AuthenticationMethod.KERBEROS, ugi.getAuthenticationMethod());
141    assertEquals(krbPrincipal, ugi.getUserName());
142
143    clientConf.setBoolean(
144      SecurityConstants.UNSAFE_HBASE_CLIENT_KERBEROS_HOSTNAME_DISABLE_REVERSEDNS, false);
145    clientConf.set(HBaseKerberosUtils.KRB_PRINCIPAL, "hbase/_HOST@" + KDC.getRealm());
146
147    // The InetAddress for localhost is always the same, so here we just modify it to simulate
148    // hostname mismatch
149    InetAddress addr = InetAddress.getByName(HOST);
150    String originalCanonicalHostname = addr.getCanonicalHostName();
151    assertNotEquals("127.0.0.1", originalCanonicalHostname);
152    setCanonicalHostName(addr, "127.0.0.1");
153    // should fail because of canonical hostname does not match the principal name
154    assertThrows(Exception.class, () -> callRpcService(User.create(ugi2)));
155
156    clientConf
157      .setBoolean(SecurityConstants.UNSAFE_HBASE_CLIENT_KERBEROS_HOSTNAME_DISABLE_REVERSEDNS, true);
158    // should pass since we do not use canonical hostname
159    callRpcService(User.create(ugi2));
160
161    clientConf.setBoolean(
162      SecurityConstants.UNSAFE_HBASE_CLIENT_KERBEROS_HOSTNAME_DISABLE_REVERSEDNS, false);
163    setCanonicalHostName(addr, originalCanonicalHostname);
164    // should pass since we set the canonical hostname back, which should be same with the one in
165    // the principal name
166    callRpcService(User.create(ugi2));
167  }
168
169  @TestTemplate
170  public void testRpcServerFallbackToSimpleAuth() throws Exception {
171    String clientUsername = "testuser";
172    UserGroupInformation clientUgi =
173      UserGroupInformation.createUserForTesting(clientUsername, new String[] { clientUsername });
174
175    // check that the client user is insecure
176    assertNotSame(ugi, clientUgi);
177    assertEquals(AuthenticationMethod.SIMPLE, clientUgi.getAuthenticationMethod());
178    assertEquals(clientUsername, clientUgi.getUserName());
179
180    clientConf.set(User.HBASE_SECURITY_CONF_KEY, "simple");
181    serverConf.setBoolean(RpcServer.FALLBACK_TO_INSECURE_CLIENT_AUTH, true);
182    callRpcService(User.create(clientUgi));
183  }
184
185  @TestTemplate
186  public void testRpcServerDisallowFallbackToSimpleAuth() throws Exception {
187    String clientUsername = "testuser";
188    UserGroupInformation clientUgi =
189      UserGroupInformation.createUserForTesting(clientUsername, new String[] { clientUsername });
190
191    // check that the client user is insecure
192    assertNotSame(ugi, clientUgi);
193    assertEquals(AuthenticationMethod.SIMPLE, clientUgi.getAuthenticationMethod());
194    assertEquals(clientUsername, clientUgi.getUserName());
195
196    clientConf.set(User.HBASE_SECURITY_CONF_KEY, "simple");
197    serverConf.setBoolean(RpcServer.FALLBACK_TO_INSECURE_CLIENT_AUTH, false);
198    IOException error =
199      assertThrows(IOException.class, () -> callRpcService(User.create(clientUgi)));
200    // server just closes the connection, so we could get broken pipe, or EOF, or connection closed,
201    // or socket exception
202    if (error.getMessage() == null || !error.getMessage().contains("Broken pipe")) {
203      assertThat(error, either(instanceOf(EOFException.class))
204        .or(instanceOf(ConnectionClosedException.class)).or(instanceOf(SocketException.class)));
205    }
206  }
207
208  @TestTemplate
209  public void testRpcClientFallbackToSimpleAuth() throws Exception {
210    String serverUsername = "testuser";
211    UserGroupInformation serverUgi =
212      UserGroupInformation.createUserForTesting(serverUsername, new String[] { serverUsername });
213    // check that the server user is insecure
214    assertNotSame(ugi, serverUgi);
215    assertEquals(AuthenticationMethod.SIMPLE, serverUgi.getAuthenticationMethod());
216    assertEquals(serverUsername, serverUgi.getUserName());
217
218    serverConf.set(User.HBASE_SECURITY_CONF_KEY, "simple");
219    clientConf.setBoolean(RpcClient.IPC_CLIENT_FALLBACK_TO_SIMPLE_AUTH_ALLOWED_KEY, true);
220    callRpcService(User.create(serverUgi), User.create(ugi));
221  }
222
223  @TestTemplate
224  public void testRpcClientDisallowFallbackToSimpleAuth() throws Exception {
225    String serverUsername = "testuser";
226    UserGroupInformation serverUgi =
227      UserGroupInformation.createUserForTesting(serverUsername, new String[] { serverUsername });
228    // check that the server user is insecure
229    assertNotSame(ugi, serverUgi);
230    assertEquals(AuthenticationMethod.SIMPLE, serverUgi.getAuthenticationMethod());
231    assertEquals(serverUsername, serverUgi.getUserName());
232
233    serverConf.set(User.HBASE_SECURITY_CONF_KEY, "simple");
234    clientConf.setBoolean(RpcClient.IPC_CLIENT_FALLBACK_TO_SIMPLE_AUTH_ALLOWED_KEY, false);
235    assertThrows(FallbackDisallowedException.class,
236      () -> callRpcService(User.create(serverUgi), User.create(ugi)));
237  }
238
239  private void setRpcProtection(String clientProtection, String serverProtection) {
240    clientConf.set("hbase.rpc.protection", clientProtection);
241    serverConf.set("hbase.rpc.protection", serverProtection);
242  }
243
244  /**
245   * Test various combinations of Server and Client qops.
246   */
247  @TestTemplate
248  public void testSaslWithCommonQop() throws Exception {
249    setRpcProtection("privacy,authentication", "authentication");
250    callRpcService();
251
252    setRpcProtection("authentication", "privacy,authentication");
253    callRpcService();
254
255    setRpcProtection("integrity,authentication", "privacy,authentication");
256    callRpcService();
257
258    setRpcProtection("integrity,authentication", "integrity,authentication");
259    callRpcService();
260
261    setRpcProtection("privacy,authentication", "privacy,authentication");
262    callRpcService();
263  }
264
265  @TestTemplate
266  public void testSaslNoCommonQop() throws Exception {
267    setRpcProtection("integrity", "privacy");
268    SaslException se = assertThrows(SaslException.class, () -> callRpcService());
269    assertEquals("No common protection layer between client and server", se.getMessage());
270  }
271
272  /**
273   * Test sasl encryption with Crypto AES.
274   */
275  @TestTemplate
276  public void testSaslWithCryptoAES() throws Exception {
277    setRpcProtection("privacy", "privacy");
278    setCryptoAES("true", "true");
279    callRpcService();
280  }
281
282  /**
283   * Test various combinations of Server and Client configuration for Crypto AES.
284   */
285  @TestTemplate
286  public void testDifferentConfWithCryptoAES() throws Exception {
287    setRpcProtection("privacy", "privacy");
288
289    setCryptoAES("false", "true");
290    callRpcService();
291
292    setCryptoAES("true", "false");
293    try {
294      callRpcService();
295      fail("The exception should be thrown out for the rpc timeout.");
296    } catch (Exception e) {
297      // ignore the expected exception
298    }
299  }
300
301  private void setCryptoAES(String clientCryptoAES, String serverCryptoAES) {
302    clientConf.set("hbase.rpc.crypto.encryption.aes.enabled", clientCryptoAES);
303    serverConf.set("hbase.rpc.crypto.encryption.aes.enabled", serverCryptoAES);
304  }
305
306  /**
307   * Sets up a RPC Server and a Client. Does a RPC checks the result. If an exception is thrown from
308   * the stub, this function will throw root cause of that exception.
309   */
310  private void callRpcService(User serverUser, User clientUser) throws Exception {
311    SecurityInfo securityInfoMock = Mockito.mock(SecurityInfo.class);
312    Mockito.when(securityInfoMock.getServerPrincipals())
313      .thenReturn(Collections.singletonList(HBaseKerberosUtils.KRB_PRINCIPAL));
314    SecurityInfo.addInfo("TestProtobufRpcProto", securityInfoMock);
315
316    InetSocketAddress isa = new InetSocketAddress(HOST, 0);
317
318    RpcServer rpcServer = serverUser.getUGI()
319      .doAs((PrivilegedExceptionAction<
320        RpcServer>) () -> RpcServerFactory.createRpcServer(null, "AbstractTestSecureIPC",
321          Lists.newArrayList(
322            new RpcServer.BlockingServiceAndInterface((BlockingService) SERVICE, null)),
323          isa, serverConf, new FifoRpcScheduler(serverConf, 1)));
324    rpcServer.start();
325    try (RpcClient rpcClient =
326      RpcClientFactory.createClient(clientConf, HConstants.DEFAULT_CLUSTER_ID.toString())) {
327      BlockingInterface stub =
328        newBlockingStub(rpcClient, rpcServer.getListenerAddress(), clientUser);
329      TestThread th1 = new TestThread(stub);
330      final Throwable exception[] = new Throwable[1];
331      Collections.synchronizedList(new ArrayList<Throwable>());
332      Thread.UncaughtExceptionHandler exceptionHandler = new Thread.UncaughtExceptionHandler() {
333        @Override
334        public void uncaughtException(Thread th, Throwable ex) {
335          exception[0] = ex;
336        }
337      };
338      th1.setUncaughtExceptionHandler(exceptionHandler);
339      th1.start();
340      th1.join();
341      if (exception[0] != null) {
342        // throw root cause.
343        while (exception[0].getCause() != null) {
344          exception[0] = exception[0].getCause();
345        }
346        throw (Exception) exception[0];
347      }
348    } finally {
349      rpcServer.stop();
350    }
351  }
352
353  private void callRpcService(User clientUser) throws Exception {
354    callRpcService(User.create(ugi), clientUser);
355  }
356
357  private void callRpcService() throws Exception {
358    callRpcService(User.create(ugi));
359  }
360
361  public static class TestThread extends Thread {
362    private final BlockingInterface stub;
363
364    public TestThread(BlockingInterface stub) {
365      this.stub = stub;
366    }
367
368    @Override
369    public void run() {
370      try {
371        int[] messageSize = new int[] { 100, 1000, 10000 };
372        for (int i = 0; i < messageSize.length; i++) {
373          String input = RandomStringUtils.insecure().next(messageSize[i]);
374          String result =
375            stub.echo(null, TestProtos.EchoRequestProto.newBuilder().setMessage(input).build())
376              .getMessage();
377          assertEquals(input, result);
378        }
379      } catch (org.apache.hbase.thirdparty.com.google.protobuf.ServiceException e) {
380        throw new RuntimeException(e);
381      }
382    }
383  }
384}