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.junit.Assert.assertEquals;
021import static org.junit.Assert.assertFalse;
022import static org.junit.Assert.assertTrue;
023import static org.junit.Assert.fail;
024import static org.mockito.ArgumentMatchers.any;
025import static org.mockito.ArgumentMatchers.anyString;
026import static org.mockito.Mockito.mock;
027import static org.mockito.Mockito.verify;
028import static org.mockito.Mockito.when;
029
030import java.io.IOException;
031import java.net.InetAddress;
032import java.util.Map;
033
034import javax.security.auth.callback.Callback;
035import javax.security.auth.callback.NameCallback;
036import javax.security.auth.callback.PasswordCallback;
037import javax.security.auth.callback.TextOutputCallback;
038import javax.security.auth.callback.UnsupportedCallbackException;
039import javax.security.sasl.RealmCallback;
040import javax.security.sasl.Sasl;
041import javax.security.sasl.SaslClient;
042
043import org.apache.hadoop.conf.Configuration;
044import org.apache.hadoop.hbase.HBaseClassTestRule;
045import org.apache.hadoop.hbase.HBaseConfiguration;
046import org.apache.hadoop.hbase.security.provider.DigestSaslClientAuthenticationProvider;
047import org.apache.hadoop.hbase.security.provider.DigestSaslClientAuthenticationProvider.DigestSaslClientCallbackHandler;
048import org.apache.hadoop.hbase.security.provider.GssSaslClientAuthenticationProvider;
049import org.apache.hadoop.hbase.security.provider.SimpleSaslClientAuthenticationProvider;
050import org.apache.hadoop.hbase.testclassification.SecurityTests;
051import org.apache.hadoop.hbase.testclassification.SmallTests;
052import org.apache.hadoop.hbase.util.Bytes;
053import org.apache.hadoop.io.DataInputBuffer;
054import org.apache.hadoop.io.DataOutputBuffer;
055import org.apache.hadoop.io.WritableUtils;
056import org.apache.hadoop.security.token.Token;
057import org.apache.hadoop.security.token.TokenIdentifier;
058import org.apache.log4j.Level;
059import org.apache.log4j.Logger;
060import org.junit.Assert;
061import org.junit.BeforeClass;
062import org.junit.ClassRule;
063import org.junit.Rule;
064import org.junit.Test;
065import org.junit.experimental.categories.Category;
066import org.junit.rules.ExpectedException;
067import org.mockito.Mockito;
068
069import org.apache.hbase.thirdparty.com.google.common.base.Strings;
070
071@Category({SecurityTests.class, SmallTests.class})
072public class TestHBaseSaslRpcClient {
073
074  @ClassRule
075  public static final HBaseClassTestRule CLASS_RULE =
076      HBaseClassTestRule.forClass(TestHBaseSaslRpcClient.class);
077
078  static {
079    System.setProperty("java.security.krb5.realm", "DOMAIN.COM");
080    System.setProperty("java.security.krb5.kdc", "DOMAIN.COM");
081  }
082
083  static final String DEFAULT_USER_NAME = "principal";
084  static final String DEFAULT_USER_PASSWORD = "password";
085
086  private static final Logger LOG = Logger.getLogger(TestHBaseSaslRpcClient.class);
087
088
089  @Rule
090  public ExpectedException exception = ExpectedException.none();
091
092  @BeforeClass
093  public static void before() {
094    Logger.getRootLogger().setLevel(Level.DEBUG);
095  }
096
097  @Test
098  public void testSaslClientUsesGivenRpcProtection() throws Exception {
099    Token<? extends TokenIdentifier> token = createTokenMockWithCredentials(DEFAULT_USER_NAME,
100        DEFAULT_USER_PASSWORD);
101    DigestSaslClientAuthenticationProvider provider = new DigestSaslClientAuthenticationProvider();
102    for (SaslUtil.QualityOfProtection qop : SaslUtil.QualityOfProtection.values()) {
103      String negotiatedQop = new HBaseSaslRpcClient(HBaseConfiguration.create(), provider, token,
104          Mockito.mock(InetAddress.class), Mockito.mock(SecurityInfo.class), false, qop.name(),
105          false) {
106        public String getQop() {
107          return saslProps.get(Sasl.QOP);
108        }
109      }.getQop();
110      assertEquals(negotiatedQop, qop.getSaslQop());
111    }
112  }
113
114  @Test
115  public void testDigestSaslClientCallbackHandler() throws UnsupportedCallbackException {
116    final Token<? extends TokenIdentifier> token = createTokenMock();
117    when(token.getIdentifier()).thenReturn(Bytes.toBytes(DEFAULT_USER_NAME));
118    when(token.getPassword()).thenReturn(Bytes.toBytes(DEFAULT_USER_PASSWORD));
119
120    final NameCallback nameCallback = mock(NameCallback.class);
121    final PasswordCallback passwordCallback = mock(PasswordCallback.class);
122    final RealmCallback realmCallback = mock(RealmCallback.class);
123
124    // We can provide a realmCallback, but HBase presently does nothing with it.
125    Callback[] callbackArray = {nameCallback, passwordCallback, realmCallback};
126    final DigestSaslClientCallbackHandler saslClCallbackHandler =
127        new DigestSaslClientCallbackHandler(token);
128    saslClCallbackHandler.handle(callbackArray);
129    verify(nameCallback).setName(anyString());
130    verify(passwordCallback).setPassword(any());
131  }
132
133  @Test
134  public void testDigestSaslClientCallbackHandlerWithException() {
135    final Token<? extends TokenIdentifier> token = createTokenMock();
136    when(token.getIdentifier()).thenReturn(Bytes.toBytes(DEFAULT_USER_NAME));
137    when(token.getPassword()).thenReturn(Bytes.toBytes(DEFAULT_USER_PASSWORD));
138    final DigestSaslClientCallbackHandler saslClCallbackHandler =
139        new DigestSaslClientCallbackHandler(token);
140    try {
141      saslClCallbackHandler.handle(new Callback[] { mock(TextOutputCallback.class) });
142    } catch (UnsupportedCallbackException expEx) {
143      //expected
144    } catch (Exception ex) {
145      fail("testDigestSaslClientCallbackHandlerWithException error : " + ex.getMessage());
146    }
147  }
148
149  @Test
150  public void testHBaseSaslRpcClientCreation() throws Exception {
151    //creation kerberos principal check section
152    assertFalse(assertSuccessCreationKerberosPrincipal(null));
153    assertFalse(assertSuccessCreationKerberosPrincipal("DOMAIN.COM"));
154    assertFalse(assertSuccessCreationKerberosPrincipal("principal/DOMAIN.COM"));
155    if (!assertSuccessCreationKerberosPrincipal("principal/localhost@DOMAIN.COM")) {
156      // XXX: This can fail if kerberos support in the OS is not sane, see HBASE-10107.
157      // For now, don't assert, just warn
158      LOG.warn("Could not create a SASL client with valid Kerberos credential");
159    }
160
161    //creation digest principal check section
162    assertFalse(assertSuccessCreationDigestPrincipal(null, null));
163    assertFalse(assertSuccessCreationDigestPrincipal("", ""));
164    assertFalse(assertSuccessCreationDigestPrincipal("", null));
165    assertFalse(assertSuccessCreationDigestPrincipal(null, ""));
166    assertTrue(assertSuccessCreationDigestPrincipal(DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD));
167
168    //creation simple principal check section
169    assertFalse(assertSuccessCreationSimplePrincipal("", ""));
170    assertFalse(assertSuccessCreationSimplePrincipal(null, null));
171    assertFalse(assertSuccessCreationSimplePrincipal(DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD));
172
173    //exceptions check section
174    assertTrue(assertIOExceptionThenSaslClientIsNull(DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD));
175    assertTrue(assertIOExceptionWhenGetStreamsBeforeConnectCall(
176        DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD));
177  }
178
179  @Test
180  public void testAuthMethodReadWrite() throws IOException {
181    DataInputBuffer in = new DataInputBuffer();
182    DataOutputBuffer out = new DataOutputBuffer();
183
184    assertAuthMethodRead(in, AuthMethod.SIMPLE);
185    assertAuthMethodRead(in, AuthMethod.KERBEROS);
186    assertAuthMethodRead(in, AuthMethod.DIGEST);
187
188    assertAuthMethodWrite(out, AuthMethod.SIMPLE);
189    assertAuthMethodWrite(out, AuthMethod.KERBEROS);
190    assertAuthMethodWrite(out, AuthMethod.DIGEST);
191  }
192
193  private void assertAuthMethodRead(DataInputBuffer in, AuthMethod authMethod)
194      throws IOException {
195    in.reset(new byte[] {authMethod.code}, 1);
196    assertEquals(authMethod, AuthMethod.read(in));
197  }
198
199  private void assertAuthMethodWrite(DataOutputBuffer out, AuthMethod authMethod)
200      throws IOException {
201    authMethod.write(out);
202    assertEquals(authMethod.code, out.getData()[0]);
203    out.reset();
204  }
205
206  private boolean assertIOExceptionWhenGetStreamsBeforeConnectCall(String principal,
207      String password) throws IOException {
208    boolean inState = false;
209    boolean outState = false;
210
211    DigestSaslClientAuthenticationProvider provider = new DigestSaslClientAuthenticationProvider() {
212      @Override
213      public SaslClient createClient(Configuration conf, InetAddress serverAddress,
214          SecurityInfo securityInfo, Token<? extends TokenIdentifier> token,
215          boolean fallbackAllowed, Map<String, String> saslProps) {
216        return Mockito.mock(SaslClient.class);
217      }
218    };
219    HBaseSaslRpcClient rpcClient = new HBaseSaslRpcClient(HBaseConfiguration.create(), provider,
220        createTokenMockWithCredentials(principal, password), Mockito.mock(InetAddress.class),
221        Mockito.mock(SecurityInfo.class), false);
222
223    try {
224      rpcClient.getInputStream();
225    } catch(IOException ex) {
226      //Sasl authentication exchange hasn't completed yet
227      inState = true;
228    }
229
230    try {
231      rpcClient.getOutputStream();
232    } catch(IOException ex) {
233      //Sasl authentication exchange hasn't completed yet
234      outState = true;
235    }
236
237    return inState && outState;
238  }
239
240  private boolean assertIOExceptionThenSaslClientIsNull(String principal, String password) {
241    try {
242      DigestSaslClientAuthenticationProvider provider =
243          new DigestSaslClientAuthenticationProvider() {
244        @Override
245        public SaslClient createClient(Configuration conf, InetAddress serverAddress,
246            SecurityInfo securityInfo,
247            Token<? extends TokenIdentifier> token, boolean fallbackAllowed,
248            Map<String, String> saslProps) {
249          return null;
250        }
251      };
252      new HBaseSaslRpcClient(HBaseConfiguration.create(), provider,
253          createTokenMockWithCredentials(principal, password), Mockito.mock(InetAddress.class),
254          Mockito.mock(SecurityInfo.class), false);
255      return false;
256    } catch (IOException ex) {
257      return true;
258    }
259  }
260
261  private boolean assertSuccessCreationKerberosPrincipal(String principal) {
262    HBaseSaslRpcClient rpcClient = null;
263    try {
264      rpcClient = createSaslRpcClientForKerberos(principal);
265    } catch(Exception ex) {
266      LOG.error(ex.getMessage(), ex);
267    }
268    return rpcClient != null;
269  }
270
271  private boolean assertSuccessCreationDigestPrincipal(String principal, String password) {
272    HBaseSaslRpcClient rpcClient = null;
273    try {
274      rpcClient = new HBaseSaslRpcClient(HBaseConfiguration.create(),
275          new DigestSaslClientAuthenticationProvider(),
276          createTokenMockWithCredentials(principal, password), Mockito.mock(InetAddress.class),
277          Mockito.mock(SecurityInfo.class), false);
278    } catch(Exception ex) {
279      LOG.error(ex.getMessage(), ex);
280    }
281    return rpcClient != null;
282  }
283
284  private boolean assertSuccessCreationSimplePrincipal(String principal, String password) {
285    HBaseSaslRpcClient rpcClient = null;
286    try {
287      rpcClient = createSaslRpcClientSimple(principal, password);
288    } catch(Exception ex) {
289      LOG.error(ex.getMessage(), ex);
290    }
291    return rpcClient != null;
292  }
293
294  private HBaseSaslRpcClient createSaslRpcClientForKerberos(String principal)
295      throws IOException {
296    return new HBaseSaslRpcClient(HBaseConfiguration.create(),
297        new GssSaslClientAuthenticationProvider(), createTokenMock(),
298        Mockito.mock(InetAddress.class), Mockito.mock(SecurityInfo.class), false);
299  }
300
301  private Token<? extends TokenIdentifier> createTokenMockWithCredentials(
302      String principal, String password)
303      throws IOException {
304    Token<? extends TokenIdentifier> token = createTokenMock();
305    if (!Strings.isNullOrEmpty(principal) && !Strings.isNullOrEmpty(password)) {
306      when(token.getIdentifier()).thenReturn(Bytes.toBytes(DEFAULT_USER_NAME));
307      when(token.getPassword()).thenReturn(Bytes.toBytes(DEFAULT_USER_PASSWORD));
308    }
309    return token;
310  }
311
312  private HBaseSaslRpcClient createSaslRpcClientSimple(String principal, String password)
313      throws IOException {
314    return new HBaseSaslRpcClient(HBaseConfiguration.create(),
315        new SimpleSaslClientAuthenticationProvider(), createTokenMock(),
316        Mockito.mock(InetAddress.class), Mockito.mock(SecurityInfo.class), false);
317  }
318
319  @SuppressWarnings("unchecked")
320  private Token<? extends TokenIdentifier> createTokenMock() {
321    return mock(Token.class);
322  }
323
324  @Test(expected = IOException.class)
325   public void testFailedEvaluateResponse() throws IOException {
326    //prep mockin the SaslClient
327    SimpleSaslClientAuthenticationProvider mockProvider =
328      Mockito.mock(SimpleSaslClientAuthenticationProvider.class);
329    SaslClient mockClient = Mockito.mock(SaslClient.class);
330    Assert.assertNotNull(mockProvider);
331    Assert.assertNotNull(mockClient);
332    Mockito.when(mockProvider.createClient(Mockito.any(), Mockito.any(), Mockito.any(),
333      Mockito.any(), Mockito.anyBoolean(), Mockito.any())).thenReturn(mockClient);
334    HBaseSaslRpcClient rpcClient = new HBaseSaslRpcClient(HBaseConfiguration.create(),
335      mockProvider, createTokenMock(),
336      Mockito.mock(InetAddress.class), Mockito.mock(SecurityInfo.class), false);
337
338    //simulate getting an error from a failed saslServer.evaluateResponse
339    DataOutputBuffer errorBuffer = new DataOutputBuffer();
340    errorBuffer.writeInt(SaslStatus.ERROR.state);
341    WritableUtils.writeString(errorBuffer, IOException.class.getName());
342    WritableUtils.writeString(errorBuffer, "Invalid Token");
343
344    DataInputBuffer in = new DataInputBuffer();
345    in.reset(errorBuffer.getData(), 0, errorBuffer.getLength());
346    DataOutputBuffer out = new DataOutputBuffer();
347
348    //simulate that authentication exchange has completed quickly after sending the token
349    Mockito.when(mockClient.isComplete()).thenReturn(true);
350    rpcClient.saslConnect(in, out);
351  }
352}