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