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