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.security.token.Token;
054import org.apache.hadoop.security.token.TokenIdentifier;
055import org.junit.ClassRule;
056import org.junit.Rule;
057import org.junit.Test;
058import org.junit.experimental.categories.Category;
059import org.junit.rules.ExpectedException;
060import org.mockito.Mockito;
061import org.slf4j.Logger;
062import org.slf4j.LoggerFactory;
063
064import org.apache.hbase.thirdparty.com.google.common.base.Strings;
065
066@Category({ SecurityTests.class, SmallTests.class })
067public class TestHBaseSaslRpcClient {
068
069  @ClassRule
070  public static final HBaseClassTestRule CLASS_RULE =
071    HBaseClassTestRule.forClass(TestHBaseSaslRpcClient.class);
072
073  static {
074    System.setProperty("java.security.krb5.realm", "DOMAIN.COM");
075    System.setProperty("java.security.krb5.kdc", "DOMAIN.COM");
076  }
077
078  static final String DEFAULT_USER_NAME = "principal";
079  static final String DEFAULT_USER_PASSWORD = "password";
080
081  private static final Logger LOG = LoggerFactory.getLogger(TestHBaseSaslRpcClient.class);
082
083  @Rule
084  public ExpectedException exception = ExpectedException.none();
085
086  @Test
087  public void testSaslClientUsesGivenRpcProtection() throws Exception {
088    Token<? extends TokenIdentifier> token =
089      createTokenMockWithCredentials(DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD);
090    DigestSaslClientAuthenticationProvider provider = new DigestSaslClientAuthenticationProvider();
091    for (SaslUtil.QualityOfProtection qop : SaslUtil.QualityOfProtection.values()) {
092      String negotiatedQop = new HBaseSaslRpcClient(HBaseConfiguration.create(), provider, token,
093        Mockito.mock(InetAddress.class), Mockito.mock(SecurityInfo.class), false, qop.name(),
094        false) {
095        public String getQop() {
096          return saslProps.get(Sasl.QOP);
097        }
098      }.getQop();
099      assertEquals(negotiatedQop, qop.getSaslQop());
100    }
101  }
102
103  @Test
104  public void testDigestSaslClientCallbackHandler() throws UnsupportedCallbackException {
105    final Token<? extends TokenIdentifier> token = createTokenMock();
106    when(token.getIdentifier()).thenReturn(Bytes.toBytes(DEFAULT_USER_NAME));
107    when(token.getPassword()).thenReturn(Bytes.toBytes(DEFAULT_USER_PASSWORD));
108
109    final NameCallback nameCallback = mock(NameCallback.class);
110    final PasswordCallback passwordCallback = mock(PasswordCallback.class);
111    final RealmCallback realmCallback = mock(RealmCallback.class);
112
113    // We can provide a realmCallback, but HBase presently does nothing with it.
114    Callback[] callbackArray = { nameCallback, passwordCallback, realmCallback };
115    final DigestSaslClientCallbackHandler saslClCallbackHandler =
116      new DigestSaslClientCallbackHandler(token);
117    saslClCallbackHandler.handle(callbackArray);
118    verify(nameCallback).setName(anyString());
119    verify(passwordCallback).setPassword(any());
120  }
121
122  @Test
123  public void testDigestSaslClientCallbackHandlerWithException() {
124    final Token<? extends TokenIdentifier> token = createTokenMock();
125    when(token.getIdentifier()).thenReturn(Bytes.toBytes(DEFAULT_USER_NAME));
126    when(token.getPassword()).thenReturn(Bytes.toBytes(DEFAULT_USER_PASSWORD));
127    final DigestSaslClientCallbackHandler saslClCallbackHandler =
128      new DigestSaslClientCallbackHandler(token);
129    try {
130      saslClCallbackHandler.handle(new Callback[] { mock(TextOutputCallback.class) });
131    } catch (UnsupportedCallbackException expEx) {
132      // expected
133    } catch (Exception ex) {
134      fail("testDigestSaslClientCallbackHandlerWithException error : " + ex.getMessage());
135    }
136  }
137
138  @Test
139  public void testHBaseSaslRpcClientCreation() throws Exception {
140    // creation kerberos principal check section
141    // Note this is mocked in a way that doesn't care about principal names
142    assertFalse(assertSuccessCreationKerberos());
143
144    // creation digest principal check section
145    assertFalse(assertSuccessCreationDigestPrincipal(null, null));
146    assertFalse(assertSuccessCreationDigestPrincipal("", ""));
147    assertFalse(assertSuccessCreationDigestPrincipal("", null));
148    assertFalse(assertSuccessCreationDigestPrincipal(null, ""));
149    assertTrue(assertSuccessCreationDigestPrincipal(DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD));
150
151    // creation simple principal check section
152    // Note this is mocked in a way that doesn't care about principal names
153    assertFalse(assertSuccessCreationSimple());
154
155    // exceptions check section
156    assertTrue(assertIOExceptionThenSaslClientIsNull(DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD));
157    assertTrue(
158      assertIOExceptionWhenGetStreamsBeforeConnectCall(DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD));
159  }
160
161  @Test
162  public void testAuthMethodReadWrite() throws IOException {
163    DataInputBuffer in = new DataInputBuffer();
164    DataOutputBuffer out = new DataOutputBuffer();
165
166    assertAuthMethodRead(in, AuthMethod.SIMPLE);
167    assertAuthMethodRead(in, AuthMethod.KERBEROS);
168    assertAuthMethodRead(in, AuthMethod.DIGEST);
169
170    assertAuthMethodWrite(out, AuthMethod.SIMPLE);
171    assertAuthMethodWrite(out, AuthMethod.KERBEROS);
172    assertAuthMethodWrite(out, AuthMethod.DIGEST);
173  }
174
175  private void assertAuthMethodRead(DataInputBuffer in, AuthMethod authMethod) throws IOException {
176    in.reset(new byte[] { authMethod.code }, 1);
177    assertEquals(authMethod, AuthMethod.read(in));
178  }
179
180  private void assertAuthMethodWrite(DataOutputBuffer out, AuthMethod authMethod)
181    throws IOException {
182    authMethod.write(out);
183    assertEquals(authMethod.code, out.getData()[0]);
184    out.reset();
185  }
186
187  private boolean assertIOExceptionWhenGetStreamsBeforeConnectCall(String principal,
188    String password) throws IOException {
189    boolean inState = false;
190    boolean outState = false;
191
192    DigestSaslClientAuthenticationProvider provider = new DigestSaslClientAuthenticationProvider() {
193      @Override
194      public SaslClient createClient(Configuration conf, InetAddress serverAddress,
195        SecurityInfo securityInfo, Token<? extends TokenIdentifier> token, boolean fallbackAllowed,
196        Map<String, String> saslProps) {
197        return Mockito.mock(SaslClient.class);
198      }
199    };
200    HBaseSaslRpcClient rpcClient = new HBaseSaslRpcClient(HBaseConfiguration.create(), provider,
201      createTokenMockWithCredentials(principal, password), Mockito.mock(InetAddress.class),
202      Mockito.mock(SecurityInfo.class), false);
203
204    try {
205      rpcClient.getInputStream();
206    } catch (IOException ex) {
207      // Sasl authentication exchange hasn't completed yet
208      inState = true;
209    }
210
211    try {
212      rpcClient.getOutputStream();
213    } catch (IOException ex) {
214      // Sasl authentication exchange hasn't completed yet
215      outState = true;
216    }
217
218    return inState && outState;
219  }
220
221  private boolean assertIOExceptionThenSaslClientIsNull(String principal, String password) {
222    try {
223      DigestSaslClientAuthenticationProvider provider =
224        new DigestSaslClientAuthenticationProvider() {
225          @Override
226          public SaslClient createClient(Configuration conf, InetAddress serverAddress,
227            SecurityInfo securityInfo, Token<? extends TokenIdentifier> token,
228            boolean fallbackAllowed, Map<String, String> saslProps) {
229            return null;
230          }
231        };
232      new HBaseSaslRpcClient(HBaseConfiguration.create(), provider,
233        createTokenMockWithCredentials(principal, password), Mockito.mock(InetAddress.class),
234        Mockito.mock(SecurityInfo.class), false);
235      return false;
236    } catch (IOException ex) {
237      return true;
238    }
239  }
240
241  private boolean assertSuccessCreationKerberos() {
242    HBaseSaslRpcClient rpcClient = null;
243    try {
244      // createSaslRpcClientForKerberos is mocked in a way that doesn't care about principal names
245      rpcClient = createSaslRpcClientForKerberos();
246    } catch (Exception ex) {
247      LOG.error(ex.getMessage(), ex);
248    }
249    return rpcClient != null;
250  }
251
252  private boolean assertSuccessCreationDigestPrincipal(String principal, String password) {
253    HBaseSaslRpcClient rpcClient = null;
254    try {
255      rpcClient = new HBaseSaslRpcClient(HBaseConfiguration.create(),
256        new DigestSaslClientAuthenticationProvider(),
257        createTokenMockWithCredentials(principal, password), Mockito.mock(InetAddress.class),
258        Mockito.mock(SecurityInfo.class), false);
259    } catch (Exception ex) {
260      LOG.error(ex.getMessage(), ex);
261    }
262    return rpcClient != null;
263  }
264
265  private boolean assertSuccessCreationSimple() {
266    HBaseSaslRpcClient rpcClient = null;
267    try {
268      rpcClient = createSaslRpcClientSimple();
269    } catch (Exception ex) {
270      LOG.error(ex.getMessage(), ex);
271    }
272    return rpcClient != null;
273  }
274
275  private HBaseSaslRpcClient createSaslRpcClientForKerberos() throws IOException {
276    return new HBaseSaslRpcClient(HBaseConfiguration.create(),
277      new GssSaslClientAuthenticationProvider(), createTokenMock(), Mockito.mock(InetAddress.class),
278      Mockito.mock(SecurityInfo.class), false);
279  }
280
281  private Token<? extends TokenIdentifier> createTokenMockWithCredentials(String principal,
282    String password) throws IOException {
283    Token<? extends TokenIdentifier> token = createTokenMock();
284    if (!Strings.isNullOrEmpty(principal) && !Strings.isNullOrEmpty(password)) {
285      when(token.getIdentifier()).thenReturn(Bytes.toBytes(DEFAULT_USER_NAME));
286      when(token.getPassword()).thenReturn(Bytes.toBytes(DEFAULT_USER_PASSWORD));
287    }
288    return token;
289  }
290
291  private HBaseSaslRpcClient createSaslRpcClientSimple() throws IOException {
292    return new HBaseSaslRpcClient(HBaseConfiguration.create(),
293      new SimpleSaslClientAuthenticationProvider(), createTokenMock(),
294      Mockito.mock(InetAddress.class), Mockito.mock(SecurityInfo.class), false);
295  }
296
297  @SuppressWarnings("unchecked")
298  private Token<? extends TokenIdentifier> createTokenMock() {
299    return mock(Token.class);
300  }
301}