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 */
018
019package org.apache.hadoop.security;
020
021import java.io.ByteArrayInputStream;
022import java.io.DataInput;
023import java.io.DataInputStream;
024import java.io.DataOutput;
025import java.io.IOException;
026import java.util.Map;
027import java.util.TreeMap;
028
029import javax.security.auth.callback.Callback;
030import javax.security.auth.callback.CallbackHandler;
031import javax.security.auth.callback.NameCallback;
032import javax.security.auth.callback.PasswordCallback;
033import javax.security.auth.callback.UnsupportedCallbackException;
034import javax.security.sasl.AuthorizeCallback;
035import javax.security.sasl.RealmCallback;
036import javax.security.sasl.Sasl;
037
038import org.apache.commons.codec.binary.Base64;
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.apache.hadoop.classification.InterfaceAudience;
042import org.apache.hadoop.classification.InterfaceStability;
043import org.apache.hadoop.conf.Configuration;
044import org.apache.hadoop.ipc.Server;
045import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
046import org.apache.hadoop.security.token.SecretManager;
047import org.apache.hadoop.security.token.TokenIdentifier;
048import org.apache.hadoop.security.token.SecretManager.InvalidToken;
049
050/**
051 * A utility class for dealing with SASL on RPC server
052 */
053@InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
054@InterfaceStability.Evolving
055public class SaslRpcServer {
056  public static final Log LOG = LogFactory.getLog(SaslRpcServer.class);
057  public static final String SASL_DEFAULT_REALM = "default";
058  public static final Map<String, String> SASL_PROPS = 
059      new TreeMap<String, String>();
060
061  public static final int SWITCH_TO_SIMPLE_AUTH = -88;
062
063  public static enum QualityOfProtection {
064    AUTHENTICATION("auth"),
065    INTEGRITY("auth-int"),
066    PRIVACY("auth-conf");
067    
068    public final String saslQop;
069    
070    private QualityOfProtection(String saslQop) {
071      this.saslQop = saslQop;
072    }
073    
074    public String getSaslQop() {
075      return saslQop;
076    }
077  }
078  
079  public static void init(Configuration conf) {
080    QualityOfProtection saslQOP = QualityOfProtection.AUTHENTICATION;
081    String rpcProtection = conf.get("hadoop.rpc.protection",
082        QualityOfProtection.AUTHENTICATION.name().toLowerCase());
083    if (QualityOfProtection.INTEGRITY.name().toLowerCase()
084        .equals(rpcProtection)) {
085      saslQOP = QualityOfProtection.INTEGRITY;
086    } else if (QualityOfProtection.PRIVACY.name().toLowerCase().equals(
087        rpcProtection)) {
088      saslQOP = QualityOfProtection.PRIVACY;
089    }
090    
091    SASL_PROPS.put(Sasl.QOP, saslQOP.getSaslQop());
092    SASL_PROPS.put(Sasl.SERVER_AUTH, "true");
093  }
094  
095  static String encodeIdentifier(byte[] identifier) {
096    return new String(Base64.encodeBase64(identifier));
097  }
098
099  static byte[] decodeIdentifier(String identifier) {
100    return Base64.decodeBase64(identifier.getBytes());
101  }
102
103  public static <T extends TokenIdentifier> T getIdentifier(String id,
104      SecretManager<T> secretManager) throws InvalidToken {
105    byte[] tokenId = decodeIdentifier(id);
106    T tokenIdentifier = secretManager.createIdentifier();
107    try {
108      tokenIdentifier.readFields(new DataInputStream(new ByteArrayInputStream(
109          tokenId)));
110    } catch (IOException e) {
111      throw (InvalidToken) new InvalidToken(
112          "Can't de-serialize tokenIdentifier").initCause(e);
113    }
114    return tokenIdentifier;
115  }
116
117  static char[] encodePassword(byte[] password) {
118    return new String(Base64.encodeBase64(password)).toCharArray();
119  }
120
121  /** Splitting fully qualified Kerberos name into parts */
122  public static String[] splitKerberosName(String fullName) {
123    return fullName.split("[/@]");
124  }
125
126  @InterfaceStability.Evolving
127  public enum SaslStatus {
128    SUCCESS (0),
129    ERROR (1);
130    
131    public final int state;
132    private SaslStatus(int state) {
133      this.state = state;
134    }
135  }
136  
137  /** Authentication method */
138  @InterfaceStability.Evolving
139  public static enum AuthMethod {
140    SIMPLE((byte) 80, "", AuthenticationMethod.SIMPLE),
141    KERBEROS((byte) 81, "GSSAPI", AuthenticationMethod.KERBEROS),
142    DIGEST((byte) 82, "DIGEST-MD5", AuthenticationMethod.TOKEN);
143
144    /** The code for this method. */
145    public final byte code;
146    public final String mechanismName;
147    public final AuthenticationMethod authenticationMethod;
148
149    private AuthMethod(byte code, String mechanismName, 
150                       AuthenticationMethod authMethod) {
151      this.code = code;
152      this.mechanismName = mechanismName;
153      this.authenticationMethod = authMethod;
154    }
155
156    private static final int FIRST_CODE = values()[0].code;
157
158    /** Return the object represented by the code. */
159    private static AuthMethod valueOf(byte code) {
160      final int i = (code & 0xff) - FIRST_CODE;
161      return i < 0 || i >= values().length ? null : values()[i];
162    }
163
164    /** Return the SASL mechanism name */
165    public String getMechanismName() {
166      return mechanismName;
167    }
168
169    /** Read from in */
170    public static AuthMethod read(DataInput in) throws IOException {
171      return valueOf(in.readByte());
172    }
173
174    /** Write to out */
175    public void write(DataOutput out) throws IOException {
176      out.write(code);
177    }
178  };
179
180  /** CallbackHandler for SASL DIGEST-MD5 mechanism */
181  @InterfaceStability.Evolving
182  public static class SaslDigestCallbackHandler implements CallbackHandler {
183    private SecretManager<TokenIdentifier> secretManager;
184    private Server.Connection connection; 
185    
186    public SaslDigestCallbackHandler(
187        SecretManager<TokenIdentifier> secretManager,
188        Server.Connection connection) {
189      this.secretManager = secretManager;
190      this.connection = connection;
191    }
192
193    private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken {
194      return encodePassword(secretManager.retrievePassword(tokenid));
195    }
196
197    /** {@inheritDoc} */
198    @Override
199    public void handle(Callback[] callbacks) throws InvalidToken,
200        UnsupportedCallbackException {
201      NameCallback nc = null;
202      PasswordCallback pc = null;
203      AuthorizeCallback ac = null;
204      for (Callback callback : callbacks) {
205        if (callback instanceof AuthorizeCallback) {
206          ac = (AuthorizeCallback) callback;
207        } else if (callback instanceof NameCallback) {
208          nc = (NameCallback) callback;
209        } else if (callback instanceof PasswordCallback) {
210          pc = (PasswordCallback) callback;
211        } else if (callback instanceof RealmCallback) {
212          continue; // realm is ignored
213        } else {
214          throw new UnsupportedCallbackException(callback,
215              "Unrecognized SASL DIGEST-MD5 Callback");
216        }
217      }
218      if (pc != null) {
219        TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(), secretManager);
220        char[] password = getPassword(tokenIdentifier);
221        UserGroupInformation user = null;
222        user = tokenIdentifier.getUser(); // may throw exception
223        connection.attemptingUser = user;
224        
225        if (LOG.isDebugEnabled()) {
226          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 (LOG.isDebugEnabled()) {
241            String username =
242              getIdentifier(authzid, secretManager).getUser().getUserName();
243            LOG.debug("SASL server DIGEST-MD5 callback: setting "
244                + "canonicalized client ID: " + username);
245          }
246          ac.setAuthorizedID(authzid);
247        }
248      }
249    }
250  }
251
252  /** CallbackHandler for SASL GSSAPI Kerberos mechanism */
253  @InterfaceStability.Evolving
254  public static class SaslGssCallbackHandler implements CallbackHandler {
255
256    /** {@inheritDoc} */
257    @Override
258    public void handle(Callback[] callbacks) throws
259        UnsupportedCallbackException {
260      AuthorizeCallback ac = null;
261      for (Callback callback : callbacks) {
262        if (callback instanceof AuthorizeCallback) {
263          ac = (AuthorizeCallback) callback;
264        } else {
265          throw new UnsupportedCallbackException(callback,
266              "Unrecognized SASL GSSAPI Callback");
267        }
268      }
269      if (ac != null) {
270        String authid = ac.getAuthenticationID();
271        String authzid = ac.getAuthorizationID();
272        if (authid.equals(authzid)) {
273          ac.setAuthorized(true);
274        } else {
275          ac.setAuthorized(false);
276        }
277        if (ac.isAuthorized()) {
278          if (LOG.isDebugEnabled())
279            LOG.debug("SASL server GSSAPI callback: setting "
280                + "canonicalized client ID: " + authzid);
281          ac.setAuthorizedID(authzid);
282        }
283      }
284    }
285  }
286}