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