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.Matchers.any;
025import static org.mockito.Matchers.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 javax.security.auth.callback.Callback;
032import javax.security.auth.callback.CallbackHandler;
033import javax.security.auth.callback.NameCallback;
034import javax.security.auth.callback.PasswordCallback;
035import javax.security.auth.callback.TextOutputCallback;
036import javax.security.auth.callback.UnsupportedCallbackException;
037import javax.security.sasl.RealmCallback;
038import javax.security.sasl.RealmChoiceCallback;
039import javax.security.sasl.Sasl;
040import javax.security.sasl.SaslClient;
041import org.apache.hadoop.hbase.HBaseClassTestRule;
042import org.apache.hadoop.hbase.security.AbstractHBaseSaslRpcClient.SaslClientCallbackHandler;
043import org.apache.hadoop.hbase.testclassification.SecurityTests;
044import org.apache.hadoop.hbase.testclassification.SmallTests;
045import org.apache.hadoop.hbase.util.Bytes;
046import org.apache.hadoop.io.DataInputBuffer;
047import org.apache.hadoop.io.DataOutputBuffer;
048import org.apache.hadoop.security.token.Token;
049import org.apache.hadoop.security.token.TokenIdentifier;
050import org.apache.log4j.Level;
051import org.apache.log4j.Logger;
052import org.junit.BeforeClass;
053import org.junit.ClassRule;
054import org.junit.Rule;
055import org.junit.Test;
056import org.junit.experimental.categories.Category;
057import org.junit.rules.ExpectedException;
058import org.mockito.Mockito;
059
060import org.apache.hbase.thirdparty.com.google.common.base.Strings;
061
062@Category({SecurityTests.class, SmallTests.class})
063public class TestHBaseSaslRpcClient {
064
065  @ClassRule
066  public static final HBaseClassTestRule CLASS_RULE =
067      HBaseClassTestRule.forClass(TestHBaseSaslRpcClient.class);
068
069  static {
070    System.setProperty("java.security.krb5.realm", "DOMAIN.COM");
071    System.setProperty("java.security.krb5.kdc", "DOMAIN.COM");
072  }
073
074  static final String DEFAULT_USER_NAME = "principal";
075  static final String DEFAULT_USER_PASSWORD = "password";
076
077  private static final Logger LOG = Logger.getLogger(TestHBaseSaslRpcClient.class);
078
079
080  @Rule
081  public ExpectedException exception = ExpectedException.none();
082
083  @BeforeClass
084  public static void before() {
085    Logger.getRootLogger().setLevel(Level.DEBUG);
086  }
087
088  @Test
089  public void testSaslClientUsesGivenRpcProtection() throws Exception {
090    Token<? extends TokenIdentifier> token = createTokenMockWithCredentials(DEFAULT_USER_NAME,
091        DEFAULT_USER_PASSWORD);
092    for (SaslUtil.QualityOfProtection qop : SaslUtil.QualityOfProtection.values()) {
093      String negotiatedQop = new HBaseSaslRpcClient(AuthMethod.DIGEST, token,
094          "principal/host@DOMAIN.COM", false, qop.name(), 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 testSaslClientCallbackHandler() 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    final RealmChoiceCallback realmChoiceCallback = mock(RealmChoiceCallback.class);
113
114    Callback[] callbackArray = {nameCallback, passwordCallback, realmCallback, realmChoiceCallback};
115    final SaslClientCallbackHandler saslClCallbackHandler = new SaslClientCallbackHandler(token);
116    saslClCallbackHandler.handle(callbackArray);
117    verify(nameCallback).setName(anyString());
118    verify(realmCallback).setText(any());
119    verify(passwordCallback).setPassword(any());
120  }
121
122  @Test
123  public void testSaslClientCallbackHandlerWithException() {
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 SaslClientCallbackHandler saslClCallbackHandler = new SaslClientCallbackHandler(token);
128    try {
129      saslClCallbackHandler.handle(new Callback[] { mock(TextOutputCallback.class) });
130    } catch (UnsupportedCallbackException expEx) {
131      //expected
132    } catch (Exception ex) {
133      fail("testSaslClientCallbackHandlerWithException error : " + ex.getMessage());
134    }
135  }
136
137  @Test
138  public void testHBaseSaslRpcClientCreation() throws Exception {
139    //creation kerberos principal check section
140    assertFalse(assertSuccessCreationKerberosPrincipal(null));
141    assertFalse(assertSuccessCreationKerberosPrincipal("DOMAIN.COM"));
142    assertFalse(assertSuccessCreationKerberosPrincipal("principal/DOMAIN.COM"));
143    if (!assertSuccessCreationKerberosPrincipal("principal/localhost@DOMAIN.COM")) {
144      // XXX: This can fail if kerberos support in the OS is not sane, see HBASE-10107.
145      // For now, don't assert, just warn
146      LOG.warn("Could not create a SASL client with valid Kerberos credential");
147    }
148
149    //creation digest principal check section
150    assertFalse(assertSuccessCreationDigestPrincipal(null, null));
151    assertFalse(assertSuccessCreationDigestPrincipal("", ""));
152    assertFalse(assertSuccessCreationDigestPrincipal("", null));
153    assertFalse(assertSuccessCreationDigestPrincipal(null, ""));
154    assertTrue(assertSuccessCreationDigestPrincipal(DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD));
155
156    //creation simple principal check section
157    assertFalse(assertSuccessCreationSimplePrincipal("", ""));
158    assertFalse(assertSuccessCreationSimplePrincipal(null, null));
159    assertFalse(assertSuccessCreationSimplePrincipal(DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD));
160
161    //exceptions check section
162    assertTrue(assertIOExceptionThenSaslClientIsNull(DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD));
163    assertTrue(assertIOExceptionWhenGetStreamsBeforeConnectCall(
164        DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD));
165  }
166
167  @Test
168  public void testAuthMethodReadWrite() throws IOException {
169    DataInputBuffer in = new DataInputBuffer();
170    DataOutputBuffer out = new DataOutputBuffer();
171
172    assertAuthMethodRead(in, AuthMethod.SIMPLE);
173    assertAuthMethodRead(in, AuthMethod.KERBEROS);
174    assertAuthMethodRead(in, AuthMethod.DIGEST);
175
176    assertAuthMethodWrite(out, AuthMethod.SIMPLE);
177    assertAuthMethodWrite(out, AuthMethod.KERBEROS);
178    assertAuthMethodWrite(out, AuthMethod.DIGEST);
179  }
180
181  private void assertAuthMethodRead(DataInputBuffer in, AuthMethod authMethod)
182      throws IOException {
183    in.reset(new byte[] {authMethod.code}, 1);
184    assertEquals(authMethod, AuthMethod.read(in));
185  }
186
187  private void assertAuthMethodWrite(DataOutputBuffer out, AuthMethod authMethod)
188      throws IOException {
189    authMethod.write(out);
190    assertEquals(authMethod.code, out.getData()[0]);
191    out.reset();
192  }
193
194  private boolean assertIOExceptionWhenGetStreamsBeforeConnectCall(String principal,
195      String password) throws IOException {
196    boolean inState = false;
197    boolean outState = false;
198
199    HBaseSaslRpcClient rpcClient = new HBaseSaslRpcClient(AuthMethod.DIGEST,
200        createTokenMockWithCredentials(principal, password), principal, false) {
201      @Override
202      public SaslClient createDigestSaslClient(String[] mechanismNames,
203          String saslDefaultRealm, CallbackHandler saslClientCallbackHandler)
204              throws IOException {
205        return Mockito.mock(SaslClient.class);
206      }
207
208      @Override
209      public SaslClient createKerberosSaslClient(String[] mechanismNames,
210          String userFirstPart, String userSecondPart) throws IOException {
211        return Mockito.mock(SaslClient.class);
212      }
213    };
214
215    try {
216      rpcClient.getInputStream();
217    } catch(IOException ex) {
218      //Sasl authentication exchange hasn't completed yet
219      inState = true;
220    }
221
222    try {
223      rpcClient.getOutputStream();
224    } catch(IOException ex) {
225      //Sasl authentication exchange hasn't completed yet
226      outState = true;
227    }
228
229    return inState && outState;
230  }
231
232  private boolean assertIOExceptionThenSaslClientIsNull(String principal, String password) {
233    try {
234      new HBaseSaslRpcClient(AuthMethod.DIGEST,
235          createTokenMockWithCredentials(principal, password), principal, false) {
236        @Override
237        public SaslClient createDigestSaslClient(String[] mechanismNames,
238            String saslDefaultRealm, CallbackHandler saslClientCallbackHandler)
239                throws IOException {
240          return null;
241        }
242
243        @Override
244        public SaslClient createKerberosSaslClient(String[] mechanismNames,
245            String userFirstPart, String userSecondPart) throws IOException {
246          return null;
247        }
248      };
249      return false;
250    } catch (IOException ex) {
251      return true;
252    }
253  }
254
255  private boolean assertSuccessCreationKerberosPrincipal(String principal) {
256    HBaseSaslRpcClient rpcClient = null;
257    try {
258      rpcClient = createSaslRpcClientForKerberos(principal);
259    } catch(Exception ex) {
260      LOG.error(ex.getMessage(), ex);
261    }
262    return rpcClient != null;
263  }
264
265  private boolean assertSuccessCreationDigestPrincipal(String principal, String password) {
266    HBaseSaslRpcClient rpcClient = null;
267    try {
268      rpcClient = new HBaseSaslRpcClient(AuthMethod.DIGEST,
269          createTokenMockWithCredentials(principal, password), principal, false);
270    } catch(Exception ex) {
271      LOG.error(ex.getMessage(), ex);
272    }
273    return rpcClient != null;
274  }
275
276  private boolean assertSuccessCreationSimplePrincipal(String principal, String password) {
277    HBaseSaslRpcClient rpcClient = null;
278    try {
279      rpcClient = createSaslRpcClientSimple(principal, password);
280    } catch(Exception ex) {
281      LOG.error(ex.getMessage(), ex);
282    }
283    return rpcClient != null;
284  }
285
286  private HBaseSaslRpcClient createSaslRpcClientForKerberos(String principal)
287      throws IOException {
288    return new HBaseSaslRpcClient(AuthMethod.KERBEROS, createTokenMock(), principal, false);
289  }
290
291  private Token<? extends TokenIdentifier> createTokenMockWithCredentials(
292      String principal, String password)
293      throws IOException {
294    Token<? extends TokenIdentifier> token = createTokenMock();
295    if (!Strings.isNullOrEmpty(principal) && !Strings.isNullOrEmpty(password)) {
296      when(token.getIdentifier()).thenReturn(Bytes.toBytes(DEFAULT_USER_NAME));
297      when(token.getPassword()).thenReturn(Bytes.toBytes(DEFAULT_USER_PASSWORD));
298    }
299    return token;
300  }
301
302  private HBaseSaslRpcClient createSaslRpcClientSimple(String principal, String password)
303      throws IOException {
304    return new HBaseSaslRpcClient(AuthMethod.SIMPLE, createTokenMock(), principal, false);
305  }
306
307  @SuppressWarnings("unchecked")
308  private Token<? extends TokenIdentifier> createTokenMock() {
309    return mock(Token.class);
310  }
311}