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