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 java.io.ByteArrayInputStream;
021import java.io.DataInputStream;
022import java.io.IOException;
023import java.security.PrivilegedExceptionAction;
024import java.util.Map;
025
026import javax.security.auth.callback.Callback;
027import javax.security.auth.callback.CallbackHandler;
028import javax.security.auth.callback.NameCallback;
029import javax.security.auth.callback.PasswordCallback;
030import javax.security.auth.callback.UnsupportedCallbackException;
031import javax.security.sasl.AuthorizeCallback;
032import javax.security.sasl.RealmCallback;
033import javax.security.sasl.Sasl;
034import javax.security.sasl.SaslException;
035import javax.security.sasl.SaslServer;
036
037import org.apache.yetus.audience.InterfaceAudience;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040import org.apache.hadoop.security.UserGroupInformation;
041import org.apache.hadoop.security.token.SecretManager;
042import org.apache.hadoop.security.token.SecretManager.InvalidToken;
043import org.apache.hadoop.security.token.TokenIdentifier;
044
045/**
046 * A utility class that encapsulates SASL logic for RPC server. Copied from
047 * <code>org.apache.hadoop.security</code>
048 */
049@InterfaceAudience.Private
050public class HBaseSaslRpcServer {
051
052  private static final Logger LOG = LoggerFactory.getLogger(HBaseSaslRpcServer.class);
053
054  private final SaslServer saslServer;
055
056  private UserGroupInformation attemptingUser; // user name before auth
057
058  public HBaseSaslRpcServer(AuthMethod method, Map<String, String> saslProps,
059      SecretManager<TokenIdentifier> secretManager) throws IOException {
060    switch (method) {
061      case DIGEST:
062        if (secretManager == null) {
063          throw new AccessDeniedException("Server is not configured to do DIGEST authentication.");
064        }
065        saslServer = Sasl.createSaslServer(AuthMethod.DIGEST.getMechanismName(), null,
066          SaslUtil.SASL_DEFAULT_REALM, saslProps, new SaslDigestCallbackHandler(secretManager));
067        break;
068      case KERBEROS:
069        UserGroupInformation current = UserGroupInformation.getCurrentUser();
070        String fullName = current.getUserName();
071        if (LOG.isDebugEnabled()) {
072          LOG.debug("Kerberos principal name is " + fullName);
073        }
074        String[] names = SaslUtil.splitKerberosName(fullName);
075        if (names.length != 3) {
076          throw new AccessDeniedException(
077              "Kerberos principal name does NOT have the expected " + "hostname part: " + fullName);
078        }
079        try {
080          saslServer = current.doAs(new PrivilegedExceptionAction<SaslServer>() {
081            @Override
082            public SaslServer run() throws SaslException {
083              return Sasl.createSaslServer(AuthMethod.KERBEROS.getMechanismName(), names[0],
084                names[1], saslProps, new SaslGssCallbackHandler());
085            }
086          });
087        } catch (InterruptedException e) {
088          // should not happen
089          throw new AssertionError(e);
090        }
091        break;
092      default:
093        throw new IOException("Unknown authentication method " + method);
094    }
095  }
096
097  public boolean isComplete() {
098    return saslServer.isComplete();
099  }
100
101  public byte[] evaluateResponse(byte[] response) throws SaslException {
102    return saslServer.evaluateResponse(response);
103  }
104
105  /** Release resources used by wrapped saslServer */
106  public void dispose() {
107    SaslUtil.safeDispose(saslServer);
108  }
109
110  public UserGroupInformation getAttemptingUser() {
111    return attemptingUser;
112  }
113
114  public byte[] wrap(byte[] buf, int off, int len) throws SaslException {
115    return saslServer.wrap(buf, off, len);
116  }
117
118  public byte[] unwrap(byte[] buf, int off, int len) throws SaslException {
119    return saslServer.unwrap(buf, off, len);
120  }
121
122  public String getNegotiatedQop() {
123    return (String) saslServer.getNegotiatedProperty(Sasl.QOP);
124  }
125
126  public String getAuthorizationID() {
127    return saslServer.getAuthorizationID();
128  }
129
130  public static <T extends TokenIdentifier> T getIdentifier(String id,
131      SecretManager<T> secretManager) throws InvalidToken {
132    byte[] tokenId = SaslUtil.decodeIdentifier(id);
133    T tokenIdentifier = secretManager.createIdentifier();
134    try {
135      tokenIdentifier.readFields(new DataInputStream(new ByteArrayInputStream(tokenId)));
136    } catch (IOException e) {
137      throw (InvalidToken) new InvalidToken("Can't de-serialize tokenIdentifier").initCause(e);
138    }
139    return tokenIdentifier;
140  }
141
142  /** CallbackHandler for SASL DIGEST-MD5 mechanism */
143  private class SaslDigestCallbackHandler implements CallbackHandler {
144    private SecretManager<TokenIdentifier> secretManager;
145
146    public SaslDigestCallbackHandler(SecretManager<TokenIdentifier> secretManager) {
147      this.secretManager = secretManager;
148    }
149
150    private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken {
151      return SaslUtil.encodePassword(secretManager.retrievePassword(tokenid));
152    }
153
154    /** {@inheritDoc} */
155    @Override
156    public void handle(Callback[] callbacks) throws InvalidToken, UnsupportedCallbackException {
157      NameCallback nc = null;
158      PasswordCallback pc = null;
159      AuthorizeCallback ac = null;
160      for (Callback callback : callbacks) {
161        if (callback instanceof AuthorizeCallback) {
162          ac = (AuthorizeCallback) callback;
163        } else if (callback instanceof NameCallback) {
164          nc = (NameCallback) callback;
165        } else if (callback instanceof PasswordCallback) {
166          pc = (PasswordCallback) callback;
167        } else if (callback instanceof RealmCallback) {
168          continue; // realm is ignored
169        } else {
170          throw new UnsupportedCallbackException(callback, "Unrecognized SASL DIGEST-MD5 Callback");
171        }
172      }
173      if (pc != null) {
174        TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(), secretManager);
175        char[] password = getPassword(tokenIdentifier);
176        UserGroupInformation user = tokenIdentifier.getUser(); // may throw exception
177        attemptingUser = user;
178        if (LOG.isTraceEnabled()) {
179          LOG.trace("SASL server DIGEST-MD5 callback: setting password " + "for client: " +
180              tokenIdentifier.getUser());
181        }
182        pc.setPassword(password);
183      }
184      if (ac != null) {
185        String authid = ac.getAuthenticationID();
186        String authzid = ac.getAuthorizationID();
187        if (authid.equals(authzid)) {
188          ac.setAuthorized(true);
189        } else {
190          ac.setAuthorized(false);
191        }
192        if (ac.isAuthorized()) {
193          if (LOG.isTraceEnabled()) {
194            String username = getIdentifier(authzid, secretManager).getUser().getUserName();
195            LOG.trace(
196              "SASL server DIGEST-MD5 callback: setting " + "canonicalized client ID: " + username);
197          }
198          ac.setAuthorizedID(authzid);
199        }
200      }
201    }
202  }
203
204  /** CallbackHandler for SASL GSSAPI Kerberos mechanism */
205  private static class SaslGssCallbackHandler implements CallbackHandler {
206
207    /** {@inheritDoc} */
208    @Override
209    public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
210      AuthorizeCallback ac = null;
211      for (Callback callback : callbacks) {
212        if (callback instanceof AuthorizeCallback) {
213          ac = (AuthorizeCallback) callback;
214        } else {
215          throw new UnsupportedCallbackException(callback, "Unrecognized SASL GSSAPI Callback");
216        }
217      }
218      if (ac != null) {
219        String authid = ac.getAuthenticationID();
220        String authzid = ac.getAuthorizationID();
221        if (authid.equals(authzid)) {
222          ac.setAuthorized(true);
223        } else {
224          ac.setAuthorized(false);
225        }
226        if (ac.isAuthorized()) {
227          if (LOG.isDebugEnabled()) {
228            LOG.debug(
229              "SASL server GSSAPI callback: setting " + "canonicalized client ID: " + authzid);
230          }
231          ac.setAuthorizedID(authzid);
232        }
233      }
234    }
235  }
236}