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