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    
019    package org.apache.hadoop.security;
020    
021    import java.io.ByteArrayInputStream;
022    import java.io.DataInput;
023    import java.io.DataInputStream;
024    import java.io.DataOutput;
025    import java.io.IOException;
026    import java.util.Map;
027    import java.util.TreeMap;
028    
029    import javax.security.auth.callback.Callback;
030    import javax.security.auth.callback.CallbackHandler;
031    import javax.security.auth.callback.NameCallback;
032    import javax.security.auth.callback.PasswordCallback;
033    import javax.security.auth.callback.UnsupportedCallbackException;
034    import javax.security.sasl.AuthorizeCallback;
035    import javax.security.sasl.RealmCallback;
036    import javax.security.sasl.Sasl;
037    
038    import org.apache.commons.codec.binary.Base64;
039    import org.apache.commons.logging.Log;
040    import org.apache.commons.logging.LogFactory;
041    import org.apache.hadoop.classification.InterfaceAudience;
042    import org.apache.hadoop.classification.InterfaceStability;
043    import org.apache.hadoop.conf.Configuration;
044    import org.apache.hadoop.ipc.Server;
045    import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
046    import org.apache.hadoop.security.token.SecretManager;
047    import org.apache.hadoop.security.token.TokenIdentifier;
048    import 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
055    public 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    }