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.security.rpcauth;
019
020import java.io.ByteArrayInputStream;
021import java.io.DataInputStream;
022import java.io.IOException;
023import java.util.Map;
024
025import javax.security.auth.callback.Callback;
026import javax.security.auth.callback.CallbackHandler;
027import javax.security.auth.callback.NameCallback;
028import javax.security.auth.callback.PasswordCallback;
029import javax.security.auth.callback.UnsupportedCallbackException;
030import javax.security.sasl.AuthorizeCallback;
031import javax.security.sasl.RealmCallback;
032import javax.security.sasl.RealmChoiceCallback;
033import javax.security.sasl.Sasl;
034import javax.security.sasl.SaslClient;
035import javax.security.sasl.SaslServer;
036
037import org.apache.commons.codec.binary.Base64;
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040import org.apache.hadoop.ipc.Server;
041import org.apache.hadoop.security.AccessControlException;
042import org.apache.hadoop.security.SaslRpcServer;
043import org.apache.hadoop.security.UserGroupInformation;
044import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
045import org.apache.hadoop.security.token.SecretManager;
046import org.apache.hadoop.security.token.SecretManager.InvalidToken;
047import org.apache.hadoop.security.token.Token;
048import org.apache.hadoop.security.token.TokenIdentifier;
049
050//TODO: rename this class to TokenAuthMethod and use the mechanism as "token"
051// This is because UGI.AuthenticationMethod doesn't have DIGEST anymore and
052// hadoop is deprecating SaslRpcServer.AuthMethod which has DIGEST already
053// deprecated.
054public final class DigestAuthMethod extends RpcAuthMethod {
055  private static final Log LOG = LogFactory.getLog(DigestAuthMethod.class);
056
057  static final RpcAuthMethod INSTANCE = new DigestAuthMethod();
058  private DigestAuthMethod() {
059    super((byte) 82, "token", "DIGEST-MD5", AuthenticationMethod.TOKEN);
060  }
061
062  @Override
063  public boolean isProxyAllowed() {
064    return false;
065  }
066
067  @Override
068  @SuppressWarnings("unchecked")
069  public UserGroupInformation getAuthorizedUgi(String authorizedId,
070      SecretManager secretManager) throws IOException {
071    TokenIdentifier tokenId = getIdentifier(authorizedId, secretManager);
072    UserGroupInformation ugi = tokenId.getUser();
073    if (ugi == null) {
074      throw new AccessControlException(
075          "Can't retrieve username from tokenIdentifier.");
076    }
077    ugi.addTokenIdentifier(tokenId);
078    return ugi;
079  }
080
081  @Override
082  public boolean isSasl() {
083    return true;
084  }
085
086  @Override
087  public String getProtocol() throws IOException {
088    return SaslRpcServer.SASL_DEFAULT_REALM;
089  }
090
091  @Override
092  public String getServerId() throws IOException {
093    return "";
094  }
095
096  @Override
097  @SuppressWarnings("unchecked")
098  public SaslClient createSaslClient(final Map<String, Object> saslProperties)
099      throws IOException {
100    Token<? extends TokenIdentifier> token = (Token<? extends TokenIdentifier>)
101        saslProperties.get(SaslRpcServer.SASL_AUTH_TOKEN);
102    if (LOG.isDebugEnabled())
103      LOG.debug("Creating SASL " + mechanismName
104          + " client to authenticate to service at " + token.getService());
105    return Sasl.createSaslClient(new String[] { mechanismName },
106        null, null, SaslRpcServer.SASL_DEFAULT_REALM,
107        saslProperties, new SaslClientCallbackHandler(token));
108  }
109
110  @Override
111  @SuppressWarnings("unchecked")
112  public SaslServer createSaslServer(final Server.Connection connection,
113      final Map<String, Object> saslProperties)
114      throws IOException {
115    SecretManager<TokenIdentifier> secretManager = (SecretManager<TokenIdentifier>)
116        saslProperties.get(SaslRpcServer.SASL_AUTH_SECRET_MANAGER);
117    if (secretManager == null) {
118      throw new AccessControlException(
119          "Server is not configured to do DIGEST authentication.");
120    }
121    return Sasl.createSaslServer(mechanismName, null,
122        SaslRpcServer.SASL_DEFAULT_REALM, saslProperties,
123        new SaslDigestCallbackHandler(secretManager, connection));
124  }
125
126  public static char[] encodePassword(byte[] password) {
127    return new String(Base64.encodeBase64(password)).toCharArray();
128  }
129
130  public static <T extends TokenIdentifier> T getIdentifier(String id,
131      SecretManager<T> secretManager) throws InvalidToken {
132    byte[] tokenId = decodeIdentifier(id);
133    T tokenIdentifier = secretManager.createIdentifier();
134    try {
135      tokenIdentifier.readFields(new DataInputStream(new ByteArrayInputStream(
136          tokenId)));
137    } catch (IOException e) {
138      throw (InvalidToken) new InvalidToken(
139          "Can't de-serialize tokenIdentifier").initCause(e);
140    }
141    return tokenIdentifier;
142  }
143
144  public static String encodeIdentifier(byte[] identifier) {
145    return new String(Base64.encodeBase64(identifier));
146  }
147
148  public static byte[] decodeIdentifier(String identifier) {
149    return Base64.decodeBase64(identifier.getBytes());
150  }
151
152  private static class SaslClientCallbackHandler implements CallbackHandler {
153    private final String userName;
154    private final char[] userPassword;
155
156    public SaslClientCallbackHandler(Token<? extends TokenIdentifier> token) {
157      this.userName = encodeIdentifier(token.getIdentifier());
158      this.userPassword = encodePassword(token.getPassword());
159    }
160
161    @Override
162    public void handle(Callback[] callbacks)
163        throws UnsupportedCallbackException {
164      NameCallback nc = null;
165      PasswordCallback pc = null;
166      RealmCallback rc = null;
167      for (Callback callback : callbacks) {
168        if (callback instanceof RealmChoiceCallback) {
169          continue;
170        } else if (callback instanceof NameCallback) {
171          nc = (NameCallback) callback;
172        } else if (callback instanceof PasswordCallback) {
173          pc = (PasswordCallback) callback;
174        } else if (callback instanceof RealmCallback) {
175          rc = (RealmCallback) callback;
176        } else {
177          throw new UnsupportedCallbackException(callback,
178              "Unrecognized SASL client callback");
179        }
180      }
181      if (nc != null) {
182        if (LOG.isDebugEnabled())
183          LOG.debug("SASL client callback: setting username: " + userName);
184        nc.setName(userName);
185      }
186      if (pc != null) {
187        if (LOG.isDebugEnabled())
188          LOG.debug("SASL client callback: setting userPassword");
189        pc.setPassword(userPassword);
190      }
191      if (rc != null) {
192        if (LOG.isDebugEnabled())
193          LOG.debug("SASL client callback: setting realm: "
194              + rc.getDefaultText());
195        rc.setText(rc.getDefaultText());
196      }
197    }
198  }
199
200  /** CallbackHandler for SASL DIGEST-MD5 mechanism */
201  public static class SaslDigestCallbackHandler implements CallbackHandler {
202    private SecretManager<TokenIdentifier> secretManager;
203    private Server.Connection connection;
204
205    public SaslDigestCallbackHandler(SecretManager<TokenIdentifier> secretManager,
206        Server.Connection connection) {
207      this.secretManager = secretManager;
208      this.connection = connection;
209    }
210
211    private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken {
212      return encodePassword(secretManager.retrievePassword(tokenid));
213    }
214
215    /** {@inheritDoc} */
216    @Override
217    public void handle(Callback[] callbacks) throws InvalidToken,
218        UnsupportedCallbackException {
219      NameCallback nc = null;
220      PasswordCallback pc = null;
221      AuthorizeCallback ac = null;
222      for (Callback callback : callbacks) {
223        if (callback instanceof AuthorizeCallback) {
224          ac = (AuthorizeCallback) callback;
225        } else if (callback instanceof NameCallback) {
226          nc = (NameCallback) callback;
227        } else if (callback instanceof PasswordCallback) {
228          pc = (PasswordCallback) callback;
229        } else if (callback instanceof RealmCallback) {
230          continue; // realm is ignored
231        } else {
232          throw new UnsupportedCallbackException(callback,
233              "Unrecognized SASL DIGEST-MD5 Callback");
234        }
235      }
236      if (pc != null) {
237        TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(), secretManager);
238        char[] password = getPassword(tokenIdentifier);
239        UserGroupInformation user = null;
240        user = tokenIdentifier.getUser(); // may throw exception
241        connection.attemptingUser = user;
242        if (SaslRpcServer.LOG.isDebugEnabled()) {
243          SaslRpcServer.LOG.debug("SASL server DIGEST-MD5 callback: setting password "
244              + "for client: " + tokenIdentifier.getUser());
245        }
246        pc.setPassword(password);
247      }
248      if (ac != null) {
249        String authid = ac.getAuthenticationID();
250        String authzid = ac.getAuthorizationID();
251        if (authid.equals(authzid)) {
252          ac.setAuthorized(true);
253        } else {
254          ac.setAuthorized(false);
255        }
256        if (ac.isAuthorized()) {
257          if (SaslRpcServer.LOG.isDebugEnabled()) {
258            String username =
259              getIdentifier(authzid, secretManager).getUser().getUserName();
260            SaslRpcServer.LOG.debug("SASL server DIGEST-MD5 callback: setting "
261                + "canonicalized client ID: " + username);
262          }
263          ac.setAuthorizedID(authzid);
264        }
265      }
266    }
267  }
268}